电子书:
链接: https://pan.baidu.com/s/1hQecjJw7SHQcophVIY-miw 提取码: 4ava
简介
这是一本适合入门以及深入Java并发编程的书籍,“介绍了Java并发框架、线程池的实现原理。但不仅局限于Java层面,而是深入到JVM、甚至CPU层面从底层看并发技术”。
但书中的JDK版本基于1.5 1.6 1.7,并且个人认为阅读前需要了解一定的数据结构、操作系统、JVM等相关知识
(摘自前言)
阅读本书之前,希望读者必须有一定的Java基础和开发经验,最好还有一定的并发编程基础。如果读者是一名并发编程初学者,建议读者按照顺序阅读本书,并按照书中的例子进行编码和实战。如果读者是有一定的并发编程经验,可以把本书当做一个手册,直接看需要学习的章节。以下是各章节的基本介绍:
第一章 Java并发编程的挑战,会向读者说明进入并发编程的世界里,你们可能会遇到哪些问题,以及如何解决。
第二章 Java并发编程的底层实现原理,介绍在CPU和JVM这个层面是如何帮助Java实现并发编程的。
第三章 详细深入介绍了Java的内存模型。Java线程之间的通信对程序员完全透明,内存可见性问题很容易困扰Java程序员,本章试图揭开Java内存模型神秘的面纱。
第四章 从介绍多线程技术带来的好处开始,讲述了如何启动和终止线程以及线程的状态,详细阐述了多线程之间进行通信的基本方式和等待/通知经典范式。
第五章 介绍Java并发包中与锁相关的API和组件,以及这些API和组件的使用方式和实现细节。
第六章 介绍了Java中的大部分并发容器,并深入剖析其实现原理,让读者领略大师的设计技巧。
第七章 介绍了Java中的原子操作类,并给出一些实例。
第八章 介绍了Java中提供的很多并发工具类,相信这个是并发编程中的瑞士军刀。
第九章 介绍了Java中的线程池实现原理和使用建议。
第十章 介绍了Executor框架的整体结构和成员组件。
第十一章 介绍几个并发编程的实战,以及如何排查并发编程造成的问题。
目录(仅是笔记目录)
2.2 synchronized(可参考 synchronized)
第一章 并发编程的挑战
1.1 上下文切换会影响执行效率,如何减少上下文切换
- 无锁并发模式,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据
- CAS算法(Compare And Swap)
- 使用最少线程,如减少JBOSS工作线程数,使WAITING线程尽可能少,减少上下文切换的次数
- 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换
1.2 死锁,如何避免死锁
- 避免一个线程同时获取多个锁
- 尽量保证每个锁只占一个资源
- 尝试使用定时锁
第二章 Java并发机制的底层实现原理
2.1 volatile(可参考 volatile)
2.1.1 volatile如何实现可见性
通过Lock前缀的指令。将当前处理器缓存的数据写会系统内存,并使在其他CPU里缓存了该内存地址的数据无效(每个处理器会通过嗅探在总线上传播的数据来检查自己缓存的值是否过期)
2.1.2 volatile的使用优化 —— 追加64字节(JDK7,仅限64字节缓存行处理器)
如果队列的头节点和尾节点都不足64字节,处理器会将他们都读到同一个高速缓存行中,在多处理器下每个处理器都会缓存同样的头尾节点;当一个处理器试图修改头节点时,会导致其他处理器不能访问高速缓存中的尾节点,从而影响入队和出队效率
2.2 synchronized(可参考 synchronized)
2.2.1 Synchronized锁分类
- 对于普通同步方法,锁是当前实例对象
- 对于静态同步方法,锁是当前类的Class对象
- 对于同步方法块,锁是Synchronized括号里配置的对象
2.2.2 Synchronized锁的实现原理
JVM基于进入和退出Monitor对象来实现方法同步和代码块同步。
monitorenter指令是在编译后插入到同步代码块开始的位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之匹配。任何对象都有一个monitor与之关联,并且一个monitor被持有后将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象对应的monitor的所有权,即对象的锁
2.2.2 Java对象头
synchronized用的锁是存在Java对象头里的。Java对象头里的Mark Word里默认存储对象的HashCode、分代年龄和锁标记位。32位JVM 的Mark Word的默认存储结构如表2-3所示
![](https://i-blog.csdnimg.cn/blog_migrate/127663bc72928f8df6443fd84d7dfdc6.png)
![](https://i-blog.csdnimg.cn/blog_migrate/4cab2111746a7b3e99a6726870149a78.png)
2.2.3 锁的升级与对比(这节很重要!!!)
锁的状态按级别分类:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态
偏向锁
大多数情况,锁不仅不存在多线程竞争,而且总是由同 一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。
当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出 同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否 存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需 要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则 使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。偏向锁的撤销:需要等待全局安全点(在这个时间点上没有正在执行的字节码),会暂停拥有偏向锁的线程
轻量级锁
( 1 ) 轻 量 级锁 加 锁线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
( 2 ) 轻 量 级锁 解 锁轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。图2-2是两个线程同时争夺锁,导致锁膨胀的流程图。
锁的优缺点对比
2.3 原子操作的实现原理
2.3.1 处理器如何实现原子操作
1)使用总线锁保证原子性
2)使用缓存锁保证原子性
2.3.2 Java如何实现原子操作
1)锁
2)使用循环CAS实现原子操作
第三章 Java内存模型(JMM)
3.1 JMM基础
3.1.1 JMM的抽象结构
3.1.2 从源代码到指令序列的重排序
在 执 行程序 时 , 为 了提高性能, 编译 器和 处 理器常常会 对 指令做重排序。重排序分 3 种 类型。
编译器优化的重排序 。 编译 器在不改 变单线 程程序 语义 的前提下,可以重新安排 语 句的执 行 顺 序。 指令级并行的重排序。 如果不存在数据依 赖 性, 处 理器可以改 变语 句 对应机器指令的执 行 顺 序。 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上 去可能是在乱序执 行。从Java源代码到最终实际执行的指令序列,会分别经历下面3种重排序:
上述的1属于编译器重排序,2和3属于处理器重排序。这些重排序可能会导致多线程程序出现内存可见性问题。对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。对于处理器重排序,JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障(Memory Barriers,Intel称之为 Memory Fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序。
3.1.3 内存屏障
为了保证内存可见性,Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。JMM把内存屏障指令分为4类,如下表所示。(Load和Store可以和read/write类比,Load相当于将主内存数据加载到工作内存,而store是将工作内存数据刷新到主内存)
3.1.4 happens-before 简介
JSR-133使用happens-before的概念来阐述操作之间的内存可见性。在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。
3.2 重排序
3.2.1 数据依赖性
3.2.2 as-if-serial
as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),(单线程) 程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。
3.3 volatile的内存语义
3.3.1 volatile内存语义的实现
前文提到过重排序分为编译器重排序和处理器重排序。为了实现volatile内存语义,JMM 会分别限制这两种类型的重排序类型。表3-5是JMM针对编译器制定的volatile重排序规则表。
从表3-5我们可以看出:
当第二个操作是 volatile 写 时 ,不管第一个操作是什么,都不能重排序。 这 个 规则 确保 volatile写之前的操作不会被编译器重排序到volatile写之后。 当第一个操作是 volatile 读时 ,不管第二个操作是什么,都不能重排序。 这 个 规则 确保 volatile读之后的操作不会被编译器重排序到volatile读之前。 当第一个操作是 volatile 写,第二个操作是 volatile 读时 ,不能重排序。
为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能。为此,JMM采取保守策略。下面是基于保守策略的JMM内存屏障插入策略:
- 在每个volatile写操作的前面插入一个StoreStore屏障。(保证在volatile写之前的所有普通写操作已经对处理器可见了)
- 在每个volatile写操作的后面插入一个StoreLoad屏障。(避免在volatile写之后,可能有volatile读/写重排序)
- 在每个volatile读操作的后面插入一个LoadLoad屏障。(避免volatile读与之后的普通读重排序)
在每个 volatile 读 操作的后面插入一个 LoadStore 屏障。( 避免volatile读与之后的普通写重排序 )
volatile写/读指令序列示意图
3.4 锁的内存语义
ReentrantLock为例,依赖于AQS(Java同步器框架)。AQS使用一个整形的volatile变量(state)来维护同步状态
对于公平锁:获取锁时,首先回去读state变量;释放锁时,最后写state。
对于非公平锁:获取锁时,会用CAS更新volatile变量;释放锁时,最后写state。
3.5 happens-before
3.5.1 happens-before 的定义
《JSR-133:Java Memory Model and Thread Specification》对happens-before关系的定义如下:
1)如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
2)两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照 happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)。
3.5.2 happens-before 规则
《JSR-133:Java Memory Model and Thread Specification》定义了如下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。
5)start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的 ThreadB.start()操作happens-before于线程B中的任意操作。
6)join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作
happens-before于线程A从ThreadB.join()操作成功返回。
3.6 双重检查锁定与延迟初始化
3.6.1 Double-Checked Locking 多线程不一定安全
原因:重排序
解决方法:将单例实例变量定义为volatile,或使用静态内部类实现单例
第四章 Java并发编程的基础
4.1 线程生命周期与创建方法
4.2 线程常用方法
4.3 Daemon Thread
4.4 ThreadLocal
第五章 Java中的锁
5.1 Lock
Lock接口提供的synchronized关键字所不具备的主要特性:
5.2 队列同步器(AQS)
队列同步器AbstractQueuedSynchronizer(以下简称同步器),是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作,并发包的作者(Doug Lea)期望它能够成为实现大部分同步需求的基础。
同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的3个方法(getState()、setState(int newState)和compareAndSetState(int expect,int update))来进行操作,因为它们能够保证状态的改变是安全的。子类推荐被定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用,同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型的同步组件(ReentrantLock、ReentrantReadWriteLock和CountDownLatch等)。
同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。可以这样理解二者之间的关系:锁是面向使用者的,它定义了使用者与锁交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者所需关注的领域。
5.2.1 AQS的API
同步器的设计基于模板方法模式。
重写同步器指定的方法时,需要使用同步器提供的如下3个方法来访问或修改同步状态。
1) getState():获取当前同步状态。
2) setState(int newState):设置当前同步状态。
3) compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性。
可重写方法:
AQS提供的模板方法:
同步器提供的模板方法基本上分为3类:独占式获取与释放同步状态、共享式获取与释放同步状态和查询同步队列中的等待线程情况。独占锁就是在同一时刻只能有一个线程获取到锁,而其他获取锁的线程只能处于同步队列中等待,只有获取锁的线程释放了锁,后继的线程才能够获取锁。
5.2.2 队列同步器的实现分析
同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。
节点是构成同步队列(等待队列,在5.6节中将会介绍)的基础,同步器拥有首节点(head)和尾节点(tail),没有成功获取同步状态的线程将会成为节点加入该队列的尾部,同步队列的基本结构如图5-1所示。
为了保证线程安全,AQS提供了一个基于CAS的设置尾节点的方法:compareAndSetTail(Node expect,Node update),它需要传递当前线程“认为”的尾节点和当前节点,只有设置成功后,当前节点才正式与之前的尾节点建立关联。
设置首节点是通过获取同步状态成功的线程来完成的,由于只有一个线程能够成功获取到同步状态,因此设置头节点的方法并不需要使用CAS来保证。
独占式同步状态获取与释放
在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;移出队列 (或停止自旋)的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时,同步器调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继节点。
独占式超时获取同步状态
共享式同步状态获取与释放
获取条件:tryAcquireShared(int arg)方法返回值大于等于0
释放:循环CAS唤醒后续等待节点
5.3 重入锁
ReentrantLock,支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。除此之外,该锁的还支持获取锁时的公平(先请求先响应,FIFO)和非公平性选择。
Mutex(独占式锁)不支持冲入,synchronized关键字隐式支持冲入。
5.3.1 重入锁的获取与释放
获取:识别获取锁的线程是否为当前占据锁的线程。
释放:线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示当前锁被重复获取的次数,而锁被释放时,计数自减,当计数等于0时表示锁已经成功释放。
5.3.2 公平锁与非公平锁
公平锁与非公平锁的唯一不同的位置为判断条件多了 hasQueuedPredecessors()方法,即加入了同步队列中当前节点是否有前驱节点的判断,如果该方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释 放锁之后才能继续获取锁。
5.4 读写锁
Mutex 和 ReentrantLock 都是排他锁,而读写锁允许多个读线程同时访问。
5.4.1 读写锁的实现
在ReentrantLock中同步状态表示锁被一个线程重复获取的次数,而读写锁将 变量切分成了两个部分,高16位表示读,低16位表示写。
5.4.2 读写锁的获取与释放
写锁是一个可重入的排他锁,而读锁是一个可重入的共享锁。读锁的获取不具有安全性,只有在增加读状态时依靠CAS保证线程安全(读状态时所有线程获取读锁次数的综合,而每个线程的读锁获取次数只保存在ThreadLocal中)
5.4.3 锁降级
锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁,然后将其释放,最后再获取读 锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的)写锁,再获取到 读锁,随后释放(先前拥有的)写锁的过程。
示例:
public void processData() {
readLock.lock();
if (!update) {
// 必须先释放读锁
readLock.unlock();
// 锁降级从写锁获取到开始
writeLock.lock();
try {
if (!update) {
// 准备数据的流程(略)
update = true;
}
readLock.lock();
} finally {
writeLock.unlock();
}
// 锁降级完成,写锁降级为读锁 }
}
try {
// 使用数据的流程(略)
} finally {
readLock.unlock();
}
}
上述示例中,当数据发生变更后,update变量(布尔类型且volatile修饰)被设置为false,此 时所有访问processData()方法的线程都能够感知到变化,但只有一个线程能够获取到写锁,其 他线程会被阻塞在读锁和写锁的lock()方法上。当前线程获取写锁完成数据准备之后,再获取 读锁,随后释放写锁,完成锁降级。
锁降级中读锁的获取是否必要呢?答案是必要的。主要是为了保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程(记作线程T)获取了写锁并修 改了数据,那么当前线程无法感知线程T的数据更新。如果当前线程获取读锁,即遵循锁降级 的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进 行数据更新。
5.5 LockSupport工具
当阻塞或者唤醒一个线程的时候都会使用LockSupport工具类来完成。
5.6 Condition接口
任意一个Java对象,都拥有一组监视器方法(定义在java.lang.Object上),主要包括wait()、 wait(long timeout)、notify()以及notifyAll()方法,这些方法与synchronized同步关键字配合,可以 实现等待/通知模式。Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等 待/通知模式,但是这两者在使用方式以及功能特性上还是有差别的。
5.6.1 Condition 的实现分析
ConditionObject是同步器AbstractQueuedSynchronizer的内部类,因为Condition的操作需要 获取相关联的锁,所以作为同步器的内部类也较为合理。每个Condition对象都包含着一个队 列(以下称为等待队列),该队列是Condition对象实现等待/通知功能的关键。
等待队列
等待队列是一个FIFO的队列,在队列中的每个节点都包含了一个线程引用,该线程就是 在Condition对象上等待的线程,如果一个线程调用了Condition.await()方法,那么该线程将会 释放锁、构造成节点加入等待队列并进入等待状态。事实上,节点的定义复用了同步器中节点 的定义,也就是说,同步队列和等待队列中节点类型都是同步器的静态内部类 AbstractQueuedSynchronizer.Node。
在Object的监视器模型上,一个对象拥有一个同步队列和等待队列,而并发包中的 Lock(更确切地说是同步器)拥有一个同步队列和多个等待队列,其对应关系如图5-10所示。
如图所示,Condition的实现是同步器的内部类,因此每个Condition实例都能够访问同步器 提供的方法,相当于每个Condition都拥有所属同步器的引用。
第六章 Java并发容器和框架
6.1 concurrentHashMap
6.1.1 为什么使用concurrentHashMap
1)HashMap线程不安全。多线程会导致HashMap的Entry链表形成环形数据结构,从而产生死循环
2)HashTable使用synchronized,保证了线程安全但是效率低下
3)ConcurrentHashMap的锁分段技术可有效提升并发访问率
6.1.2 ConcurrentHashMap 的结构
ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重 入锁(ReentrantLock),在ConcurrentHashMap里扮演锁的角色;HashEntry则用于存储键值对数 据。一个ConcurrentHashMap里包含一个Segment数组。Segment的结构和HashMap类似,是一种 数组和链表结构。一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元 素,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时, 必须首先获得与它对应的Segment锁