JUC 详解 -> JMM及Volatile
谈谈对Volatile的理解
- Volatile是Java虚拟机提供轻量级的同步机制
- 保证可见性
- 不保证原子性
- 禁止指令重排
可见性 ——>JMM
1. JMM:Java Memory Model
-
JMM(Java Memory Model)即为JAVA 内存模型。
-
作用:缓存一致性协议,用于定义数据读写的规则。
-
JMM定义了JVM在计算机内存(RAM)中的工作方式。JVM是整个计算机虚拟模型,所以JMM是隶属于JVM的。
-
从抽象的角度来看,JMM定义了线程工作内存和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。
-
本地内存是JMM的一个抽象概念、约定,是不真实存在的东西。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。
-
关于JMM的一些同步的约定:
- 线程解锁前,必须把共享变量立刻刷回驻村;
- 线程加锁前,必须读取主存种的最新值到工作内存种;
- 加锁和解锁是同一把锁。
JMM的 8种内存交互操作
-
内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外)
- read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
- load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
- use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
- assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
- store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
- write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
- lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
- unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
-
规则:
- 不允许read和load、store和write之一单独出现。即read了必须load,store了必须write
- 不允许线程丢弃最近的assign,即工作变量的数据改变了之后,必须告知主存
- 不允许一个线程将没有assign的数据从工作内存同步回主内存
- 一个新的变量必须在主内存(堆中)中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store之前,必须经过assign和load
- 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
- 如果对一个变量进行lock,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign初始化变量的值
- 如果一个变量没有被lock,就不能对其进行unlock。也不能unlock一个被其他线程锁住的变量
- 对一个变量进行unlock前,必须把此变量同步回主内存
问题
假如线程A修改了共享变量的值,但是B线程不能及时可见!
代码测试:
import java.util.concurrent.TimeUnit;
public class JMMDemo {
private static int number = 0;
public static void main(String[] args) {
new Thread(()->{
while(number == 0){
}
}).start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
number = 1;
System.out.println(number);
}
}
问题:输出1,程序不停止!即线程不知道主内存的值已经被修改过了!
- 要解决共享对象可见性这个问题:
- 使用volatile关键字
- 使用synchronized同步机制
- final
2. Volatile
2.1 保证可见性
代码测试:解决上述问题
import java.util.concurrent.TimeUnit;
public class JMMDemo {
//如果不加volatile 程序会死循环
//加了volatile 可以保证可见性
private static volatile int number = 0;
public static void main(String[] args) {
new Thread(()->{ //线程A对主内存的变化不知道
while(number == 0){
}
}).start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
number = 1;
System.out.println(number);
}
}
2.2 不保证原子性
- 什么叫原子性? 不可分割
- 线程A在执行任务时,不能被打扰,也不能被分割,要么同时成功,要么同时失败。
代码测试
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//Volatile 不保证原子性验证
//Lock 和 synchronized 可以保证原子性
public class VolatileDemo {
private volatile static int num = 0;
//private static Lock lock = new ReentrantLock();
//public synchronized static void add(){
public static void add(){
num ++;
// lock.lock();
// try{
// num++;
// }catch(Exception e){
// e.printStackTrace();
// }finally {
// lock.unlock();
// }
}
//理论上num结果应该为20000
public static void main(String[] args) {
for (int i = 1; i <= 20 ; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
add();
}
},String.valueOf(i)).start();
}
while (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+" " + num);
}
}
- 不用Lock和synchronized如何保证原子性?
- num ++ ; 不是一个原子性操作
- num ++ ; 不是一个原子性操作
- 使用原子类,解决原子性问题!
代码测试
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicDemo {
//原子类的Integer
private static AtomicInteger num = new AtomicInteger();
public static void add(){
num.getAndIncrement();
}
public static void main(String[] args) {
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
add();
}
}).start();
}
while(Thread.activeCount()>2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+" "+num);
}
}
原子类的高级性?
这些类的底层都和操作系统挂钩。 在内存中修改值!Unsafe类是一个很特殊的存在! CAS
2.3 禁止指令重排
指令重排
- 什么是指令重排:你写的程序,计算机并不是按照你写的那样去执行的。
- 源代码 --> 编译器优化的重排 --> 指令并行也可能重排 --> 内存系统也会重排 --> 执行
- 处理器在进行指令重排的时候,会考虑:数据之间的依赖性!
实例
int x = 1; //1
int y = 2; //2
x = x + 5; //3
y = x * x; //4
我们所期望的执行是1234 但是可能执行的是2134 1324
但不可能是4123
可能造成影响的结果 a b x y 默认都是0
线程A | 线程B |
---|---|
x = a | y = b |
b = 1 | a = 2 |
正常的结果: x = 0; y = 0
线程A | 线程B |
---|---|
b = 1 | a = 2 |
x = a | y = b |
指令重排的异常结果:x = 2; y = 1;
- volatile可以避免指令重排:
- 内存屏障,CPU指令。作用:
- 保证特定的操作的执行顺序!
- 可以保证某些变量的内存可见性!(利用这些特性,volatile实现了可见性)
- 内存屏障,CPU指令。作用:
总结
- volatile可以保证可见性;
- volatile不能保证原子性,可以用Lock、synchronized,也可以用JUC中的atomic类;
- 由于内存屏障,volatile可以避免指令重排的现象。