其实这个问题过于简洁了,应该叫做代码在并发情况下是线程安全必须要满足的三大特性
- 原子性:一个或多个操作,要么都做要么都不做,执行过程中不能被打断
比如说现在有个i++操作,底层实际上是4步
1.将i从主存读到工作内存
2.将i放到操作树栈上执行+1操作
3.将结果写入工作内存
4.将工作内存上的值刷回主存
现在i初始化为0,线程A执行i++到第二步,CPU发生调度,让线程B执行i++,最后线程A刷回主存i=1,
线程B刷回主存i=1,最后结果是不正确的
解决方案:(1)CAS(2)synchronized锁和Lock锁
- 可见性:当一个线程修改了共享变量的值,其他的线程能够看到修改的值
比如最容易复现的一个例子,有个flag的布尔值为true
线程A执行这样的代码,死循环判断如果flag为true,就输出1。然后10秒后,线程B将flag修改为false,
但是这个时候线程A却没有停下来
主要原因:线程A由于多次访问flag为true,索性就把flag=true读取缓存,即使线程B修改了flag,线程A也不从主从
读取,而是从缓存中读取错误的结果
解决方案:(1)volatile关键字。我们计算机有一个缓存一致性协议MESI和总线嗅探机制去实现这个协议,比如有个线程修改了缓存中的共享变量,这个共享变量会迅速刷回主存。途中经过总线,其他线程监听到总线中这个值变化,会将这个共享变量的缓存设置为失效,下一次必须从主存中读取,volatile作用就是标志这个变量是一个共享变量
- 有序性:程序的执行按照代码的先后顺序执行
最经典的就是单例模式,如果使用反编译命令javap -v查看,new一个对象实际上是三步
1.开辟一块空间
2.初始化对象
3.返回对象地址
但是JVM有可能会进行指令重排,实际步骤很可能会是1 3 2,在单线程情况下没有问题,但是在多线程情况下,
如果是1 3 2的执行步骤,线程A执行到了3,这时候CPU调度,线程B调用了该方法,就会报空指针异常
解决方案:(1)volatile关键字。标志这个对象不要进行指令重排