关键字volatile可以说是Java虚拟机提供的最轻量级的同步机制,当一个变量定义为volatile之后,它将具有两种特性。
第一是保证此变量对所有线程的可见性
这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。而普通变量不能做到这一点,普通变量的值在线程间传递均需要通过主内存来完成,例如,线程A修改一个普通变量的值,然后向主内存进行回写,另外一条线程B在线程A回写完成了之后再从主内存进行读取操作,新变量值才会对线程B可见。
举个例子,如下代码,如果对num 不加volatile关键字,我们会发现,主线程会一直卡在while循环,导致死循环,如果加了volatile关键字以后,“线程1获取num为”和“主线程第二次获取num为”这两句话会同时打印出来。并且它们打印出来的时间会一模一样,说明加了volatile关键字的变量会实时对所有线程可见。
public class MainThread {
volatile static int num = 0;
public static void main(String[] args) throws Exception {
new Thread(new Runnable() {
@Override
public void run() {
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
num = 3;
System.out.println("线程1获取num为" + num + " 时间为" + System.currentTimeMillis());
}
}).start();
System.out.println("主线程第一次获取num为" + num);
while (num == 0) {
//死循环
}
System.out.println("主线程第二次获取num为" + num + " 时间为" + System.currentTimeMillis());
}
}
第二个语义是禁止指令重排序优化
普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。因为在一个线程的方法执行过程中无法感知到这点,这也就是Java内存模型中描述的所谓的“线程内表现为串行的语义”(Within-Thread As-If-Serial Semantics)。
举个例子,DCL(double check lock)双重检查锁定版本的单例模式。
public class Singleton {
private volatile static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
public static void main(String[] args) {
Singleton.getInstance();
}
}
上面这个代码简单分为以下三个步骤。
1 给instance分配内存
2 调用instance的构造函数初始化对象
3 将instance指向分配的内存空间但是 步骤2和步骤3并没有明显的先后顺序,谁先谁后都可以顺利执行。如果没有volatile关键字进行修饰,可能出现先分配内存空间了,但是并没有初始化对象,volatile关键字就相当于一个内存屏障,指令排序的时候不能把后面的指令重排序到内存屏障之前的位置。
我们都知道Java并发的三大特性,原子性,可见性和有序性,volatile只能保证可见性和有序性,并不能保证原子性,我们在多线程环境下,多一个数字做连续+1处理,循环一百遍,那这个数字最后的结果一定是增加了一百吗,代码如下,运行起来,可以看到每次的结果都是不一样的,但是有一个共性,最终结果都会小于1000,结合前面一篇文章就非常好理解了,因为每个Java线程都有自己的工作内存,所以每次num++对其他Java线程而言并不是立即可见的,最终导致结果小于1000.,那我们对num加上关键字volatile再试试,代码如下
public class MainThread {
static int num = 0;
public static void main(String[] args) throws Exception {
Object lock = new Object();
for (int i = 0; i < 10000; i++) {
new Thread(new Runnable() {
@Override
public void run() {
num++;
}
}).start();
}
while (Thread.activeCount() > 1){
Thread.yield();
}
System.out.println("num = " + num);
}
}
我们会发现最终的结果始终低于10000,这是因为volatile并不能保证原子性,可能多个线程都同时获取了1,并对其做了++处理,导致最终结果小于10000,如果我们想最终结果为10000、我们可以利用synchronized来实现lock,synchronized实现代码如下。
public class MainThread {
volatile static int num = 0;
public static void main(String[] args) throws Exception {
Object lock = new Object();
for (int i = 0; i < 10000; i++) {
new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
num++;
}
}
}).start();
}
while (Thread.activeCount() > 1) {
Thread.yield();
}
System.out.println("num = " + num);
}
}
Lock实现代码如下
public class MainThread {
volatile static int num = 0;
public static void main(String[] args) throws Exception {
Lock lock = new ReentrantLock();
for (int i = 0; i < 10000; i++) {
new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
num++;
lock.unlock();
}
}).start();
}
while (Thread.activeCount() > 1){
Thread.yield();
}
System.out.println("num = " + num);
}
}
简单总结一下,volatile只能保证有序性和可见性,并不能保证原子性,对于lock和synchronized来说,可以保证原子性,有序性和可见性,我会在后面的文章中详细说明。