作为深入理解java中的锁,首先应该掌握volatile的含义和用法。
线程之间的可见性
可见性对于java初学者并不是一个从字面上就可以简单理解的名词。
往深了说,要真正掌握volatile关键字,还需要有基本的jvm的知识,这里只需要理解jvm的分区以及各个区的内容含义就知道了,参考前面的一篇博客jvm内存划分
我们需要掌握的有两点:
- 同一进程的不同线程之间其内存空间是不共享的,类似虚拟机栈上的局部变量表
- 除了jvm使用的内存空间,CPU上的寄存器等也可以保存变量(称为CPU缓存,即Cache)
因此,尤其注意在多线程共享一个变量时,需要使用volatile修饰,如下:
volatile boolean flag;
//在线程A执行
public void shutdown(){
flag = true;
}
//在线程B执行
public void doWork(){
while(!flag){
//do work
}
}
从上述代码中可以得出,当A线程调用后,B线程的函数会立刻结束,因为A线程对flag的改变被直接写入内存,而不是cpu cache,这会导致其他线程重新从内存读取flag变量。而如果不加volatile,那么B线程的循环可能会过一段时间才结束。
指令重排
一旦涉及到多线程,就会出现许多我们无法预测的问题,比如指令重排。
普通的变量会保证以下的执行:
在该方法内,所有依赖赋值结果的地方都能获取到正确的结果,单不能保证变量赋值的顺序和代码中完全一致
比如如下一段代码
//共享变量
int a = 0;
int b = 1;
volatile boolean flag = false;
//A线程执行
public void change(){
a = 100;
b = a;
flag = true;
}
//B线程执行
public void doWork(){
while(!flag){
sleep();
}
System.out.println(b);
}
在上述代码中,如果不加volatile,可能会出现B输出1。这就是指令重排带来的副作用。
我们前面说过,对于函数内部,即change()函数,肯定会保证a = 100;b = a;
的执行顺序,因为存在变量依赖,如果不加volatile,实际上却并不会保证flag = true;
在函数change()最后执行,这就是指令重排,即jvm内部对其只进行指令优化后导致语句的执行顺序改变。
于是在多线程,就出现问题了,比如在上述的B线程中,如果没有volatile,则B可能输出1,因为A线程中限制性flag=true,b的值还没来得及更新,B线程就输出b了。
happens-before
先行发生,对于前面的指令重排,并不是所有情况都会存在,java语言内部有一个happens-before原则,它可以判断一个变量的改变在多个线程中是否是可见的。
如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。
java内存模型中存在一些天然的happens-before关系,但也有些关系不在此中(比如前面的变量赋值),而jvm可以对这些不在此列的进行重排序。
volatile
然后我们来看看volatile关键字,volatile共两个作用:
命令CPU不使用CPU Cache.
即每次获取变量时,都从内存中去取,以保证变量的最新值,以此来达到变量对于所有线程都是可见的。禁止该变量的指令重排
实际上volatile的作用就到这里为止了,我们发现volatile并没有实现同步的机制,因此volatile是很轻量级的。一般来说volatile和锁或者synchronized一起使用,已达到同步的目的。
总的来说,volatile变量读操作的性能和普通变量几乎没有什么差别,但是写操作可能会慢一点。
单例举例
volatile最常用的就是计数和单例模式。
下面的例子将说明我们平时写的单例模式是有问题的。
private static class Singleton{
private static Singleton instance;
public Singleton(){}
public static Singleton getInstance(){
if(instance == null){
synchronized (Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
上述代码是最常见的创建单例的例子,就实际效果来说,也没太大问题,但问题在于上述的单例模模式并不“单例”,实际上有可能创建多个Singleton对象,问题在于,如果我们承认变量的不可见性,那么如果多个线程调用getInstance(),那么由于不可见性,即使其中一个线程创建了新对象,也存在其他线程没有发现而又创建一次的情况。
最佳的单例模式应该如下:
private static class Singleton{
private volatile static Singleton instance;
public Singleton(){}
public static Singleton getInstance(){
if(instance == null){
synchronized (Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}