将之前看过的关于并发编程的东西总结记录一下,本文简单记录Java内存模型的相关知识。

1. 并发编程两个关键问题

并发编程中,需要处理两个关键问题:线程之间如何通信及线程之间如何同步。

(1)在命令式编程(命令式编程侧重于告诉计算机先做什么后做什么,与声明式只告诉做什么,不告诉怎么做不同)中,线程间的通信机制有两种:共享内存和消息传递。

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

  ② 在消息传递的并发模型中,线程间没有公共状态,其通过发生消息进行显式通信。

(2)线程同步是用于控制不同线程间操作发生的相对顺序,在共享内存并发模型中,同步是显式进行的,如通过synchronized控制线程互斥的访问某方法或代码块;在消息传递的并发模型中,消息发送再接收之前,同步是隐式进行的。

Java的并发采用的是共享内存模型,通信是隐式的,同步是显式的。

2. Java内存模型

(1)计算机硬件结构

  介绍Java内存模型之前,先看下目前计算机硬件架构,如下图(摘自Java内存模型,具体介绍可参考该文章)。

  由于计算机的存储设备与处理器的运算速度几个数量级的差距,所以现代计算机系统都引入一层读写速度与处理器接近的高速缓存(CPU Cache)作为内存与处理器之间的缓冲。将运算需要使用的数据复制到缓存中,加快运算速度,当运算结束后再从缓存同步到内存中,这样处理器不需要等待缓慢的内存IO。

  在此情况下,引入了新的问题:缓存一致性。在多处理器系统中,每个处理器都有自己的高速缓存,它们共享同意主内存(Main Memory)。为解决缓存一致性的问题,需要各处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作,如MSI协议(监听一致性协议通过利用总线将处理器核的私有Cache 和主存储器连接在一起,每一个私有 Cache 中的控制器会时刻侦听总线上的消息,并根据消息的类型做出相应的操作---百度百科)等。

(2)Java 内存模型的抽象结构

  Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将共享变量存储到内存和从内存取出的机制,这些共享变量包括实例字段、静态字段和构成数组对象的元素,可以认为都存储在堆内存中。

  Java内存模型的抽象结构如下(摘自Java内存模型的深入理解,该图也是《Java并发编程的艺术》中的):

  ① 该模型与上边计算机硬件架构类似,但该主内存是虚拟机的一部分,主要存储所有变量(主要包含堆区部分的变量);

  ② 每条线程都有自己的本地内存(或者叫工作内存),保存主内存汇总变量的副本,线程对变量的操作都是在该内存中进行,可以与处理器高速缓存类比;

  ③ 不同线程间的通信只能通过主内存进行。

(3)内存间的交互

  一个变量从主内存拷贝到本地内存,或从本地内存同步到主内存,需要通过具体的交互协议。Java内存模型定义了8种操作来完成。

lock(锁定) 主内存,将主内存中变量标识为一条线程独占状态
unlock(解锁) 主内存,与lock相反,释放变量的锁
read(读取) 主内存,将变量从主内存传输到本地内存,以便后续的load操作
load(载入) 本地内存,将变量放入本地内存的变量副本中
use(使用) 本地内存,将该变量的值传给执行引擎,用于执行操作,对应使用到该变量的字节码指令
assign(赋值) 本地内存,将从执行引擎接收到的值赋给本地内存的变量,对应赋值字节码指令
store(存储) 本地内存,将本地内存中的变量值传送到主内存,以便后续的write操作
write(写入) 主内存,将从store操作中获取的变量值放入主内存的变量中

  如果要把一个变量从主内存复制到本地内存,需要顺序执行read和load操作,如果把变量从本地内存同步会主内存,需要顺序执行store和write操作。Java内存模型只要求上边两个顺序执行,并不能保证连续执行,即他们之间是可以插入其他命令,非原子操作。这8种基本操作必须满足以下规则:

  ① read和load、store和write不能单独出现

  ② 不允许一个线程丢弃最近assign操作,即本地内存中的改变必须同步回主内存;

  ③ 一个新共享变量只能在主内存中诞生(线程中本地变量可以)

  ④ 一个变量同一时刻只允许一个线程进行lock操作,同一线程可以多次执行lock(对应同一线程可以多次执行synchronized)

  ⑤ 对一个变量执行lock操作时,将会清空本地内存中该变量的值,再执行引擎使用该变量时,需要重新执行load或assign操作初始化变量的值

  ⑥ 对变量执行lock操作,就不可以执行unlock操作

  ⑦ 将一个变量unlock之前,必须先把该变量同步回主内存(执行store和write操作)

