- JMM 内存模型
- JMM 数据原子操作
- JMM 内存模型图
- JMM 缓存不一致问题
- 1. 总线枷锁(性能太低)
- 2. MESI 缓存一致性协议(volatile关键子底层原理)
- JMM内存模型图(volatile)
- Volatile缓存可见性实现原理
- 并发编程三大特性:可见性、原子性、有序性
- JMM 内存模型图解释
JMM 内存模型
Java线程内存模型跟cpu缓存模型类似,是基于cpu缓存模型来建立的,Java线程内存模型是标准化的,屏蔽掉了底层不同计算机的区别。
如上图多线程中,共享变量存在于主内存中,cpu读取主内存中的共享变量时,会在工作内存当中产生一个共享变量副本,实际操作的是副本,并不是主内存中的共享变量。
如下程序:类中有一个变量,可以看做是主内存中的共享变量,在主方法中有俩个线程,第一个线程中打印了一行日志 waiting data… ,然后是一个死循环,跳出循环的条件是共享变量改为true。在第二个线程中,同样打印日志,并且表面上通过prepareData方法修改共享变量为true。
public class VolatileVisibilityTest {
private static boolean initFlag = false;//可以看作是主内存中的共享变量
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("waiting data...");
while (!initFlag) { //死循环
}
System.out.println("========================success");
}
}).start();
Thread.sleep(2000);
new Thread(new Runnable() {
@Override
public void run() {
prepareData();
}
}).start();
}
public static void prepareData() {
System.out.println("prepareing data...");
initFlag = true;
System.out.println("prepare data end...");
}
}
然而运行结果如下图:
发现,线程1中打印了那行日志,但是并没有跳出死循环打印第二行日志,说明了一个问题,就是在线程2中修改的变量并没有影响到线程1。
JMM 数据原子操作
| read(读取) | 从内存中读取数据 |
|---|---|
| load(载入) | 将主内存读到的数据写入工作内存 |
| use(使用) | 从工作内存读取数据来计算 |
| assign(赋值) | 将计算好的值重新赋值到工作内存中 |
| store(存储) | 将工作内存数据写入主内存 |
| write(写入) | 将store过去的变量值赋值给主内存中的变量 |
| lock(锁定) | 将主内存变量加锁,标识为线程独占状态 |
| unlock(解锁) | 将主内存变量解锁,解锁后其它线程可以锁定该变量 |
针对上述程序中,JMM内存模型流程图如下:
- 首先在主内存中有一个共享变量initFlag,并且赋值为false。然后还有俩个线程,分别是线程1、线程2
- 第二步执行read原子操作,多核CPU同时读取主内存的共享变量。注意是并行同时执行,后面的操作也一样,我先说线程1,再说线程2
- 第三步是load原子操作,将主内存读到的数据写入到工作内存
- 第四步是use原子操作(CPU执行引擎),在上述代码中对应的是 while 死循环
- 同样线程2中也是先执行read原子操作,再执行load原子操作
- 不一样的是线程2再执行use原子操作时,是将工作副本中的值重新赋值为true
- 然后会执行store原子操作,将工作内存中的值写入到主内存中,注意此操作只会将新赋的值暂时放到主内存中
- 然后通过wirte写入原子操作之后,新赋的值才会被写入到主内存当中。
以上便是上述程序中Java内存模型中的执行过程。
JMM 缓存不一致问题 1. 总线枷锁(性能太低)
cpu从主内存读取到高速缓存,会在总线对这个数据枷锁,这样其它cpu没法去读或写这个数据,直到这个cpu使用完数据释放锁之后其它cpu才能读取该数据。这样就不能够实现多核CPU的高性能。
总线枷锁方式中,可能在read原子操作进行之前,就先执行lock枷锁原子操作,而且枷锁后,其它线程是没有办法去读取主内存的数据,必须在主内存中的变量被同步完毕之后,执行unlock解锁原子操作后,其它线程才能进行。所以其它CPU可能读到的已经是最新的值了。换句话说,总线枷锁操作是将并行执行转成了串行操作,从而性能太低。
2. MESI 缓存一致性协议(volatile关键子底层原理)
多个 CPU 从主内存读取同一个数据到各自的高速缓存,当其中某个cpu修改了缓存里面的数据,该数据会马上同步回主内存,其它cpu通过总线嗅探机制可以感知到数据的变化,从而将自己缓存里的数据失效。
上述程序中共享变量加 volatile 关键子之后的运行结果如下图:
发现线程1跳出了死循环,执行了第二步日志。
加了 volatile 之后,针对于原子操作,lock枷锁是在store原子操作之前,unlock解锁还是一样,在write原子操作之后。对比上面总线枷锁的lock和unlock方式,有何不同???
底层实现主要是通过汇编lock前缀指令,它会锁定这块内存区域的缓存(缓存行锁定)并回写到主内存。
首先,lock锁加在store之前,解决了一个并发的问题,如果lock加载read之前,那么其它cpu无法读取主内存,但是加在store之前,在一开始,所有的cpu都可以同步读取主内存,读到最后一步才加锁,枷锁力度小,对性能影响不大,在lock之后,store和write原子操作,说白了两步合起来就是给最后主内存中的数据重新进行内存地址的赋值操作,而这两步操作的是非常非常快的。
并发编程三大特性:可见性、原子性、有序性
volatile 保证可见性和有序性,但是不保证原子性,保证原子性需要借助 synchronized 这样的锁机制。
如下程序:10个线程同时进行1000次循环,执行num++操作
public class VolatileAtomicTest {
public static volatile int num = 0;
public static void increase() {
num++;
}
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[10];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 1000; j++) {
increase();
}
}
});
threads[i].start();
}
for (Thread t : threads) {
t.join();
}
System.out.println(num); //1000*10=10000
}
}
运行结果小于等于10000,为什么???
- 一种情况,线程1和线程2同时执行原子操作,都执行到assgin原子操作,在工作内存中都执行了 num 重新赋值操作,在store之前有lock枷锁,假设线程1先拿到lock枷锁,然后store回主内存,会经过主线,这个过程中,lock前缀指令会触发总线嗅探机制,线程2会将工作内存中num++的值失效掉,之后再次执行一些列原子操作。也就是说,在整个流程中,线程2执行了3次num++,但最后结果是2,有一次失效。
- volatile 不能保证 原子性 。



