1、CPU的多核并发缓存架构

在了解java的内存模型之前首先要了解一下CPU 的多核并发缓存架构。下面是架构图:

java传统架构VSDDD架构_java内存模型


我们的cpu在运算的过程中,使用的是cpu 缓存机制,每个cpu并不直接跟主内存打交道,而是拥有自己的CPU缓存。其实java的内存模型更CPU多核并发缓存架构相似。

2、 java内存模型

java内存模型图:

java传统架构VSDDD架构_java内存模型_02


上面就是java的内存模型图,其实java的内存模型应该叫做java线程内存模型,从图中我们可以看到每个线程都有自己的工作内存,当线程执行的时候,首先会去从主内存中获取共享变量,生成一个共享变量的副本。然后进行相应的操作。

从图中可以看到每个线程的工作内存都是独立的,每个线程都操作自己的工作内存。工作内存类似于CPU的高速缓存。并且线程之间是无法通信的,所以就会导致变量值不同步的问题,当线程A修改共享变量的值,线程B和线程C中的变量并不会跟着改变。下面是一段示例代码。

package com.proven.thread;

/**
 * 
* @ClassName: VolatileDemo  
* @author proven  
* @date 2019年10月15日
 */
public class VolatileDemo{
	//使用volatile 来测试java内存模型
	public static  boolean initFlag = false;
	public static void main(String[] args) throws Exception {
		
		//线程A
		new Thread(new Runnable(){		
			@Override
			public void run() {
				System.out.println("strat thread A~~~");
				while(!initFlag){
					
				}
				System.out.println("end Thread A~~~~");
			}
			
		}).start();
		Thread.sleep(2000);
		
		//线程B
		new Thread(new Runnable(){
			@Override
			public void run() {
				System.out.println("start Thread B~~~");
				initFlag = true;
				System.out.println("end thread B~~~");
			}
			
		}).start();
	}
	
}

执行结果:

strat thread A~~~
start Thread B~~~
end thread B~~~

从执行结果中,我们可以看到当线程B中的initFlag 值修改为true后,线程A中的代码还是一直在运行,说明线程A中的值并未被改变。

当线程A和线程B同时修改一个静态变量,根据java内存模型,但是这共享变量是不会同步修改。如果需要同步,需要给这个变量加个volatile 关键词。

修改后的代码:

public static volatile boolean initFlag = false;

其他代码保持不变。
执行结果:

strat thread A~~~
start Thread B~~~
end thread B~~~
end Thread A~~~~

从代码执行结果我们可以看到volatile关键字实现了线程之间变量的可见性。那么volatile底层是怎么实现的呢?

3、Volatile底层实现

JMM数据原子操作

java传统架构VSDDD架构_java内存模型_03

3.1 早期的实现

volatile关键字的早期实现方式是:总线机制

下面是实现图:

java传统架构VSDDD架构_缓存_04


实现原理:

要保护数据的一致性,就必须加锁,当线程A在访问一个共享变量的时候,就会在主内存中给这个变量加锁(lock),这时线程B就不能加载这个变量,直到线程A将这个变量写回主内存中,并且进行unlock操作。这时线程B就能够得到这个变量进行操作。

这种方式缺点:加锁的粒度太大,这种方式性能太低,当线程之间有共享变量的时候,其实线程之间是串行操作。现在的实现原理已经不使用这种技术了。

3.2 现在的实现方式:MESI缓存一致性协议(CPU总线嗅探机制)

实现原理图:

java传统架构VSDDD架构_缓存_05


当实现了MESI缓存一致性协议的时候,所有CPU 都会启动CPU总线嗅探机制(监听),线程A和线程B同时进行操作,当线程A把变量值修改后store到主内存的过程中,当数据通过总线的时候,总线嗅探机制就会监听到变量的修改,这时线程B中工作内存中的变量值就会失效,当线程B发现变量值失效之后,就会重新从主内存中read 变量到工作内存中。

底层实现主要通过汇编lock前缀指令,它会锁定这块内存区域的缓存并回写到主内存,此操作被称为“缓存锁定”,MESI缓存一致性协议机制会阻止同时修改被两个以上处理器缓存的内存区域数据,一个处理器的缓存值通过总线会写到内存会导致其他相应处理器的缓存失效。

问题思考:

  1. 如果两个线程进行同时写的操作,这个该怎么办?
  2. 当变量在store的时候,这是嗅探机制就监听到并重新进行read操作,但是这是变量还没真正写入主内存当中,这时该怎么办?

其实这两个问题在底层中都已经解决掉了,解决的方式还是加锁,其实在进行store 之前就对主内存变量加了一个lock, 当数据真正回写到主内存中时会unlock。(这种锁的粒度会小很多),可以近似的认为并不会影响性能。

在这里我们可以理解出并发编程的三大特性:

可见性:可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

原子性:即一个或者多个操作作为一个整体,要么全部执行,要么都不执行,并且操作在执行过程中不会被线程调度机制打断;而且这种操作一旦开始,就一直运行到结束,中间不会有任何上下文切换。

有序性:即程序执行的顺序按照代码的先后顺序执行。

我们可以看到,Volatile保证可见性和有序性,但是不保证原子性,保证原子性需要借助synchronized 这样的锁机制。

为什么Volatile不保证原子性?
其实从java内存模型中,我们可以知道,当两个线程同时在操作一个变量的时候,当同时写入的过程中,当一个变量开始回写到主内存中时,由于总线嗅探机制,另一个线程中的变量就会失效,但是这个过程中已经做过运算,所以就无法保持变量的原子性。