并发编程系列之Java内存模型(JMM) 收藏 阅读:263
2020-03-18 23:03:10

Java为什么设计内存模型呢?这个还要从计算机的内存模型说起。随着硬件的发展,计算机硬件特别是CPU的发展速度特别快,根据摩尔定律,集成电路上可容纳的元器件的数目,约每隔18-24个月便会增加一倍,性能也将提升一倍,后来出现了多核心CPU。现在我们分析两个问题:

问题一:怎么解决高速运算的CPU和内存读写速度慢的问题?

问题二:处理器为了尽可能利用运算单元做了哪些优化?

为了解决问题1,CPU和内存引入了缓存。一般一核CPU只含有一套L1,L2,L3缓存;多核CPU,则每个核心都含有一套L1(甚至和L2)缓存,而共享L3(或者和L2)缓存。

似乎完美解决了处理器高速运算和内存读写速度慢的问题。但是如下几种情况,会有并发问题吗?

  1. 单核单线程。这种情况还好,只有一个核心的缓存,自己独占,不会出现并发问题。

  2. 单核多线程。虽然多线程,但是每个时间片只有一个线程在跑,也不存在并发问题。

  3. 多核心多线程。L1缓存一般都核心独有的,L3(可能L2),是多个线程共享的。这就可能存在多个线程同一个时刻对共享的缓存读写,不共享的缓存中相同变量有可能值不同,这就可能存在缓存一致性问题

那现在回到问题二,为了使处理器内部的运算单元能够尽量的被充分利用,处理器可能会对输入代码进行乱序执行处理,CPU也做了优化。

JAVA也会做类似的操作吗?Java编译器JIT也会做类似的操作,对代码进行优化重排序,以达到最优的运行效率,叫做指令重排。指令重排也会带来并发问题。

并发编程中,为满足数据一致性,数据安全,大家抽象三个特性:

  1. 原子性。

    程序的一个指令或者一个操作,要执行,必须执行完毕,不被中断。注意,这里可以由于CPU调度等可以暂时挂起。

  2. 可见性

    多线程中,每个线程操作的共享变量,对其他线程是立即可见的。

  3. 有序性

    保证程序按照代码的先后顺序进行。

    说了那么多,终于回到Java的内存模型,JMM(Java Memory Model),这里说的是准寻JSR-133的JMM规范。

    JAVA为什么指定这套规范呢?

屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。

11111.jpg

注意,这里的工作内存和主内存,和JVM的内存模型中堆、栈、方法区不是一个层次划分。如果非要类比,可以勉强工作内存类比为JVM中的栈的部分区域,主内存类比为虚拟机的堆内存中的实例数据部分。

JMM规定了内存操作有八种,lock(锁定)、unlock(解锁)、read(读取)、load(载入)、use(使用)、assign(赋值)、store(存储)、write(写入)。还为这八种执行使用指定了规则,这里就不再详述了。

总之,JMM的规范是工作内存到主内存的同步过程。所有的变量都存在存储在主内存中,每个线程都对一个了自己的工作内存,每个线程不能直接操作主内存的变量,而是对主内存中的变量一个副本,对其操作。每个线程的工作内存是独有的,别的线程不能访问。

所以,JMM就是为了多线程下共享内存,存在数据不一致问题,和处理器为了优化而进行的指令重排。

Happen-Before先行发生原则,在编码中,如何确定是否并发安全的就是依靠此原则。

Java提供了volatile、synchronized、final等一系列的关键词来保证线程安全,就是java底层已经封装实现的提供的关键字。

被synchronized修饰的操作部分,是保证原子性、有序性、可见性。由于管的比较宽,所以很多程序员喜欢用,但是过度使用会带来性能问题,虽然编译器进行大量的锁优化。

volatile可以保证有序性和可见性的。

final在构造器一旦初始化后,可以保证可见性。


读后有收获,请作者喝杯咖啡


全部评论

发表评论