第12章 Java内存模型与线程
12.2硬件的效率与一致性
高速缓存解决了处理器与内存速度之前的矛盾,但会带来不一致的问题。在多路处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存。当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致。
添加图片注释,不超过 140 字(可选)
12.3 Java内存模型
12.3.1主内存与工作内存
Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用的变量的主内存副本,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的数据。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
添加图片注释,不超过 140 字(可选)
12.3.2 内存间交互操作
Java内存模型中定义了以下8种操作来完成一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存这一类的实现细节。
添加图片注释,不超过 140 字(可选)
了在执行上述8种基本操作时必须满足如下规则
添加图片注释,不超过 140 字(可选)
添加图片注释,不超过 140 字(可选)
12.3.3 对于volatile型变量的特殊规则
Volatile变量保证此变量对所有线程的可见性,禁止指令重排序优化(增加读写屏障)。
添加图片注释,不超过 140 字(可选)
12.3.4 针对long和double型变量的特殊规则
如果有多个线程共享一个并未声明为volatile的long或double类型的变量,并且同时对它们进行读取 和修改操作,那么某些线程可能会读取到一个既不是原值,也不是其他线程修改值的代表了“半个变 量”的数值。在实际开发中,除非该数据有明确可知的线程竞争,否则我们在编写代码时一般不需要因为这个原因刻意把用到的long和double变量专门声明为volatile。
12.3.5 原子性、可见性与有序性
1.原子性
基本数据类型的访问、读写都是具备原子性的。如果应用场景需要一个更大范围的原子性保证(经常会遇到),Java内存模型还提供了lock和 unlock操作来满足这种需求,也·提供了更 高层次的字节码指令monitorenter和monitorexit来隐式地使用这两个操作——synchronized关键字,因此在synchronized块之间的操作也具备原子性。
2.可见性
指当一个线程修改了共享变量的值时,其他线程能够立即得知这个修改。Volatile、synchronized和final可以实现。
3.有序性
volatile和synchronized两个关键字来保证线程之间操作的有序性。volatile关键字本身就包含了禁止指令重排序的语义,而synchronized则是由“一个变量在同一个时刻只允许一条线程对 其进行lock操作”这条规则获得的。
12.3.6 先行发生原则
时间先后顺序与先行发生原则之间基本没有因果关系
添加图片注释,不超过 140 字(可选)
添加图片注释,不超过 140 字(可选)
12.4 Java与线程
实现线程主要有三种方式:使用内核线程实现(1:1实现),使用用户线程实现(1:N实现), 使用用户线程加轻量级进程混合实现(N:M实现)。
-
内核线程实现 内核线程就是直接由 操作系统内核支持的线程,这种线程由内核来完成线程切换,将线程的任务映射到各个处理器上。每个轻量级进程都需要有一个内核线程的支持,因此一个系统支持轻量级进程的数量是有限的。
添加图片注释,不超过 140 字(可选)
-
用户线程实现
添加图片注释,不超过 140 字(可选)
用户线程的建立、同步、销毁和调度完全在用户态中完成,不需要内核的帮助。所有的线程操作都 需要由用户程序自己去处理。
-
混合实现 既存在用户线程,也存在轻量级进程。操作系统支持的轻量级进程则作为用户线程和内核线程之间的桥梁,这样可以使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用要通过轻量级进程来完成。
添加图片注释,不超过 140 字(可选)
-
Java线程的实现
以HotSpot为例,它的每一个Java线程都是直接映射到一个操作系统原生线程来实现的,线程的调度都由操作系统完成。
12.4.2 Java线程调度
调度主要方式有两种,分别是协同式线程调度和抢占式线程调度。
协同式线程调度:线程的执行时间由线程本身来控制,线程把自己的工作执行 完了之后,要主动通知系统切换到另外一个线程上去。
优点:实现简单
缺点:线程执行时间不可控 制,甚至如果一个线程的代码编写有问题,一直不告知系统进行线程切换,那么程序就会一直阻塞在那里。
抢占式调度:Java线程调度是系统自动完成。设置线程优先级不稳定因为主流虚拟机上的Java线程是被映射到系统的原生线程上来实现的,所以线程调度最终还是由操作系统说了算
12.4.3 状态转换
添加图片注释,不超过 140 字(可选)
12.5 Java与协程
12.5.1 内核线程的局限
12.5.2 协程的复苏
内核线程的调度成本主要来自于用户态与核心态之间的状态转换,而这两种状态转换的开销主要 来自于响应中断、保护和恢复执行现场的成本。
协程:用户线程,主要优势是轻量
12.5.3 Java的解决方案
在新并发模型下,一段使用纤程并发的代码会被分为两部分——执行过程(Continuation)和调度 器(Scheduler)。执行过程主要用于维护执行现场,保护、恢复上下文状态,而调度器则负责编排所有要执行的代码的顺序。将调度程序与执行过程分离的好处是,用户可以选择自行控制其中的一个或者多个,而且Java中现有的调度器也可以被直接重用。
第13章 线程安全与锁优化
13.2 线程安全
当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下 的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对 象的行为都可以获得正确的结果,那就称这个对象是线程安全的。 13.2.1 Java语言中的线程安全
-
不可变 使用final关键字修饰的变量就可以保证它是不可变的。 在Java类库API中符合不可变要求的类型, String,枚举类型及 java.lang.Number的部分子类,如Long和Double等数值包装类型、BigInteger和BigDecimal等大数据类型。
-
绝对线程安全 一 个类要达到“不管运行时环境如何,调用者都不需要任何额外的同步措施,线程安全的类不一定绝对安全如单个原子操作安全,多个原子操作组合会有问题。
添加图片注释,不超过 140 字(可选)
-
相对线程安全 它需要保证对这个对象单次的操作是线程安全的,我们在调用的时候不需要进行额外的保障措施,但是对于一些特定顺序的连续调用,就可能需 要在调用端使用额外的同步手段来保证调用的正确性。
-
线程兼容 本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用。
-
线程对立
是指不管调用端是否采取了同步措施,都无法在多线程环境中并发使用代码。Thread类的suspend()和resume()方法一个尝试去中断线程,一个尝试去恢复线程,在并发进行的情况下,无论调用时是否进行了同 步,目标线程都存在死锁风险
13.2.2 线程安全的实现方法
1.互斥同步
在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一条线程使用。临界区(Critical Section)、互斥量 (Mutex)和信号量(Semaphore)都是常见的互斥实现方式。
最基本的互斥同步手段就是synchronized关键字经过Javac编译之后会在同步块的前后分别形成 monitorenter和monitorexit这两个字节码指令。这两个字节码指令都需要一个reference类型的参数来指明 要锁定和解锁的对象。如果Java源码中的synchronized明确指定了对象参数,那就以这个对象的引用作为reference;如果没有明确指定,那将根据synchronized修饰的方法类型(如实例方法或类方法),来决定是取代码所在的对象实例还是取类型对应的Class对象来作为线程要持有的锁(加在静态方法上锁的是类)。
在执行monitorenter指令时,首先要去尝试获取对象的锁。如果这个对象没被锁定,或者当前线程已经持有了那个对象的锁,就把锁的计数器的值增加一,而在执行 monitorexit指令时会将锁计数器的值减一。一旦计数器的值为零,锁随即就被释放了。如果获取对象锁失败,那当前线程就应当被阻塞等待,直到请求锁定的对象被持有它的线程释放为止。
被synchronized修饰的同步块对同一条线程来说是可重入的。
被synchronized修饰的同步块在持有锁的线程执行完毕并释放锁之前,会无条件地阻塞后面其他线程的进入。
重入锁(ReentrantLock)是Lock接口最常见的一种实现,ReentrantLock与synchronized相比增加了一些高级功能,主要有以下三项:等待可中断、可实现公 平锁及锁可以绑定多个条件(支持多个条件变量)。
等待可中断:是指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改 为处理其他事情。可中断特性对处理执行时间非常长的同步块很有帮助。
公平锁:是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;而非公平 锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。synchronized中的锁是非 公平的,ReentrantLock在默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平 锁。不过一旦使用了公平锁,将会导致ReentrantLock的性能急剧下降,会明显影响吞吐量。
锁绑定多个条件:是指一个ReentrantLock对象可以同时绑定多个Condition对象。在synchronized 中,锁对象的wait()跟它的notify()或者notifyAll()方法配合可以实现一个隐含的条件,如果要和多于一 个的条件关联的时候,就不得不额外添加一个锁;而ReentrantLock则无须这样做,多次调用 newCondition()方法即可。
2.非阻塞同步
CAS指令需要有三个操作数,分别是内存位置(在Java中可以简单地理解为变量的内存地址,用V表示)、旧的预期值(用A表示)和准备设置的新值(用B表示)。CAS指令执行时,当且仅当V符合 A时,处理器才会用B更新V的值,否则它就不执行更新。但是,不管是否更新了V的值,都会返回V的 旧值,上述的处理过程是一个原子操作,执行期间不会被其他线程中断。CAS操作会存在“ABA问题”。
3.无同步方案
可重入代码(Reentrant Code):这种代码又称纯代码(Pure Code),是指可以在代码执行的任何 时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不 会出现任何错误,也不会对结果有所影响。可重入代码有一些共同的特征,例如,不依赖全局变量、存储在堆上的数据和公用的系统资源, 用到的状态量都由参数中传入,不调用非可重入的方法等。
13.3 锁优化
13.3.1 自旋锁与自适应自旋
挂起线程和恢复线程的操作都需要转入内核态中完成,较为费时,可以让后面请求锁的那个线程“稍等一会”,但不放弃处理器的执行时间,(执行一个忙循环)看看持有锁的线程是否很 快就会释放锁。
13.3.2 锁消除
虚拟机即时编译器在运行时,对一些代码要求同步,但是对被检测到不可能存在共享数据竞争的锁进行消除。
13.3.3 锁粗化
将同步块的作用范围限制得尽量小——只在共享数据 的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变少,即使存在锁竞争,等待锁的线程也能尽可能快地拿到锁。
13.3.4 轻量级锁
HotSpot虚拟机的对象头分为两部分,第一 部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄等。这部分数据的长度在32位和64位的Java虚拟机中分别会占用32个或64个比特,官方称它为“Mark Word”。这部分是实现轻量级锁和偏向锁的关键。另外一部分用于存储指向方法区对象类型数据的指针,如果是数组对象,还会有一个额外的部分用于存储数组长度。
Mark Word的32个比特空间里的25个比特将用于存储对象哈希码,4个比特用于存储对象分代年龄,2 个比特用于存储锁标志位,还有1个比特固定为0(这表示未进入偏向模式)。
添加图片注释,不超过 140 字(可选)
添加图片注释,不超过 140 字(可选)
轻量级锁的工作过程:在代码即将进入同步块的时候,如果此同步对象没有被锁定(锁标志位为“01”状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝---》虚拟机将使用CAS操作尝试把对象的Mark Word更新为指向Lock Record的指针---》如果这个更新动作成功了,即代表该线程拥有了这个对象的锁,并且对象Mark Word的锁标志位(Mark Word的 最后两个比特)将转变为“00”,表示此对象处于轻量级锁定状态。---》更新操作失败说明有线程竞争,虚拟 机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是,说明当前线程已经拥有了这个对象的锁,那直接进入同步块继续执行就可以了,否则就说明这个锁对象已经被其他线程抢占了。如果存在竞争则膨胀为重量级锁,状态变为10,后面等待锁的线 程也必须进入阻塞状态。
如果没有竞争,轻量级锁便通过CAS操作成功避免了使用互斥量的开销;但如果确 实存在锁竞争,除了互斥量的本身开销外,还额外发生了CAS操作的开销。因此在有竞争的情况下, 轻量级锁反而会比传统的重量级锁更慢。
添加图片注释,不超过 140 字(可选)
13.3.5 偏向锁
会偏向于第一个获得它的线 程,如果在接下来的执行过程中,该锁一直没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。启用了偏向锁后,当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设置为“01”、把偏向模式设置为“1”,表示进入偏向模式。同时使用CAS操作把获取到这个锁的线程 的ID记录在对象的Mark Word之中。如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关 的同步块时,虚拟机都可以不再进行任何同步操作。
一旦出现另外一个线程去尝试获取这个锁的情况,根据锁对象目前是否处于被锁定的状态决定是否撤销偏向(偏向模式设置为“0”),撤销后标志位恢复到未锁定(标志位 为“01”)或轻量级锁定(标志位为“00”)的状态。
添加图片注释,不超过 140 字(可选)
注:当一个对象已经计算过一 致性哈希码后,它就再也无法进入偏向锁状态了;而当一个对象当前正处于偏向锁状态,又收到需要 计算其一致性哈希码请求[1]时,它的偏向状态会被立即撤销,并且锁会膨胀为重量级锁。