Java并发探索(5)之内存模型
在开始本文前,我想请各位思考几个问题
-
1、假如程序是使用多线程执行,那么线程之间是怎么进行通信,线程之间是以什么机制来进行信息的交换呢? -
2、当一个线程修改了共享变量,那又如何通知其它线程此变量已经发生变化?
在并发编程中,多线程如何通信
和如何同步
是首要的解决问题。
线程间通信
在命令式编程中,线程间的通信机制有两种:内存共享
和消息传递
。
内存共享并发模型
-
在共享内存并发模型中,线程之间 共享程序
的公共状态
,通过写-读
内存中的公共状态进行隐式
通信。
消息传递并发模型
-
在消息传递并发模型中,线程之间 没有公共状态
,线程之间必须通过发送消息来显式
进行通信。
线程间同步
同步:指程序中用于控制不同线程间操作发生相对顺序
的机制。
-
在共享内存并发模型,同步是 显式进行
的。程序员必须显式指定
某个方法或某段代码需要在线程之间互斥
执行。 -
在消息传递并发模型,由于消息的发送必须在消息的接收 之前
,因此同步是隐式
进行的。
Java的并发采用的是共享内存
模型,Java线程之间共享程序的公共状态,通过写-读内存中的公共状态进行隐式通信,整个通信过程对程序员完全透明。
Java内存模型的由来
我们首先得明白一个问题,JMM存在的意义是什么呢?由上图可以看出,计算机在做一些我们平时的基本操作时,需要的响应时间是不一样的。
-
在现代计算机中,cpu的指令速度
远超
内存的存取速度,由于计算机的存储设备与处理器的运算速度有几个数量级
的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(Cache)
来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓存
中,让运算能快速进行,当运算结束后再从缓存同步
回内存之中,这样处理器就无须等待缓慢的内存读写了。 -
在计算机系统中,寄存器是L0级缓存,接着依次是L1一级缓存,L2二级缓存,L3三级缓存。越往上的缓存存储空间越小,速度越快 ,成本也更高;越往下的存储空间越大,速度更慢,成本也更低。从上至下,每一层都可以看做是更下一层的缓存,即:L0寄存器是L1一级缓存的缓存,L1是L2二级缓存的缓存,依次类推;每一层的数据都是来至它的下一层,所以每一层的数据是下一层的数据的子集。
Java内存模型的抽象结构
Java线程之间的通信由Java内存模型
控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。
-
从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系: 线程之间的共享变量存储在主内存
中,每个线程都有一个私有的本地内存
,本地内存中存储了该线程以读/写共享变量的副本
。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存
、写缓冲区
、寄存器
以及其他的硬件
和编译器
优化。
tips:
1、Java内存模型是一种抽象的概念,工作内存其实也并不存在,也是一个抽象概念。
2、主内存:所有线程创建的实例对象
都存放在主内存中。
3、工作内存:存储当前方法的所有本地变量
信息(工作内存中存储着主内存中的变量副本拷贝),每个线程只能访问自己的工作内存。
既然以上的Java的内存模型是个抽象概念,那么实际实现中线程的工作内存是怎样的呢?图中所示是一个双核CPU系统架构,每个核有自己的控制器和运算器,其中控制器包含一组寄存器
和操作控制器
,运算器执行算术逻辑运算
。每个核都有自己的一级缓存,在有些架构里面还有一个所有CPU都共享的二级缓存
。那么Java内存模型里面的工作内存,就对应这里的L1或者L2缓存或者CPU的寄存器。
Java内存模型的挑战
在线程并发时,处理器上的寄存器的读写的速度比内存快几个数量级,为了解决这种速度矛盾,在它们之间加入了高速缓存。
带来的问题
高速缓存带来了缓存一致性
的问题,多个线程的多个缓存共享
一个主内存区域,那么就可能出现数据一致性
问题。
如何解决
需要一个能够维护缓存一致性的协议(原则):原子性
、可见性
、有序性
、以及为了提高性能对指令的重排序
原则。
维护这些原则对性能消耗也变大了,因此编译器
和处理器
也会对指令进行重排序,来提高性能。
JMM保证原子性、可见性、有序性的解决方案
原子性:
Java 内存模型保证了 read
、load
、use
、assign
、store
、write
、lock
和unlock
操作具有原子性。
实现方式:
使用synchronized
关键字或者重入锁(ReentrantLock)
保证程序执行的原子性。
可见性:
可见性指当一个线程修改了共享变量的值,其它线程能够立即
得知这个修改。Java内存模型是通过在变量修改后将新值同步回主内存
,在变量读取前
从主内存刷新变量值来实现可见性
。
实现方式:
设置内存屏障:LoadLoad Barriers
、StoreStore Barriers
、LoadStore Barriers
、StoreLoad 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内存模型的介绍到这里就结束啦,我是二师兄,期待我的下一篇文章。
扫描二维码
获取更多精彩
带你修仙