volatile关键字
1.并发编程的三大性质
- 原子性:一个操作不可分割:要么执行并且执行的过程不会被打断,要么就不执行。
- 可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
- 有序性:程序执行语句的顺序按照代码的先后顺序执行。有时候为了优化性能,编译器会对字节码指令进行重排序。但是能保证程序最终结果与重排序之前是一致的。
Java内存模型只保证了基本读取和赋值是原子性操作(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)。
自增操作是不具备原子性的,它包括读取变量的原始值、进行加1操作、写入工作内存。那么就是说自增操作的三个子操作可能会分割开执行。
2.volatitle作用
保证 可见性 和 有序性。
- 可见性
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。 - 有序性
禁止进行指令重排序。
1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
对已读取的值:
- volatile的可见性只能保证写入的时候从缓存到主内存的这段过程不会被影响,而无法保证已经读取的值是已经被修改过的。
- volatile保证你每次读取都能读到最新的值,可是并不会更新你已经读了的值,它也无法更新你已经读了的值。
通常来说,使用volatile必须具备以下2个条件:
- 1)对变量的写操作不依赖于当前值
- 2)该变量没有包含在具有其他变量的不变式中
上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。
更大范围操作的原子性,可以通过synchronized和Lock来实现。
由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,自然就保证了有序性。
举例
新建对象 DoubleCheckLock instance = new DoubleCheckLock();
这一操作并不是一个原子操作,实例化对象的字节指令可以分为三步,如下:
1.分配对象内存:memory = allocate();
2.初始化对象:instance(memory);
3.instance指向刚分配的内存地址:instance = memory;
而由于编译器的指令重排序,以上指令可能会出现以下顺序:
1.分配对象内存:memory = allocate();
2.instance指向刚分配的内存地址:instance = memory;
3.初始化对象:instance(memory);
导致第二个线程调用instance对象时,只分配了内存地址,不为null但没有初始化,调用其方法时报错。
参考:
http://blog.csdn.net/javazejian/article/details/72772461
http://zhangpan.site/2021/05/30/37-jmm-volatile/