JMM规定了线程的工作内存和主内存之间的交互关系,以及线程之间的可见性和程序的执行顺序。一方面,要为程序员提供足够强的内存可见性保证;另一方面,对编译器和处理器的限制要尽可能地放松。JMM对程序员屏蔽了CPU以及OS内存的使用问题,能够使程序在不同的CPU和OS内存上都能够达到预期的效果。
Java采用内存共享的模式来实现线程之间的通信。编译器和处理器可以对程序进行重排序优化处理,但是需要遵守一些规则,不能随意重排序。
原子性:一个操作或者多个操作要么全部执行要么全部不执行;
可见性:当多个线程同时访问一个共享变量时,如果其中某个线程更改了该共享变量,其他线程应该可以立刻看到这个改变;
有序性:程序的执行要按照代码的先后顺序执行;
在并发编程模式中,势必会遇到上面三个概念,JMM对原子性并没有提供确切的解决方案,但是JMM解决了可见性和有序性,至于原子性则需要通过锁或者Synchronized来解决了。(具体可参考 同学别走,聊聊Lock与Synchronized吧)
如果一个操作A的操作结果需要对操作B可见,那么我们就认为操作A和操作B之间存在happens-before关系,即A happens-before B。(具体可参考 happens-before那些事儿)
happens-before原则是JMM中非常重要的一个原则,它是判断数据是否存在竞争、线程是否安全的主要依据,依靠这个原则,我们可以解决在并发环境下两个操作之间是否存在冲突的所有问题。JMM规定,两个操作存在happens-before关系并不一定要A操作先于B操作执行,只要A操作的结果对B操作可见即可。
在程序运行过程中,为了执行的效率,编译器和处理器是可以对程序进行一定的重排序,但是他们必须要满足两个条件:
执行的结果保持不变,
存在数据依赖的不能重排序。重排序是引起多线程不安全的一个重要因素。
同时顺序一致性是一个比较理想化的参考模型,它为我们提供了强大而又有力的内存可见性保证,他主要有两个特征:1 一个线程中的所有操作必须按照程序的顺序来执行;2 所有线程都只能看到一个单一的操作执行顺序,在顺序一致性模型中,每个操作都必须原则执行且立刻对所有线程可见。
Java 内存模型中定义了以下 8 种操作来完成主内存与工作内存之间交互的实现细节:
luck(锁定):作用于主内存的变量,它把一个变量标示为一条线程独占的状态。
unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到工作内存中,以便随后的 load 动作使用。
load(载入):作用于工作内存的变量,它把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。
use(使用):作用于工作内存的变量,它把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
store(存储):作用于工作内存的变量,它把工作内存中的一个变量的值传递到主内存中,以便随后的 write 操作使用。
write(写入):作用于主内存的变量,它把 store 操作从工作内存中得到的变量值放入主内存的变量中。
Java 内存模型还规定了执行上述 8 种基本操作时必须满足如下规则:
不允许 read 和 load、store 和 write 操作之一单独出现,以上两个操作必须按顺序执行,但没有保证必须连续执行,也就是说,read 与 load 之间、store 与 write 之间是可插入其他指令的。
不允许一个线程丢弃它的最近的 assign 操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
不允许一个线程无原因地(没有发生过任何 assign 操作)把数据从线程的工作内存同步回主内存中。
一个新的变量只能从主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load 或 assign)的变量,换句话说就是对一个变量实施 use 和 store 操作之前,必须先执行过了 assign 和 load 操作。
一个变量在同一个时刻只允许一条线程对其执行 lock 操作,但 lock 操作可以被同一个条线程重复执行多次,多次执行 lock 后,只有执行相同次数的 unlock 操作,变量才会被解锁。
如果对一个变量执行 lock 操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行 load 或 assign 操作初始化变量的值。
如果一个变量实现没有被 lock 操作锁定,则不允许对它执行 unlock 操作,也不允许去 unlock 一个被其他线程锁定的变量。
对一个变量执行 unlock 操作之前,必须先把此变量同步回主内存(执行 store 和 write 操作)。
关于JMM,这里分析下2个小问题?
1、普通的写操作,什么时候同步到主内存
2、普通的写操作同步到主内存时,对其他线程的本地内存有无影响
Java线程运行时有一块私有内存,当操作变量时,会将变量从主内存同步到私有内存,之后变量操作都是在私有内存中,注意,私有内存中变量何时同步回主内存时不可期的。JMM告诉我们可以通过volatile和synchronized机制来保证。
Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
根据intel的MESI协议,CPU核缓存(L1/L2)在更新数据时,如果是共享数据会使其他CPU核缓存该共享数据的缓存行失效(对应缓存行设置标志无效,下次使用时会从内存中加载)。针对第二个问题,Java普通的写操作同步到主内存时,对其他线程的本地内存有无影响?如果是遵循MESI协议的intel平台,如果Java私有内存和主内存关系与CPU缓存和内存关系一样,则Java普通写操作同步到主内存时会使其他线程私有内存失效,否则不影响。
最后看一个测试的代码:
private static boolean running = true;
public static void main(String[] args) throws Exception {
new Thread(() -> {
long i = 0;
System.out.println("thread start");
while (running) {
i++;
}
System.out.println("thread end " + i);
}).start();
Thread.sleep(10);
running = false;
System.out.println("main set running false");
}
输出结果为:
程序会一直运行下去,如果将属性running设置为valotle类型,那么线程是可以正常结束的。
推荐阅读
欢迎小伙伴关注【TopCoder】阅读更多精彩好文。