并发编程模型
并发编程中需要处理的两个关键性的问题是:线程之间的通信以及线程之间的同步。在命令式编程中,有两种通信方式:共享内存和消息传递
- 共享内存:读写内存中公共状态来隐式实现线程之间的通信,共享内存通信的同步机制是显示进行的,程序开发人员需要在某个代码或者某个方法显示的进行互斥执行
- 消息传递:通过明确的发送消息来实现通信,线程之间没有公共状态,消息传递通信的同步机制是隐式的,消息的发送在接收之前
Java的通信机制是共享内存
内存模型
在计算机中,数据的读写涉及到CPU和内存,由于CPU的处理速度非常快,而从内存中进行读写的速度慢,这样导致指令执行速度降低,所以CPU里面引入了高速缓存。数据的读写的过程就是先从内存中读取数据到高速缓存中,然后在高速缓存中计算,最后把最终结果写回至内存中。这种处理机制在单线程中是没有问题的,但是在多CPU,每条线程可能运行在不同的CPU中,这样就是导致一个问题,一个变量在不同的CPU都存在缓存,这样都存在数据的一致性问题。
如何解决这个问题?
-
在总线上加LOCK锁
-
通过缓存一致性协议
在早期的CPU当中,是通过在总线上加LOCK#锁的形式来解决缓存不一致的问题。因为CPU和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK#锁的话,也就是说阻塞了其他CPU对其他部件访问(如内存),从而使得只能有一个CPU能使用这个变量的内存。这样处理效率比较低下。所以就出现了缓存一致性协议。最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
![](https://i-blog.csdnimg.cn/blog_migrate/d00245cebda37298c244f7a503740de6.png)
Java内存模型(JMM)
JMM 定义个一个线程的写入何时对另外线程可见,可以理解为JMM是对计算机内存模型的一个抽象。在JMM中,共享变量存储在主内存中(计算机存储在内存中),每个线程拥有自己独立的本地内存(计算机每个线程有自己的高速缓存),JMM中线程的读写是先从主内存读取值到自己本地内存,在本地内存中进行读写,最后将最终结果刷新到主内存中。
内存间的交互操作
在JMM模型定义了八种操作
-
lock(锁定):作用于主内存变量,把一个变量标识为一个线程独占的状态
-
unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量可以被其他线程锁定
-
read(读取):作用于主内存变量,把一个变量的值从主内存中传输到线程的工作内存中,以便随后的load操作使用
-
load(加载):作用于工作内存变量,把read操作从主内存的读取的变量的值放入到工作内存的变量副本中
-
use(使用):作用于工作内存变量,把工作内存变量的值传递给执行引擎,每当虚拟机需要使用这个变量的字节码的指令时都会执行这个操作
-
assign(赋值):作用于工作内存变量,把一个从执行引擎得到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作
-
store(存储):作用于工作内存变量,将工作内存的变量的值传送到主内存中,以便后续的write操作使用
-
write(写入):作用于主内存变量,把store操作从工作内存得到的值写入到主内存变量中
从主内存复制到工作内存,必须顺序执行 read 和 load 操作,从工作内存复制到主内存,必须顺序执行 store 和 write 操作
JMM规定执行以上八个操作的规则:
-
不允许read和load、store和write 单一出现,也就是说需要成双成对对线
-
不允许一个线程丢弃它最近的assign操作,就是说工作内存变量发生改变,必须将改变同步至主内存中
-
不允许线程无原因(没有发生assign操作)把工作内存变量同步至主内存中
-
对一个变量执行store和write操作执行,必须执行过了assign和load操作
-
一个变量只能有一个线程执行lock操作,一个线程可以执行多个lock,但是对应的需要执行同样的unlock操作
-
对一个变量执行lock操作,会清空工作内存此变量的值,在执行引擎使用该变量的前,需要执行load和assign操作
-
在执行unlock之前,必须将变量的改变同步到主内存中,就是执行store和write操作
-
lock和unlock需要成双成对出现
原子性/可见性/有序性
Java内存模型是按照原子性、可见性、有序性三个特征来建立的。并发需要正确的执行,需要保证原子性 可见性 有序性,有一个没保证有可能会导致运行不正确。
-
原子性:一个操作或者多个操作要么全部执行而且执行过程不被打断,要么全部不执行。
-
可见性:多个线程访问一个变量,一个线程修改了共享变量,其他线程能立刻看到这个变量的修改
-
有序性:程序执行的顺序按照代码的先后顺序执行
在JMM中如何保证原子性 可见性 有序性
-
在Java内存模型中,只保证基本数据类型的读写是原子性操作,如果需要保证多范围的原型性,需要通过同步控制,可以使用 synchronized 和 lock来实现,保证同一时刻只有一个线程执行该代码块。
-
对于可见性,Java中提供了volatitle保证可见性。 通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
-
在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。 在Java里面,可以通过volatile关键字来保证一定的“有序性”。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。
-
在JMM中,有一些先天的有序性,不需要任何手段保证有序性,称为happen-before原则
happen-before原则
-
程序的次序原则:一个线程内,按照代码的次序,每一个操作happen-before于后续动作
-
锁规则:解锁unlock happen-before于加锁 lock
-
volatile原则:对一个volatile变量的写 happen-before于对volatile变量的读
-
传递性:A happen-before B,B happen-before C => A happen-before C
-
线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
-
线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
-
线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
-
对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始