一、原子性、可见性
volatile与synchronized都具有原子性和可见性,但底层实现不同。
1、线程间的通信-共享数据
介绍原子性和可见性之前,先介绍一下共享数据如何在线程间通信。
实例是存在堆内存中的,堆内存在线程间共享。局部变量、方法不会在线程间共享,所以不会有可见性问题。共享变量存储在堆内存中,为了加快执行效率,每个线程都有一块私有的本地内存,私有的本地内存存储了共享变量的副本。接下来线程从本地内存中读取共享变量,不从堆内存中读取。例如两个线程,线程A在修改共享变量后,会刷新堆内存的共享变量,线程B也会同步堆内存的值。但是线程A具体什么时候刷新堆内存的数据是不确定的,线程B什么时候同步堆内存的值也是不确定的。
2、volatile原子性、可见性的实现
对于声明了volatile的变量进行写操作的时候,JVM会把这个变量所在的缓存行数据立即写回到堆内存。同时,JMM将其他线程本地内存中的共享变量的值会置为失效,其他线程在读取变量时,会从堆内存中重新读取。
volatile对变量的单个读/写具有原子性,但不能做到复合操作的原子性,例如volatile++。如果要实现复合操作的原子性,建议使用java.util.concurrent.atomic包下的Atomic类;
3、synchronized原子性、可见性实现
synchronized是将锁释放后,会把数据同步到堆内存中;另一个线程获取锁后,会从堆内存同步共享数据到线程本地内存中。
现在通过使用javap工具查看生成的class文件信息来分析synchronized关键字的实现细节。
public class SynchronizedJVM {
public void method1() {
synchronized (this) {
}
}
public synchronized void method2() {
}
}
在class路径下执行javap -v SynchronizedJVM.class:
{
public com.study.multiThread.SynchronizedJVM();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #8 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/study/multiThread/SynchronizedJVM;
public void method1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: dup
2: monitorenter
3: monitorexit
4: return
LineNumberTable:
line 6: 0
line 8: 4
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/study/multiThread/SynchronizedJVM;
public synchronized void method2();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 12: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this Lcom/study/multiThread/SynchronizedJVM;
}
可以看到synchronized同步块的实现使用了monitorenter和monitorexit指令,而同步方法是依靠方法修饰符上的ACC_SYNCHRONIZED来完成的。无论采用哪种方式,本质是对一个对象的监视器(monitor)进行获取,而这个获取过程是排他的,也就是同一时刻只能有一个线程获取到synchronized所保护的对象监视器。线程想要执行同步块中的方法,先要通过monitorenter获得对象的监视器,如果失败,就进入对象的同步队列,线程进入blocked状态;如果成功,线程就可以进入同步代码块。获得锁的线程执行monitorexit指令后,会唤醒阻塞在同步队列中的线程,同步队列中的线程再次通过monitorenter争夺对象的监视器(monitor)。
二、volatile禁止指令重排序
volatile禁止指令重排序,典型的应用是单例模式中的双重检查机制。
由于synchronized是一个重量级锁,会影响运行效率。双重检查机制中,先判断单例变量是否为null,再进入同步代码块。这样只在第一次获取单例变量时,会有效率影响。
public class DoubleCheckSingleton {
private volatile static DoubleCheckSingleton singleton;
private DoubleCheckSingleton() {}
public static DoubleCheckSingleton getInstance() {
if(singleton == null) {
synchronized (singleton) {
if(singleton == null) {
singleton = new DoubleCheckSingleton();
}
}
}
return singleton;
}
}
单例变量需要用volatile修饰,否则,在new单例对象时会出现问题。使用new来创建一个对象可以分解为如下的3行伪代码:
memory = allocate(); //1.分配对象的内存空间
ctorInstance(memory); //2.初始化对象
instance = memory; //3.设置instance指向刚分配的内存地址
上面3行伪代码中的2和3之间可能会发生重排序,排序后的执行顺序如下:
memory = allocate(); //1.分配对象的内存空间
instance = memory; //3.设置instance指向刚分配的内存地址,此时对象还没有初始化,但instance == null 判断为false
ctorInstance(memory); //2.初始化对象
指令重排序后,在多线程情况下,可能会发生A线程正在new对象,执行了3,但还没有执行2。此时B线程进入方法获取单例对象,执行同步代码块外的非空判断,发现变量非空,但此时对象还未初始化,B线程获取到的是一个未被初始化的对象。
使用volatile修饰后,禁止指令重排序。即,先初始化对象后,再设置instance指向刚分配的内存地址。这样就就不存在获取到未被初始化的对象。
三、其他区别
- volatile仅能使用在变量级别;synchronized则可以使用在代码块和方法上;
- volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。