一、 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() {
@Override
public 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条字节码指令构成的,自增操作不具备原子性
getstatic
iconst_1
add
putstatic
当执行任何一条指令的时候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; // 1
public static Instance getInstance(){
if( instance == null){ // 2
synchronized (safeDoubleChenkedLocking.class){ // 3
if( instance == null){ // 4
instance = new Instance();
}
}
}
return instance;
}
}
- synchroinzed关键字加上类锁是为了防止多线程的并发问题。
- //2 中第一次判断为空,为了提高效率,减少加锁的成本。但是必须保证共享变量是volatile修饰的,因为如果不是volatile修饰的话会出现指令重排问题,另一个线程 执行了 为对象分配内存,instance 指向了内存。还没来得及初始化对象,就直接返回对象。
- 防止 线程A通过了 //2的校验,此刻线程B也通过了//2的判断,然后之后两个线程都会走一遍同步代码块,所以同步代码块内部也需要加一次判空操作。