JMM关于同步的规定:
- 线程解锁前,必须吧共享变量的值刷新回主内存
- 线程加锁前,必须读取主内存的最新值到自己的工作内存
- 加锁解锁是同一把锁
volatile是java虚拟机提供的轻量级的同步机制具有以下特点:
- 保证可见性
- 不保证原子性
- 禁止指令重排
不保证原子性
原子性的定义
一个程序,它要么完整的被执行,要么完全不执行。这种特性就叫原子性。
volatile不保证原子性代码验证
package com.hyq.test;
public class VolatileAtomDemo {
// volatile不保证原子性
// 原子性:保证数据一致性、完整性
volatile int number = 0;
public void addPlusPlus() {
number++;
}
public static void main(String[] args) {
VolatileAtomDemo volatileAtomDemo = new VolatileAtomDemo();
for (int j = 0; j < 20; j++) {
new Thread(() -> {
for (int i = 0; i < 1000; i++) {
volatileAtomDemo.addPlusPlus();
}
}, String.valueOf(j)).start();
} // 后台默认两个线程:一个是main线程,一个是gc线程
while (Thread.activeCount() > 2) {
Thread.yield();
}
// 如果volatile保证原子性的话,最终的结果应该是20000
// 但是每次程序执行结果都不等于20000
System.out.println(Thread.currentThread().getName() + "\t final number result = " + volatileAtomDemo.number);
}
}
代码执行结果如下:多次执行结果证明volatile不保证原子性
解决volatile不保证原子性问题
- 方法前加synchronized解决
public synchronized void addPlusPlus() {
number++;
}
- 加锁解决
Lock lock = new ReentrantLock();
public void addPlusPlus() {
lock.lock();
number++;
lock.unlock();
}
- 原子类解决
package com.hyq.test;
import java.util.concurrent.atomic.AtomicInteger;
public class VolatileSolveAtomDemo {
// 原子Integer类型,保证原子性
private AtomicInteger atomicNumber = new AtomicInteger();
// 底层通过CAS保证原子性
public void addPlusPlus() {
atomicNumber.getAndIncrement();
}
public static void main(String[] args) {
VolatileSolveAtomDemo volatileSolveAtomDemo = new VolatileSolveAtomDemo();
for (int j = 0; j < 20; j++) {
new Thread(() -> {
for (int i = 0; i < 1000; i++) {
volatileSolveAtomDemo.addPlusPlus();
}
}, String.valueOf(j)).start();
}
// 后台默认两个线程:一个是main线程,一个是gc线程
while (Thread.activeCount() > 2) {
Thread.yield();
}
// 因为volatile不保证原子性,所以选择原子类AtomicInteger来解决volatile不保证原子 性问题
// 最终每次程序执行结果都等于20000
System.out.println(Thread.currentThread().getName() + "\tfinal number result = " + volatileSolveAtomDemo.atomicNumber.get());
}
}
Volatile禁止指令重排
volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象。\
内存屏障(Memory Barrier)又称内存栅栏 ,是一个CPU指令,它的作用有两个: 保证特定操作执行的顺序性; 保证某些变量的内存可见性(利用该特性实现volatile内存可见性)
volatile实现禁止指令重排优化底层原理:
由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译 器和CPU,不管什么指令都不能和这条Memory Barrier指令重排,也就是说通过插入内存屏障,就能 禁止在内存屏障前后的指令执行重排优化。内存屏障另外一个作用就是强制刷出各种CPU的缓存数据, 因此任何CPU上的线程都能读取到这些数据的最新版本。
- 左边:写操作场景:先StoreStore指令,后StoreLoad指令。
- 右边:读操作场景:先LoadLoad指令,后LoadStore指令。
LoadLoad Barriers
示例:Load1; LoadLoad; Load2
该屏障确保Load1数据的装载先于Load2及其后所有装载指令的的操作
StoreStore Barriers
示例:Store1; StoreStore; Store2
该屏障确保Store1立刻刷新数据到内存(使其对其他处理器可见)的操作先于Store2及其后所有存储指令的操作
LoadStore Barriers
示例:Load1; LoadStore; Store2
确保Load1的数据装载先于Store2及其后所有的存储指令刷新数据到内存的操作
StoreLoad Barriers
示例:Store1; StoreLoad; Load2
该屏障确保Store1立刻刷新数据到内存的操作先于Load2及其后所有装载装载指令的操作。它会使该屏障之前的所有内存访问指令(存储指令和访问指令)完成之后,才执行该屏障之后的内存访问指令。
volatile使用场景
单例模式(DCL-Double Check Lock双端检锁机制)
public class DCLSingleTon {
private DCLSingleTon(){
System.out.println(Thread.currentThread().getName() +" 构造方法执行了");
}
private static volatile DCLSingleTon singleTon = null;
public static DCLSingleTon getInstance(){
//加锁前后都做一次判断
if (singleTon == null){
synchronized (DCLSingleTon.class){
if (singleTon ==null){
singleTon = new DCLSingleTon();
}
}
}
return singleTon;
}
public static void main(String[] args) {
//多线程环境下,执行多次,构造方法只执行一次;说明只有一个对象
for(int i = 1;i<100;i++){
new Thread(new Runnable() {
@Override
public void run() {
DCLSingleTon.getInstance();
}
}).start();
}
}
}