1.并发编程模型的两个关键问题:线程之间的通信和同步。
线程之间的通信有两种:共享内存和消息传递。
同步是指程序中用于控制不同线程间操作发生相对顺序的机制。
Java的并发采用的是共享内存模型,下面是Java内存模型的抽象结构:
JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程有一个本地内存,本地内存存储了该线程以读/写共享变量的副本。本地内存是一个抽象概念,并不真实存在。
当线程A要与线程B通信的话,必须经历下面两个步骤:
(1).线程A把本地内存A中更新过的共享变量刷新到主内存中;
(2).线程B到主内存中去读取线程A之前已经更新过的共享变量。
2.happens-before简介:
在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。
相关的规则如下:
(1).程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后序操作;
(2).监视器锁规则:对于一个锁的解锁,happens-before于随后对这个锁的加锁;
(3).volatile变量规则:对于一个volatile变量域的写,happens-before于任意后续对于这个volatile变量域的读;
(4).传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
注意:两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行。happens-before仅仅要求前一个操作(执行的结果)对于后一个操作可见,且前一个操作按顺序排在第二个操作之前。
3.重排序:
(1)重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。简单来说:
int a = 0;
int b = 1;
上面两个语句,执行的顺序可能是先执行第二条,再执行第一条。
(2).数据依赖性:如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时两个操作之间就存在数据依赖性。
a.写后读;
b.写后写;
c.读后写;
上面3中情况,只要重排序两个操作的顺序,程序的执行结果就会改变。因此编译器和处理器在重排序时,会遵守数据依赖性,即不会改变存在数据依赖关系的两个操作的执行顺序。
(3).as-if-serial语义:不管怎么重排序,程序的执行结果不变。(更在单线程中执行的结果一致)
4.volatile的内存语义:
前面已经介绍过volatile了:volatile、synchronized和CAS
可以把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步。
volatile变量自身具有下面两个特性:
(1).对一个volatile变量的读,总能看到(任意线程)对这个volatile变量最后的写入;
(2) .对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。
volatile写-读的内存语义:
(1).volatile写的内存语义:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存;
(2).volatile读的内存语义:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来从主内存中读取共享变量。
5.锁的内存语义:
当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中;
当线程获取锁时,JMM会把该线程对应的本地内存置为无效,从而使的被监视器保护的临界区代码必须从主内存中读取共享变量。
根据上述可发现:锁释放与volatile变量写有相同的内存语义;锁获取与volatile变量读有相同的内存语义。
6.final域的内存语义:
相对锁和volatile变量,final域的读和写更像是普通变量的访问。
总的来说,对于final域,编译器和处理器要遵循两个重排序规则:
(1).在构造函数内对一个final域的写入,域随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序;
(2).初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。