(4)原子性、可见性和有序性

  Java内存模型具有原子性、可见性和有序性等3个特性。

  ① 原子性,上边描述的8种操作都具有原子性,如需要更大范围的原子性,可以使用lock和unlock操作。虽然该指令并没有开放给用户使用,但提高了更高层次的字节码指令monitorenter和monitorexit,其对应Java代码中锁定代码块的synchronized关键字。

  ② 可见性,当一个线程对共享变量修改后,其他线程可以立即得到该值。Java内存模型是通过将修改后的变量值同步回主内存,在线程读取前从主内存刷新该变量新值实现可见性,对普通变量和volatile变量都是如此,只是volatile变量的特殊规则保证新值能立即同步到主内存,每次使用前立即从主内存刷新,故volatile能保证多线程操作时变量的可见性,具体volatile讲述见后续文章。

  除了volatile之外,还有两个关键字能实现可见性,synchronized和final,前者通过对线程顺序执行实现,final修饰的字段在构造器中初始化后,并且构造器没有this引用逃逸(把this引用传递出去),其他线程就可以看到final字段的值。

  ③ 有序性,如果在本线程内观察,所有操作都是有序的,即无论编译器或处理器对指令怎么进行重排序,执行结果不变,遵从as-if-serial语义;如果一个线程观察另一个线程,所有操作都是无序的,是指存在“指令重排序”和“本地内存与主内存同步延迟”的现象。

  Java提供了volatile和synchronized保证线程之间的有序性,前者禁止指令重排序,后者由上边规则4获得。

3. 总结

1. Java线程间通信是通过共享内存的方式进行的,通信是隐式地通过更新主内存实现;线程间同步通过显式地通过互斥等方式实现

2. 计算机硬件架构中引入高速缓存以加速处理器,通过相关协议解决多处理器中“缓存一致性”的问题

3. Java内存模型与家算计硬件架构类似,主内存中存储共享变量,本地内存或工作内存中存储变量拷贝,用于各线程计算,可以通过锁实现同步

4. 提供8种操作用于主内存与本地内存的交互,并且必须遵循一定的规则使用

5. Java内存弄下具有原子性、可见性和有序性。

6. 另外,Java内存模型中还存在“先行发生”(happen-before)的原则,其是内存模型中定义的两项操作之间的偏序关系,如说A先行发生于B,则A操作的结果B可以观察到。如Monitor Lock Rule、volatile变量写操作先行于读操作、线程终结、对象终结等。是判断数据是否存在竞争、线程是否安全的主要依据。

4.参考

《Java并发编程的艺术》

《深入理解Java虚拟机》

最新文章

  1. win10使用技巧之如何打出偏僻字母
  2. iOS三种正则表达式
  3. 种子填充算法描述及C++代码实现
  4. 为ListView组件加上快速滑块以及修改快速滑块图像
  5. XML团队介绍发布!
  6. iPhone中修改iMessage关联手机号码的终极方法
  7. opencv笔记4:模板运算和常见滤波操作
  8. SVG添加链接(转载)
  9. winform模拟鼠标按键
  10. jQuery给CheckBox全选与不全选
  11. C#访问C++动态分配的数组指针
  12. octave installation on RHEL6.4
  13. Android版本更新时对SQLite数据库升级或者降级遇到的问题
  14. Shell 脚本中调用另一个 Shell 脚本的三种方式
  15. Mysql对用户的操作
  16. angular 过滤器(日期转换,时间转换,数据转换等)
  17. AI VGG16
  18. Building Microservices with Spring Boot and Apache Thrift. Part 2. Swifty services
  19. 二十、Flyweight 享元模式
  20. 支付宝 iOS SDK 官方下载页面[转]

热门文章

  1. 【Python】【demo实验8】【练习实例】【计算当天是当年的第几天】
  2. spring-boot 使用jdk6(三)
  3. idea配置glassFish
  4. PAT B1018.锤子剪刀布(20)
  5. python中的类变量和对象变量,以及传值传引用的探究
  6. Djangon简介
  7. 树莓派和STM32通过USB和串口通信记录
  8. git clone ssh 时出现 fatal: Could not read from remote repository
  9. Linux系统介绍及部署
  10. 测试使用Timer定时调用http接口