Java并发编程理论入门理解笔记
参考材料:极客时间《Java并发编程实战》–王宝令
并发编程Bug的源头
你我都知道,编写正确的并发程序是一件非常困难的事情,并发程序的Bug
往往会诡异的出现,然后又诡异的消失。为了能够轻松并精准地解决并发编程中的Bug
,了解这些Bug
出现的源头是必须的。
计算机中有很主要的三个部分:CPU
、内存和I/O
设备,我们清楚的知道,其三者之间是存在速度的差异的。为了合理的利用CPU
的高性能,平衡这三者的速度差异,计算机系统结构、操作系统以及编译程序都做出了方案去进行优化,使得我们的程序可以更加高效率地运行,但同时也会带来相应的问题。(这里我们就不得不提一件事情,任何的解决方案在解决当前问题的同时,也往往会引起其他的问题,所以在采用一项技术的同时,一定要清楚它带来的问题是什么,以及如何规避)
在计算机系统结构中,我们引入了缓存,用于均衡内存和CPU
之间的速度差异。但是在这个多核的时代,每一个CPU
中都有自己的缓存,这个时候,CPU
缓存与内存的数据一致性就没有那么容易保持了,简单来说就是两个CPU
之间对各自的缓存中的变量进行的操作对另一个CPU
是不可见的,这就带来的可见性的问题(一个线程对共享变量的修改,另外一个线程能够立刻看到,我们称为可见性)。
在操作系统中,我们增加了进程、线程,以分时复用CPU
,进而均衡CPU
与I/O
设备的速度差异。操作系统允许某一个进程先执行一小段时间,然后待过了一段时间之后,操作系统会重新选择一个进程来执行,这个一段时间,我们就称为时间片。Java
并发程序是基于多线程的,这自然也会涉及到任务切换。因为CPU能保证的原子操作是CPU指令级别的,而不是高级语言的操作符,这就导致在Java
并发编程的过程中出现原子性的问题(我们把一个或者多个操作在CPU
执行的过程中不被中断的特性称为原子性)。
在编译程序中,我们会优化指令的执行次序,使得缓存能够得到更加合理的利用。但是由于编译程序对原有的语句的执行顺序进行了改变,在某些情况下会影响到程序的最终结果。这就是编译程序带来的有序性问题(有序性即程序执行的顺序按照代码的先后顺序执行)。
在这里我们以及清楚的了解了到了并发编程Bug
的主要的源头:可见性、原子性和有序性。只要我们能够深刻理解可见性、原子性、有序性在并发场景下的原理,很多并发Bug
都是可以理解、可以诊断的。那么我们该如何去解决这三个问题呢?
Java内存模型
Java
内存模型(Java Memory Model
,JMM
)用于屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果,JMM
规范了Java虚拟机与计算机内存是如何协同工作的:规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。站在一个程序员的角度上进行理解,Java
内存模型规范了JVM如何提供按需禁用缓存和编译优化的方法。具体来说,这些方法包括volatile
、synchronized
和final
三个关键字,以及Happens-Before
规则。所以说Java
内存模型为我们解决可见性和有序性问题提供了帮助。
volatile
关键字其实并非是Java
语言特有的,在C
语言里也有,它最原始的意义就是禁用CPU
缓存。如果我们将一个变量使用volatile
修饰,这就指示 编译器,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。
volatile
语义增强是离不开Happens-Before
规则,所谓Happens-Before
规则要表达的是:前面一个操作的结果对后续操作是可见的。这里只说明以下的六项规则:
-
程序次序规则:在一个线程内,按照控制流顺序,书写在前面的操作
Happens-Before
于书写在后面的操作。 -
管程锁定规则:一个
unlock
操作Happens-Before
于后面对一个锁的lock
操作。 -
volatile
变量规则:对一个volatile变量的写操作Happens-Before
于后面对这个变量的读操作。