volatile是java虚拟机提供的一种轻量级的同步机制,那么volatile到底是怎么实现轻量级同步的?
可见性
什么是可见性?这个得从java内存模型说起
java内存模型
JMM把内存条的内存定义为主存,CPU的高速缓存定义为工作内存,线程在运算前会把主存的共享数据拷贝一份副本到自己的工作内存,完成运算后再把数据更新到主存,但每个线程间的工作内存的数据是不共享的,也就是线程1修改了数据再回写到主存,线程2是不知道的
代码说话
public class VolatileVisibilityTest {
public static void main(String[] args) {
Data data = new Data();
//线程T更新number的值
new Thread(() -> {
try {
//先睡3秒,让main线程读取到number的原始值
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
data.setNumber(100);
System.out.println(Thread.currentThread().getName() + ": 修改number的值为" + data.getNumber());
}, "T").start();
//线程main读取number的值,如果线程main能感知到线程T修改了number的值,将会结束循环,否则一直在死循环中
while (data.getNumber() == 0) {}
System.out.println(Thread.currentThread().getName() + ": 结束循环,number的值已变为" + data.getNumber());
}
}
class Data {
private /*volatile*/ int number = 0;
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
}
大家试着运行一下volatile注释前和注释后的区别
在注释volatile前,main线程是能感知到T线程对number的值做出了修改的
在注释volatile后,main线程无感知T线程对number的值做出的修改,一直在循环中
不保证原子性
注意,volatile是轻量级的同步机制,所以不保证原子性,先上代码
public class VolatileAtomicityTest {
public static void main(String[] args) throws InterruptedException {
Data data = new Data();
//开启20个线程,每条线程执行1000次number++,理论上最后的结果应该是20000
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
data.increment();
}
}, "Thread--" + i).start();
}
//当只有main线程和GC线程存活才结束循环,否则放弃执行权
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(data.getNumber());
}
}
public class Data {
private volatile int number = 0;
public void increment() {
number++;
}
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
}
这段代码执行多次,只有偶尔的结果是20000,其他结果都是小于20000.但是若是给increment方法加上synchronized同步锁,则执行多次结果都是20000,这证明volatile不保证原子性
我们都知道number++不是原子性的,对应的字节码如下:
getfield //从主存拷贝到工作内存
iadd //自增
putfield //回写到主存
假设主存内的number=0,当线程1执行了字节码getfield和iadd后,准备执行putfield时被挂起,线程2执行getfield得到number的值仍然为0,并执行iadd,这个时候切又回线程1,虽然number加了volatile关键字,线程2修改了number的值线程1是能感知到的,但线程1已经开始执行putfield了,所以回写主存number=1,线程2也回写主存number=1,这时候就发生了写丢失.
所以单靠volatile是无法保证原子性的
有序性
先来说说啥是指令重排,编译器和虚拟机会重新排列无数据依赖的语句来优化程序,也就是说,你写的代码虚拟机不一定是按顺序执行的
public class TestRearrangement {
private int a = 0;
private boolean flag = false;
public void method1() {
a = 1; //语句1
flag = true; //语句2
}
public void method2() {
if (flag) {
a = a + 5;
System.out.println("a=" + a);
}
}
}
代码中的语句1和语句2,因为它们之间没有数据依赖,在指令重排时,很有可能是先执行语句2再执行语句1.这在单线程环境下没毛病,但在多线程环境下,语句1和语句2的顺序很可能会影响method2方法执行的结果.例如线程1先执行语句2然后挂起,线程2执行method2,那么a的值就会是5,如果线程1先执行语句1,那么线程2执行完method2,a的值为6
若给a和flag都加上volatile关键字,编译器就不会对相关代码进行重排优化
应用场景
懒汉式单例
首先来看看懒汉式单例的写法
public class Singleton {
private volatile static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) { //第6行
synchronized (Singleton.class) {
if (instance == null) { //第8行
instance = new Singleton();
}
}
}
return instance;
}
}
为什么加了synchronized还要加volatile呢?关键点在于instance = new Singleton()这行代码
instance = new Singleton();
// 可以分解为以下三个步骤
1 memory=allocate();// 分配内存 相当于c的malloc
2 ctorInstanc(memory) //初始化对象
3 s=memory //设置s指向刚分配的地址
// 上述三个步骤可能会被重排序为 1-3-2,也就是:
1 memory=allocate();// 分配内存 相当于c的malloc
3 s=memory //设置s指向刚分配的地址
2 ctorInstanc(memory) //初始化对象
因为synchronized不能保证有序,所以instance = new Singleton()的指令有可能会被重新排序,当重新排序为1-3-2,线程1执行完指令3时,instance已经被分配了内存地址,所以instance!=null;这个时候线程1挂起,线程2访问到第6行代码if (instance == null),因为instance!=null所以直接return instance,但此时instance对象还没创建好,因为线程1的指令2还没执行完,所以此时的instance只是具有内存地址但却是空对象
所以,单例的懒汉式写法要加上volatile关键字
网友评论