一、 volatile的应用
在多线程并发编程中synchronized和volatile都扮演着重要的角色,volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的可见性。
如果volatile变量使用恰当的话,比synchronized的使用和执行成本更低,因为它不会引起线程上下文切换和调度。
1. volatile的定义与实现原理
Java允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该通过排它锁单独获得这个变量。Java提供了volatile,在某些情况下比锁更方便。
如果一个字段被声明为volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。
CPU术语
内存屏障:是一组处理器指令,用户实现对内存操作的顺序限制。 缓存行:CPU高速缓存中可以分配的最小存储单位。
1、可见性
volatile变量在各个线程的工作内存中不存在一致性问题,但是java里面的运算并非原子操作,导致volatile变量的运算在并发下一样是不安全的
各个线程的工作内存中,volatile变量也可以存在不一致的情况,由于每次使用前都要先刷新,执行引擎看不到不一致的情况,因此可以认为不存在一致性问题
X86处理器下通过工具获取JIT编译器生成的汇编指令来查看对volatile进行写操作时,CPU会做什么事情
Java代码如下:
Singletion instance = new Singletion(); //instance是volatile变量
汇编代码如下:lock addl
0x01a3deld: movb $0x0,0x1104800(%esi);0x01a3de24: lock addl $0x0,(%esp);
有valatile修饰的共享变量进行写操作的时候会多出来第二行汇编代码,Lock前缀的指令在多核处理器下会引发了两件事
1)将当前处理器缓存行的数据写回到系统内存。
2)这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。
1.1 volatile变量运算并发不安全eg:
public class VolatileTest {public static volatile int race = 0;public static final int COUNT = 20;public static void increase() {race++;}public static void main(String[] args) {Thread[] threads = new Thread[COUNT];for (int i = 0; i < COUNT; i++) {threads[i] = new Thread(new Runnable() {@Overridepublic void run() {for (int j = 0; j < 10000; j++) {increase();}}});threads[i].start();}while (Thread.activeCount() > 1) {Thread.yield();}System.out.println(race);}}
该示例发起20个线程,每个线程对race进行1w次自增操作,如果正确并发race应该等于200000
Expect: race == 200000
Actual:race总是小于预期值
问题产生的原因是由于race++实际是由4条字节码指令构成的,自增操作不具备原子性
getstaticiconst_1addputstatic当执行任何一条指令的时候race的值都有可能被别的线程修改,当前线程还是会把当前不准确的数值赋值给race。
一条字节码指令也并不意味这条指令就是原子操作。
1.2 volatile控制并发的场景举例
volatile boolean shutdownRequested;public void shutdown() {shutdownRequested = true;}public void doWork() {while(!shutdownRequested){// do somthing}}
如上示例中,当shutdown()方法被调用时,能保证所有线程的doWork方法都停下来。
2、禁止指令重排优化
双重检查锁定(DCL)
双重检查锁标准代码
public class safeDoubleChenkedLocking {private volatile static Instance instance; // 1public static Instance getInstance(){if( instance == null){ // 2synchronized (safeDoubleChenkedLocking.class){ // 3if( instance == null){ // 4instance = new Instance();}}}return instance;}}
- synchroinzed关键字加上类锁是为了防止多线程的并发问题。
- //2 中第一次判断为空,为了提高效率,减少加锁的成本。但是必须保证共享变量是volatile修饰的,因为如果不是volatile修饰的话会出现指令重排问题,另一个线程 执行了 为对象分配内存,instance 指向了内存。还没来得及初始化对象,就直接返回对象。
- 防止 线程A通过了 //2的校验,此刻线程B也通过了//2的判断,然后之后两个线程都会走一遍同步代码块,所以同步代码块内部也需要加一次判空操作。
