vlambda博客
学习文章列表

Java并发探索(5)之内存模型

    在开始本文前,我想请各位思考几个问题

  • 1、假如程序是使用多线程执行,那么线程之间是怎么进行通信,线程之间是以什么机制来进行信息的交换呢?
  • 2、当一个线程修改了共享变量,那又如何通知其它线程此变量已经发生变化?

在并发编程中,多线程如何通信如何同步是首要的解决问题。

线程间通信

在命令式编程中,线程间的通信机制有两种:内存共享消息传递

内存共享并发模型

  • 在共享内存并发模型中,线程之间 共享程序公共状态,通过 写-读内存中的公共状态进行 隐式通信。

消息传递并发模型

  • 在消息传递并发模型中,线程之间 没有公共状态,线程之间必须通过发送消息来 显式进行通信。

线程间同步

同步:指程序中用于控制不同线程间操作发生相对顺序的机制。

  • 在共享内存并发模型,同步是 显式进行的。程序员必须 显式指定某个方法或某段代码需要在线程之间 互斥执行。
  • 在消息传递并发模型,由于消息的发送必须在消息的接收 之前,因此同步是 隐式进行的。

Java的并发采用的是共享内存模型,Java线程之间共享程序的公共状态,通过写-读内存中的公共状态进行隐式通信,整个通信过程对程序员完全透明。

Java内存模型的由来

我们首先得明白一个问题,JMM存在的意义是什么呢?Java并发探索(5)之内存模型由上图可以看出,计算机在做一些我们平时的基本操作时,需要的响应时间是不一样的。

  • 在现代计算机中,cpu的指令速度远超内存的存取速度,由于计算机的存储设备与处理器的运算速度有几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了。

  • 在计算机系统中,寄存器是L0级缓存,接着依次是L1一级缓存,L2二级缓存,L3三级缓存。越往上的缓存存储空间越小,速度越快 ,成本也更高;越往下的存储空间越大,速度更慢,成本也更低。从上至下,每一层都可以看做是更下一层的缓存,即:L0寄存器是L1一级缓存的缓存,L1是L2二级缓存的缓存,依次类推;每一层的数据都是来至它的下一层,所以每一层的数据是下一层的数据的子集。Java并发探索(5)之内存模型

Java内存模型的抽象结构

Java线程之间的通信由Java内存模型控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。

  • 从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系: 线程之间的共享变量存储在主内存中,每个线程都有一个 私有的本地内存,本地内存中存储了该线程以读/写共享变量的 副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了 缓存写缓冲区寄存器以及 其他的硬件编译器优化。
Java并发探索(5)之内存模型
Java内存模型

tips:
1、Java内存模型是一种抽象的概念,工作内存其实也并不存在,也是一个抽象概念。
2、主内存:所有线程创建的实例对象都存放在主内存中。
3、工作内存:存储当前方法的所有本地变量信息(工作内存中存储着主内存中的变量副本拷贝),每个线程只能访问自己的工作内存。

既然以上的Java的内存模型是个抽象概念,那么实际实现中线程的工作内存是怎样的呢?图中所示是一个双核CPU系统架构,每个核有自己的控制器和运算器,其中控制器包含一组寄存器操作控制器,运算器执行算术逻辑运算。每个核都有自己的一级缓存,在有些架构里面还有一个所有CPU都共享的二级缓存。那么Java内存模型里面的工作内存,就对应这里的L1或者L2缓存或者CPU的寄存器。

Java内存模型的挑战

在线程并发时,处理器上的寄存器的读写的速度比内存快几个数量级,为了解决这种速度矛盾,在它们之间加入了高速缓存。

带来的问题
高速缓存带来了缓存一致性的问题,多个线程的多个缓存共享一个主内存区域,那么就可能出现数据一致性问题。

如何解决
需要一个能够维护缓存一致性的协议(原则):原子性可见性有序性、以及为了提高性能对指令的重排序原则。
维护这些原则对性能消耗也变大了,因此编译器处理器也会对指令进行重排序,来提高性能。

JMM保证原子性、可见性、有序性的解决方案

原子性:
Java 内存模型保证了 readloaduseassignstorewritelockunlock操作具有原子性。

实现方式:
使用synchronized关键字或者重入锁(ReentrantLock)保证程序执行的原子性。

可见性:
可见性指当一个线程修改了共享变量的值,其它线程能够立即得知这个修改。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性

实现方式:
设置内存屏障:LoadLoad BarriersStoreStore BarriersLoadStore BarriersStoreLoad Barriers(全能)

  • 使用 volatile修饰变量
  • 使用 synchronized修饰:对一个变量执行 unlock 操作之前,必须把变量值同步回主内存。
  • 使用 final关键字修饰:被 final 关键字修饰的字段在构造器中一旦初始化完成,并且没有发生 this逃逸(逃逸的意思是其它线程通过this引用访问到 初始化了一半的对象),那么其它线程就能看见final字段的值。

有序性
在本线程内观察,所有操作都是有序的。在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排序。

实现方式:

  • volatile关键字通过 添加内存屏障的方式来 禁止指令重排,即重排序时不能把后面的指令放到内存屏障之前,从而保证有序性。
  • synchronized 也可以有序性,它保证每个时刻 只有一个线程执行同步代码,相当于是让线程顺序执行同步代码。

JMM内部还通过happens-before原则来保证多线程环境下两个操作间的原子性可见性以及有序性

总结

CPU的指令速度和内存的存取速度相差几个数量级,为了解决这种速度矛盾,在线程并发时,在它们之间加入了高速缓存。在程序中,Java内存模型就是为了解决这个问题的,可是在解决问题的同时又带来了数据一致性的问题,内存模型通过原子性有序性可见性解决数据一致性的问题。

最后再来思考一个问题吧:Java内存模型的Java内存区域区别是什么?

  • Java内存模型是一个 抽象概念,描述的是一组规则,围绕 原子性有序性可见性展开
  • Java内存区域是在内存JVM中实际的划分,一个是 抽象概念一个是 实际划分,二者在某些概念上有一些联系,比如内存区域的 共享数据区私有数据区,可以对应JMM中的 主内存区域,JMM中的 私有区域或者 工作内存可以对应 JVM中的虚拟机栈本地方法栈以及 程序计数器

好啦,Java内存模型的介绍到这里就结束啦,我是二师兄,期待我的下一篇文章。

扫描二维码

获取更多精彩

带你修仙