一、什么是JMM
1、概念
- JMM:Java虚拟机是一个实现了跨平台的虚拟系统,因此它也有自己的内存模型,即Java内存模型(Java Memory Model, JMM)。
2、JMM同步
-
线程解锁前,必须把共享变量立刻刷回主存。
-
线程加锁前,必须读取主存中的最新值到工作内存中!
-
加锁和解锁是同一把锁。
3、八种操作
- 内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类型的变量来说,load、store、read和writ操作在某些平台上允许例外)
- lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
- unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
- read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
- load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
- use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
- assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
- store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
- write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
4、使用规则
- 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
- 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
- 不允许一个线程将没有assign的数据从工作内存同步回主内存
- 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过assign和load操作
- 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
- 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
- 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
- 对一个变量进行unlock操作之前,必须把此变量同步回主内存
5、存在问题
- 程序不知道主内存的值已经被修改过了
二、Volatile
1、概念
- Volatile是Java 虚拟机提供的轻量级同步机制,类似于synchronized,但是没有其强大。
- 特点:
①、保证可见性
②、不保证原子性
③、防止指令重排
2、代码演示
- 保证可见性
①、没有使用Volatile关键字,持续一直处于运行状态,线程对主内存的变化不知道
public class VolatileTest {
// 不加 volatile 程序就会死循环!
// 加 volatile 可以保证可见性
private static int sum=0;
public static void main(String[] args) {
new Thread(()->{
while (sum==0){
}
}).start();
try{
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
sum=1;
System.out.println(sum);
}
}
运行结果:
②、使用了volatile关键字,线程可以得知主内存的变化
public class VolatileTest {
// 不加 volatile 程序就会死循环!
// 加 volatile 可以保证可见性
private volatile static int sum=0;
public static void main(String[] args) {
new Thread(()->{
while (sum==0){
}
}).start();
try{
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
sum=1;
System.out.println(sum);
}
}
运结果:
- 不保证原子性
①、使用volatile时
public class VolatileTest2 {
private volatile static int num=0;
public static void add(){
num++;
}
//理论上值应该为20000
public static void main(String[] args) {
for (int i=1;i<=20;i++){
new Thread(()->{
for (int j=1;j<=1000;j++){
add();
}
}).start();
}
//判断剩下的线程是否大于2个,如果大于则表明20个线程创建成功了
while (Thread.activeCount()>2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+"---->"+num);
}
}
运行结果:
②、使用AtomicInteger保证其原子性
public class VolatileTest2 {
private volatile static int num=0;
private volatile static AtomicInteger atomicInteger= new AtomicInteger();
public static void add(){
// AtomicInteger + 1 方法, CAS
atomicInteger.getAndIncrement();
}
//理论上值应该为20000
public static void main(String[] args) {
for (int i=1;i<=20;i++){
new Thread(()->{
for (int j=1;j<=1000;j++){
add();
}
}).start();
}
//判断剩下的线程是否大于2个,如果大于则表明20个线程创建成功了
while (Thread.activeCount()>2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+"---->"+atomicInteger);
}
}
运行结果:
3、指令重排
- 指令重排:我们写的程序,计算机并不是按照你写的那样去执行的。
- 代码执行过程:源代码 —> 编译器优化的重排 —> 指令并行也可能会重排 —> 内存系统也会重排 ——> 执行
- 正常执行结果:x = 0;,y = 0;
线程A | 线程B |
---|---|
x = a | y = b |
b =1 | y = b |
- 指令重排可能导致的结果: x = 2,y = 1;
线程A | 线程B |
---|---|
b =1 | y = b |
x = a | y = b |
- volatile可以避免指令重排
①、保证特定操作的执行顺序
②、可以保证某些变量的内存可见性
③、volatile 内存屏障在单例模式中使用的最多