文章目录
一、java内存模型(JMM)
1.JMM示意图
JMM是一种抽象的模型(实际是不存在的)
如上图,有线程1和线程2。
(1)线程1、2的功能
- 线程1 用来读取state。
- 线程2 用来读取state -> 修改state -> 写回主内存。
(2)主内存、工作内存的作用
- 主内存 用来存放共享信息。(若是基本数据类型,直接存放,若是引用类型,则引用的地址存放在工作内存,引用的对象存放在堆中)
- 工作内存 用来存放私有信息(不同线程的工作内存之间相互不可见)。
(3)工作方式
- 线程修改私有数据 :直接在工作内存修改。
- 线程修改共享数据 :把新数值赋值到工作内存,再刷新到主内存
2.通过volatile对JMM的理解
public class javaDemo {
volatile static boolean state = false;
public static void main(String args[])throws InterruptedException{
new Thread(new Runnable(){ //线程1
public void run(){
System.out.println("等待数据");
while(!state){
}
System.out.println("成功");
}
}).start();
Thread.sleep(2000);
new Thread(new Runnable() { //线程2
public void run() {
System.out.println("准备数据");
state = true;
System.out.println("准备数据完毕");
}
}).start();
}
}
(1)当state的参数不用volatile修饰时,运行结果
对应到JMM中,此时线程1并不能使用更新过后的state,所以无法打印“成功”(线程1一直在while死循环中)。
(2)当state的参数使用volatile修饰时,运行结果
对应到JMM,中,根据缓存一致性协议。由于线程2修改了state。所以线程1的工作内存中的state失效。当线程1需要继续使用state时,需要重新从主内存中读取最新的state。所以,可以打印出“成功”。
二、为什么每个线程要设置单独的工作内存?
因为CPU的运行速度比主内存快得多。为了解决两者之间的读写速度矛盾(提高效率)。故在他们之间加入了高速缓存(相当于设置了一个工作内存)。
三、加入高速缓存带来的问题(缓存一致性)及解决办法
(1)缓存一致性问题
多线程并发处理的不同步。
(2)解决办法
- 总线加锁(已过时,核心思想是将并行转换为串行)
CPU从主内存读取数据到高速缓存,会在总线对这个数据加锁。这样其他CPU无法读写这个数据。直到该CPU使用完数据后才解锁。此时,解锁后其他CPU才能使用该数据。 - MESI缓存一致性协议
多个CPU从主内存读取同一个数据到各自的高速缓存,当其中某个CPU修改了该数据,那么该数据会立即(不会等线程其他代码执行完)同步会主内存。同时,其他CPU通过总线嗅探机制感知到数据的变化从而将自己的高速缓存中的数据失效(此时,需要再从主内存读数据,读取到的也就是新数据了)。
四、内存间交互操作
JMM定义了8个操作来完成 ***主内存***和***工作内存***的数据交互操作
数据原子操作 | 功能 |
---|---|
read(读取) | 从主内存读取数据 |
load(载入) | 将主内存读到的数据写入工作内存 |
use(使用) | 从工作内存读取数据来计算 |
assign(赋值) | 将计算好的值重新赋值到工作内存中 |
store(存储) | 将工作内存数据写入主内存 |
write(写入) | 将store过去的变量值赋值给主内存中的变量 |
lock(锁定) | 将主内存变量加锁,标识为线程独占状态 |
unlock(解锁) | 将主内存变量解锁,解锁后其他线程可以锁定该变量 |
五、JMM是如何保证并发编程的三大特性(原子性、可见性、有序性)
1.原子性的保证
操作 | 是否具有原子性 | |
---|---|---|
x=1 | 有(x为私有数据) | 写x(注:若x是私有数据有原子性,若是共享数据无原子性) |
y=x | 无 | 读x -> 写y |
i++ | 无 | 读i -> +1 -> 写i |
总结:
- 多个原子性的操作合并到一起没有原子性。
- JMM保证原子性,使用Synchronized和JUC中的lock关键字。
2.可见性的保证
- volatile关键字在JMM上实现MESI协议。
- Synchronized加锁。
- JUC的lock。
3.有序性的保证
Happens-before原则:
- 程序顺序原则:重排序后程序结果不变。
- 锁定原则:后一次加锁必须等待前一次解锁。
- volatile原则:不允许变化(霸道原则)。
- 传递原则:A->B->C ,则A->C。