Java并发编程的艺术
第1章 并发编程的挑战
1.1 上下文切换
单核处理器也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制,当前线程执行时间片后会切换到下一个任务,这种切换会影响到多线程的执行速度
多线程不一定快
多线程会有创建和上下文切换的开销,所以在并发操作次数不够多的时候速度比串行慢
如何减少上下文切换
无锁并发编程、CAS算法、使用最少线程和协程
-
无锁并发编程:
多线程竞争锁引起上下文切换,可以使用办法来避免使用锁,比如将数据按照hash算法取模分段,不同的线程处理不同段的数据
-
CAS算法:
Java的atomic包使用CAS算法更新数据就不需要加锁
-
使用最少线程:
避免创建不需要的线程,比如任务很少创建了很多的线程,造成大量线程等待
-
协程:
单线程里实现多任务调度,并在单线程里面维持多个任务的切换
1.2 死锁
出现死锁我们可以通过dump线程查看哪个线程出现了问题,使用jps查看Java进程号,导出dump文件查看
避免死锁的方法:
- 避免一个线程同时获取多个锁
- 避免一个线程在锁内同时占用多个资源,保证每个锁只占用一个资源
- 尝试使用定时锁,使用lock。tryLock(timeout)替代使用内部锁机制
- 对于数据库锁,加锁和解锁必须在同一个数据库连接里
1.3 资源限制的挑战
资源限制:在进行并发编程时,程序的执行速度受限于计算机硬件资源或软件资源,硬件资源限制:带宽的上传/下载速度,硬盘读写和CPU处理速度;软件资源限制:数据库的连接数和socket连接数
第2章 Java并发机制的底层实现原理
Java代码编译后变成字节码,字节码被类加载器加载到JVM中,JVM执行字节码转变为汇编指令在CPU上执行
2.1 volatile 的应用
volatile 是轻量级的synchronized,在多处理器中保证了共享变量的可见性。可见性:一个线程修改共享变量时,另外一个线程可以读到这个修改的值。不会引起上下文切换和调度,比synchronized的使用和执行成本更低。
volatile 定义与实现原理
定义:一个字段被声明为volatile,jvm保证所有线程看到的这个变量的值是一致的
我们通过获取JIT编译器生成的汇编指令查看对volatile写操作时,其修饰的共享变量会多出这行代码:lock add1 ...
,生成的**lock前缀指令**会做两件事情:
-
将当前缓存行的数据写回到系统内存
多处理环境中,lock信号确保在声言该信号期间处理器可以独占任何共享内存(锁住总线,其他线程无法访问),最近的处理器锁缓存并写回到内存,利用缓存一致性机制保证修改的原子性,被称为“缓存锁定”,缓存一致性机制阻止同时修改两个以上处理器缓存的内存区域数据
-
这个写的操作会使得其他CPU里缓存了该内存地址的数据无效
嗅探技术保证内部缓存、系统内存和其他处理器的缓存的数据在总线上保持一致
为了提高处理速度,处理器不直接和内存通信,而是先将系统内存的数据读到内部缓存后再进行操作,但是操作完不知道何时会写到内存
缓存一致性协议:对声明了volatile关键字的变量进行写操作,jvm会向处理器发送一条lock前缀指令,将缓存行写到系统内存。每个处理器通过嗅探总线上传播的数据来检查自己缓存的值是不是过期了,如果发现自己缓存行对应的内存地址被修改就将当前处理器的缓存行设置为无效状态,当要对这个数据进行修改的时候重新从系统内存中把数据读到处理器缓存里
volatile使用优化
jdk7的并发包下新增一个队列集合类LinkedTransferQueue
2.2 synchronized 的实现原理与应用
Java中的每个对象都可以作为锁,表现为:
- 修饰普通同步方法,锁:当前实例对象
- 静态方法,锁:类的class对象
- 同步方法块:synchronized括号里配置的对象
jvm基于进入和退出Monitor对象来实现方法和代码块同步。代码块同步使用monitorenter,monitorexit指令实现的
monitorenter 在编译后插入到同步代码块的开始位置,moniterexit 插入到方法结束处和异常处;任何一个对象都有一个monitor与之关联,当一个monitor被持有后将处于锁定状态
Java对象在内存中的存储结构主要由三部分组成:对象头、实例数据、填充数据
Java对象头
主要存储运行时的数据。synchronized 锁存在Java对象头里的,如果对象是数组,虚拟机用3个字宽存储对象头,否则用2个字宽。32位虚拟机中,1个字宽等于4个字节
Mark Word 默认存储对象的HashCode、分代年龄和锁表标记位
在运行时mark word会随锁标志位变化而变化,可能变成存储 偏向锁、重量级锁、轻量级锁这些情况
锁的升级和对比
无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,逐渐升级,锁升级不能降级的策略
因为synchronized之前是重量级锁,效率低下所以引入了偏向锁和轻量级锁(这就是在JDK1.6对synchronized做的优化)
为什么synchronized在JDK1.6以前效率低下?
与它的同步方式有关,阻塞或唤醒一个线程需要操作系统切换CPU的状态,synchronized 通过对象内部的监视器锁monitor来实现的,监视器锁本质依赖于底层操作系统的mutex lock 互斥锁来实现。操作系统实现线程之间的切换需要从用户态转换到和心态,成本非常高。这种依赖于操作系统mutex lock实现的锁我们称之为重量级锁
- 偏向锁
- 引入原因:大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了线程获得锁的代价更低
- 实现:一个线程获取到锁,在对象头和栈帧中锁记录里存储锁偏向的线程ID,以后再次进入不需要使用CAS操作加锁和解锁,简单测试对象头的mark word里是否存储指向当前线程的偏向锁。
- 测试成功,表示线程已经获得锁
- 测试失败,再测试mark word 中偏向锁的标识是否为1,没有设置则使用CAS竞争锁,设置了尝试使用CAS将对象头的偏向锁指向当前线程
- 撤销:等到竞争出现才释放锁,需要等待全局安全点(这个时间点上没有正在执行的字节码)
- 实现:一个线程获取到锁,在对象头和栈帧中锁记录里存储锁偏向的线程ID,以后再次进入不需要使用CAS操作加锁和解锁,简单测试对象头的mark word里是否存储指向当前线程的偏向锁。
- 关闭:通过JVM参数 -XX:-UseBiasedLocking = false;,程序默认进入轻量级锁状态
- 轻量级锁
- 加锁:执行同步块前,jvm在栈帧中创建用于存储锁记录的空间;将对象头的mark word复制到锁记录中;CAS修改mark word,成功就获得锁,失败说明其他线程竞争锁,当前线程使用自旋来获取锁
- 解锁:使用CAS将mark word 替换回到对象头(如果比较当前锁标志位为释放,就将其设置为锁定),如果成功表示没有竞争,失败表示存在竞争膨胀为重量级锁
- 获取:
- 关闭偏向锁功能
- 多个线程竞争偏向锁导致偏向锁升级
锁的升级过程:
锁状态的思路
锁对比
2.3 原子操作的实现原理
原子操作
不能被中断的一个或一系列操作
-
CAS:compare and swap
需要输入两个数值,一个旧值(期望操作之前的值),和一个新值,在操作期间比较旧值有没有发生变化,如果没有发生变化才交换成新值,发生了变化就不交换
-
内存顺序冲突:
由假共享引起的,假共享指多个CPU同时修改同一个缓存行的不同部分而引起其中一个CPU的操作无效
处理器实现原子操作
-
使用总线锁,当一个处理器在总线上输出lock#信号,其他处理器的请求将被阻塞,这个处理器就可以独占共享内存
-
缓存锁,缓存区域在lock期间被锁定,执行锁操作写回到内存时,处理器修改内部的缓存地址,利用缓存一致性操作保证原子性
-
不能使用缓存锁定的情况:
操作数据不能被缓存在处理器内部,处理器不支持缓存锁定
-
Java如何实现原子操作
锁和CAS:CAS操作利用了处理器提供的CMPXCHG指令实现的
-
使用CAS实现原子操作
三大问题:
- ABA问题:值变化A–B--A,CAS检查时会认为它没有发生变化,我们可以在变量前面追加版本号
- Java的atomic包里面提供了一个类AtomicStampedRefernce解决ABA问题,它的compareandswap方法检验版本号和值,都相等才更新
- 循环时间长:CAS长时间不成功,CPU开销很大,JVM支持处理器提供的pause指令效率会提高
- 只能保证一个共享变量的原子操作:将多个变量合成一个或者使用锁
- ABA问题:值变化A–B--A,CAS检查时会认为它没有发生变化,我们可以在变量前面追加版本号
-
使用锁
除了偏向锁,jvm实现锁的方式都用了CAS
第3章 Java内存模型
3个同步原语:synchronized、volatile、final
3.1 Java内存模型基础
并发编程两个关键问题
- 线程间如何通信?
- 线程间如何同步?
通信:交换信息
命令式编程中,线程通信的方式:
-
共享内存
通过读-写内存中的公共状态进行隐式通信
-
消息传递
发送消息进行显式通信
同步:控制不同线程间操作发生相对顺序的机制;共享内存里面同步显示进行,程序员必须指定哪一部分方法或代码需要互斥执行;消息传递消息发送必须在接收之前,隐式同步
Java采用共享内存模型
Java内存模型的抽象结构
堆内存:存放 实例域、静态域、数组元素,在线程之间共享
线程不共享,不存在内存可见性问题:局部变量、方法定义参数、异常处理器参数
抽象结构:共享变量存储在主内存,每个线程都有一个本地内存,存储了共享内存读写的副本,本地内存是JVM的抽象概念,不真实存在
A线程与B通信:
- A将本地内存中更新的值刷新到主内存
- B从主内存中读取A更新的值
从源代码到指令序列的重排序
指令重排序的原因:提高性能
-
编译器优化的重排序
编译器在不改变单线程程序语义的情况下,重新安排语句的执行顺序
-
指令级并行的重排序
如果不存在依赖性,处理器可以改变机器指令的执行顺序,将多条指令重叠执行
-
内存系统的重排序
处理器使用缓存和读写缓冲区,看起来乱序执行
源代码经过3种排序形成最终执行的指令序列,这些重排序可能导致内存可见性问题;1属性编译器重排序,23属于处理器重排序,JMM处理处理器重排序的规则是,要求Java编译器生成指令序列时,插入特定的内存屏障,禁止特定类型的处理器重排序
并发编程模型的分类
写缓冲区:临时保存向内存写入的数据,避免处理器停顿等待向内存写入数据产生的延迟,减少对内存总线的占用,但是仅对所在的处理器可见
比如A 读到了 B 线程放在内存的共享数据,其实这个而数据已经改变但是还没有刷新到内存,A读到了脏数据
屏障类型:
- LoadLoad: 指令之前的数据先于之后的数据
- StoreStore:指令之前的数据对其他处理器可见(刷新到内存),之后的指令才继续执行
- LoadStore:指令之前先装载,后序指令再存储刷新到内存
- StoreLoad:指令之前操作可见性,后序指令再装载;拥有其他三个屏障的效果,但是因为要把写缓冲区的数据全部刷新到内存,开销大
happens-before 操作
前一个操作顺序排在后一个操作之前,且对后一个操作可见,但是不一定前一个操作要先执行
3.2 重排序
编译器和处理器为了优化程序性能而对指令进行重新排序的一种手段
数据依赖性
两个操作访问同一个变量,有一个操作为写,此时这两个操作就具有数据依赖性;编译器和处理器不会改变单线程和单个处理器中的具有数据依赖性的操作执行顺序;多线程不考虑
满足as - if - serial:不管怎么重排序,单线程的执行结果都不能改变
重排序对多线程的影响
多线程中,对存在依赖关系的操作重排序,可能会改变程序的执行结果
3.3 顺序一致性
程序未正确同步就会出现数据竞争,定义如下:一个线程写,另一个线程读,且他们没有通过同步来排序
JVM保证:如果程序正确同步,就具有顺序一致性
顺序一致性内存模型:
提供了内存可见性保证,两大特性:1. 一个线程中的所有操作必须按照程序的顺序来执行;2. 所有线程都只能看到一个单一的操作执行顺序。
同步程序在顺序一致性模型中整体有序,未同步的整体无序,线程看到自己执行有序,且能看到整体的执行顺序(线程看到的结果是一样的)
但是在JMM中没有这个保证,实现方针:在不改变(正确同步的)程序执行结果的前提下,尽可能为编译器和处理器的优化打开方便之门
未同步程序的执行特性
jvm在堆上分配对象时,首先将内存空间进行清零,然后才会在上面分配对象
- 顺序一致性模型保证单线程内的操作会按程序的顺序执行,而JMM不保证单线程的操作会按程序的顺序执行
- 前者保证线程可以看到一致的操作执行顺序,而JMM不保证所有线程能看到一致的操作执行顺序
- JMM不保证对64位的long/double类型变量的写操作具有原子性
3.4 volatile 的内存语义
特性:
- 可见性:对一个volatile变量的读,总能看到任意线程对这个volatile变量最后的写入
- 原子性:对单个volatile变量的读/写具有原子性,类似volatile++复合操作不具有原子性
volatile写-读建立的happens-before关系
JDK5开始,volatile 的写-读可以实现线程通信,写时刷新到主存(向另一个要读的线程发出共享变量修改的消息),读时将本地内存置为无效从主存中读取(B接收了A修改变量的消息)
volatile的写读和锁的释放-获取具有相同的内存语义
volatile的写-读内存语义
写一个volatile变量时,JMM将线程对应的本地内存中的共享变量值刷新到主存
读一个volatile变量时,JMM把该线程对象的本地内存置为无效,接下来线程将从主存中读取共享变量
volatile 内存语义的实现
volatile重排序规则:
- 第二个操作是写,volatile 写之前的操作不会被排到写后面
- 第一个是读,volatile 读之后的操作不会被排到读之前
- 先写再读,不能重排序
基于保守策略的JMM内存屏障插入策略:
- StoreStore --> volatile写 --> StoreLoad
- LoadLoad --> volatile读 --> LoadStore
3.5 锁的内存语义
锁的释放获取建立的happens-before关系:A获得锁,执行,释放锁,B获得锁,执行,释放锁
锁的释放和获取内存语义
释放:本地内存共享变量刷新到主存(对B发出修改的消息)
获取锁:本地内存置为无效,从主存读取共享变量(接收这个修改的消息)
锁内存语义的实现
ReentrantLock 的实现依赖于AQS , 利用RenntantLock 的源代码分析:
lock 调用轨迹:
公平锁
ReentrantLock.lock() --> FairSync.lock() ---> AbstractQueuedSynchronizer.acquire(int arg) ---> ReentrantLock.Acquire(int acquires)
最后一步开始真正加锁,在获取锁的时候首先读这个volatile变量
解锁方法:
ReentrantLock.unlock() ---> AQS.release(int arg) ---> Sync.release(int release)
在释放锁的最后写volatile变量state
非公平锁:释放和公平锁一样,加锁使用AQS.compareAndSetState(int expect,int update)
为什么CAS同时具有volatile读和写的内存语义?
编译器不能对CAS与CAS前面和后面的任意内存操作重排序
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
在unsafe类中这是一个本地方法,最终底层调用为C++,程序会根据当前处理器类型决定是否添加lock前缀,多处理器就添加,否则省略。
Intel 手册对lock前缀的说明:
- 确保对内存的读-改-写操作原子执行,对总线或者缓存加锁
- 禁止该指令与之前或之后的读写指令重排序
- 写缓冲区所有数据刷新到主存
2、3所具有的内存屏障效果,足以同时实现volatile读和volatile写的内存语义
总结公平锁和非公平锁的内存语义
释放时,都要写一个volatile变量state
公平锁获取先读volatile变量
非公平锁获取,用CAS更新volatile变量,同时具有volatile的读写语义
concurrent 包的实现
4种通信方式
B线程读volatile变量 | B线程用CAS更新这个变量 | |
---|---|---|
A线程写volatile变量 | 1 | 2 |
A用CAS更新一个volatile变量 | 3 | 4 |
volatile变量的读写和CAS可以实现线程的通信,这些特性就形成了整个concurrent包得以实现的基石。
concurrent包通用的实现模式:
- 声明共享变量为volatile;
- 使用CAS的原子条件更新来实现线程之间的同步
- 使用volatile的读写和CAS具有的volatile读写语义来实现线程间的通信
AQS,非阻塞数据结构和原子变量类都是使用这种模式实现的,而concurrent包中的高层又是依赖这些基础类实现的
3.6 final 域的内存语义
相对于volatile和锁,对final域的读写更像是普通的变量访问
重排序规则
- 构造函数内,要先写入final域,再将其引用赋值给一个引用变量
- 初次读一个包含final域的对象的引用与随后初次读,这两个操作之间不能重排序
-
写final域的重排序规则:
- 禁止把final域的写重排序到构造函数外,就是说final域的写必须在构造函数执行期间完成;这样任何一个线程看到的都是初始化之后的final值,而普通域没有这个保障;
-
读final域的重排序规则:
-
针对处理器:初次对对象引用,先于初次读该对象包含的final域
针对编译器:在final域操作前插入一个loadload屏障
保证了:在读一个final域之前,一定会先读包含这个final域的对象的引用
-
-
final域为引用类型:
写的重排序规则:构造函数内,对final引用对象的成员域的写入,先于,在构造函数外把这个引用赋值给一个引用变量。
-
为什么final引用不能从构造函数内“溢出”
为了保证final域在初始化完成后才被其他线程所见,如果溢出(就是在构造函数内有了引用),final域可能没有被正确初始化
final语义在处理器中的实现
X86处理器为例,它不会对间接依赖关系重排序,所以不需要插入内存屏障
JSR-133增强final域的语义,增加了内存屏障,保证其不溢出
3.8 双重检查锁定与延迟初始化
在Java多线程中,有时候需要采用延迟初始化来降低初始化类和创建对象的开销,只有在使用这些对象时才进行初始化,这就是双重检查锁定的由来
示例代码也是单例模式的一种实现方式,volatile可以解决下述的问题,就是禁止重排序
出现的问题:
A线程判断对象为空进行加锁初始化,在初始化分为三个步骤:分配对象空间,指向内存地址,初始化对象;在单线程中这个顺序没有问题;多线程中,可能是A还没有初始化对象,B已经判断该实例不为空,就进行引用,其实此时实例还没初始化。
两种解决方案:
-
不允许先执行内存再初始化:volatile — 》针对实例字段
-
不允许其他线程看到1的过程 —》针对静态字段
基于类初始化解决:类的初始化阶段jvm会获取锁,这个锁可以同步多个线程对同一个类的初始化;即,添加static字段,这样类初始化时就会进行这个对象的初始化
一个类被立即初始化的情况:
T是一个类,一个T类型的实例被创建 T中声明的一个静态方法被调用 T中声明的一个静态字段被赋值 T中声明的一个静态字段被使用,且不是常量 T是一个顶级类
类初始化的阶段:
A获得class对象锁(B此时在condition中等待) —> A初始化对象(包括声明的静态字段) —>condition中等待线程,释放初始化锁 —> B获得初始化锁,发现已经初始化,进行释放
第4章 Java并发编程的基础
4.1 线程简介
现代操作系统调度的最小单元是线程,也叫轻量级进程,一个进程可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,能够访问共享的内存变量
使用多线程的原因
更多的处理器核心,要充分利用
将一致性不强的操作派发给其他线程处理,尽快响应用户请求的线程
Java提供了更好的多线程编程模型
线程优先级
现代操作系统基本采用时分的形式调度运行的线程,时间片用完需要进行切换,优先级决定了线程需要多/少分配一些处理器资源
Java中,通过一个int型成员变量priority来控制,1~10,线程构建时需要通过setPriority(int)方法修改,默认为5,频繁阻塞的线程设置高优先级,偏重计算的设置低优先级;设置优先级只是增大/减小了线程获取资源的概率,JVM不一定要这样执行
线程的状态
Daemon线程
Java虚拟机中不存在非Daemon线程,jvm就会退出(所以我们不能依赖finally来关闭守护线程,可能还没执行到这里jvm就退出了),我们可以通过Thread.setDaemon(true)将线程设置为守护线程
4.2 启动和终止线程
调用start方法启动,run()方法执行完毕线程终止
构造线程:由其parent线程来进行空间分配,同时还会为其分配一个唯一ID来标识
启动线程:调用start(),告诉jvm只要线程规划器空闲应立即启动调用
理解中断:
其他线程调用该线程的interrupt()方法对其中断,线程通过方法isInterrupt()来进行判断是否被中断
过期方法:suspend(线程不释放已经占有的资源),resume,stop(终结一个线程时不会保证资源的正常释放)
安全的终止线程:设立标志位
4.3 线程间通信(★)
4.3.1 volatile 和 synchronized 关键字
volatile 修饰字段(成员变量),多线程可以拥有主存共享变量的一个拷贝,但是看到的变量不一定是最新的,volatile保证了所有线程对变量访问的可见性
synchronized 可以修饰方法或者同步块,确保多个线程在同一时刻只有一个线程处于方法或者同步块中,保证了线程对变量访问的可见性和排他性
synchronized 的实现细节:
- 同步块的实现使用了monitorenter 和 moniterexit 指令
- 同步方法依靠方法修饰符上的ACC_SYNCHRONIZED来完成(两种方式本质都是对一个对象的监视器的获取,这个过程是排他的)
A线程获取到监视器锁(访问一个synchronized修饰的object),访问对象;此时B获取锁失败进入同步队列,状态变为BLOCK;当A释放锁,就唤醒同步队列中阻塞的线程,此时B重新获取
4.3.2 等待/通知机制
生产者/消费者机制
while(value != desire){
Thread.sleep(1000);
}
dosomething()
消费者线程不断循环检查变量是否满足预期,不满足就睡眠一段时间,防止过快的无效尝试,满足就退出完成消费者的工作
存在问题:
- 可能睡眠时错过了变化的条件
- 降低睡眠时间就会增加开销
所以出现了等待通知机制:
A调用对象O的wait方法进入等待状态,B调用O的notify 或者notify all 方法。A和B通过O的这些方法进行交互
使用细节:
- 使用wait\notify\nofityAll需要先对对象加锁
- 调用wait后,线程由RUNNABLE变为WAITING,将当前线程放入等待队列
- nofity、nofityall方法调用后,还需要B释放锁,A才可以从wait方法返回
- nofity方法将等待队列的一个等待线程移动到同步队列,notifyall时将等待队列的所有等待线程移动到同步队列
- 从wait方法返回的前提是获得了调用对象的锁
等待方(消费者):
synchronized(对象){
while(条件不满足){
对象.wait();
}
其他逻辑
}
通知方(生产者)
synchronized(对象){
改变条件
对象.notifyAll();
}
4.3.3 管道输入/输出流
主要用于线程之间的数据传输,传输的媒介为内存,必须要进行绑定,调用connect()方法
4.3.4 Thread.join()的使用
A执行了Thread.join():A等待Thread线程终止之后才从thread.join返回,可以设置超时;相当于A接受了thread线程的通知
4.3.5 ThreadLocal 的使用
线程变量,ThreadLocal 对象为键、任意对象为值的存储结构
故,一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值
4.4 线程应用实例
等待超时模式
设定超时时间T,那么当前时间now+T之后就会超时,返回默认值
添加超时后,即使方法执行时间过长,也不会“永久”阻塞调用者,会按照调用者的要求按时返回
第5章 Java中的锁(JUC)
介绍juc包中与锁相关的API和组件,使用方式和实现细节
5.1 Lock 接口
锁:控制多个线程访问共享资源的方式,JDK1.5后增加了lock显式的获取和释放锁,同时可以中断的获取和超时获取
Lock lock = new ReentrantLock();
lock.lock();
try{
} finally{
lock.unlock();
}
注意:
- 不要将获取锁的过程写在try中,因为如果在获取锁时发生了异常,异常抛出锁就会无故释放
- 在finally块中释放锁,保证获取到锁后最终能够释放
public interface Lock {
//获取锁
void lock();
//中断的获取锁,会响应中断
void lockInterruptibly() throws InterruptedException;
//尝试非阻塞获取锁
boolean tryLock();
//超时获取锁
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
//释放锁
void unlock();
//获取等待通知的组件,当前线程获得了锁才能调用组件的wait方法,调用后当前线程就会释放锁
Condition newCondition();
}
5.2 队列同步器(AQS)
AQS:用来构建锁或者其他同步组件的基础框架,基于模板方法模式,使用一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作
使用方式:继承,并实现它的抽象方法来管理同步状态
private volatile int state;
protected final int getState() {return state;}
protected final void setState(int newState) {state = newState;}
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
这三个方法能够保证状态的改变是安全的
同步器是实现锁的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义;锁是面向使用者的,同步器面向的是锁的使用者
模板方法模式:定义一个模板结构,将具体内容延迟到子类去实现
我们通常使用静态内部类继承AQS
队列同步器的实现分析
-
同步队列
同步器依赖同步队列(FIFO的双向队列)来完成同步状态的管理
A 如果获取同步状态失败,同步器将A及其等待状态等信息构造成为一个Node加入同步队列,A变为BLOCK,当同步状态释放时,将同步队列的首节点中线程唤醒,其再次尝试获取同步状态
Node保存了:
static final class Node { static final Node SHARED = new Node(); static final Node EXCLUSIVE = null; //等待状态 static final int CANCELLED = 1; static final int SIGNAL = -1;//后继节点线程处于等待状态 static final int CONDITION = -2;//节点在等待队列中,等待在condition上,当线程对condition调用了signal方法就会从等待队列加入同步队列中 static final int PROPAGATE = -3; //等待状态 volatile int waitStatus; //前驱节点 volatile Node prev; //后继节点 volatile Node next; //获取同步状态的线程 volatile Thread thread; //等待队列中的后继节点 Node nextWaiter; }
节点是构成同步队列的基础,没有成功获取同步状态的线程将会成为节点加入该队列的尾部,同步器包含两个引用:head tail,加入队列的过程要保证安全,因此同步器提供了一个CAS设置尾节点的方法需要传递当前线程“认为的”head 和 tail,设置成功后,当前节点才正式与之前的尾节点建立关联
private final boolean compareAndSetTail(Node expect, Node update) {
//这是一个本地方法
return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}
设置首节点不需要CAS原子保证,因为将head = head.next就可以
独占式同步状态获取与释放
-
获取:通过同步器的acquire(int arg) 方法获取同步状态
-
获取失败进入同步队列不会对中断进行响应,即不会从同步队列中移出
以“死循环”的方式获得同步状态,获取到就可以从这个自旋过程中退出
-
头节点释放同步状态(调用release方法)唤醒后继节点,被唤醒的节点检查自己的前驱节点是否为头节点,是就尝试获取同步状态,为了符合FIFO规则
共享式同步状态获取与释放
与独占式主要区别:同一时刻是否有多个线程同时获取到同步状态
- 调用同步器的acquireShared(int arg)方法获取同步状态, —> tryAcquireShared(int arg)
- releaseShared()释放资源,唤醒后序处于等待的节点
- 能够支持多个线程同时访问的并发组件 Semaphore,和独占式的区别:必须确保同步状态线程安全释放,一般通过循环和CAS来保证的
独占式超时获取同步状态
获取锁失败判断是否超时,如果超时进入快速的自旋过程,否则继续等待,这个时间小于等于1000ns时就不会等待,进入自旋
5.3 重入锁
ReentrantLock,该锁能够支持一个线程对资源重复加锁,在调用lock方法时,已经获取到锁的线程,能够再次调用lock方法获取锁而不被阻塞,支持公平锁(排队打饭)和非公平锁
实现重进入
重进入:已经获取到锁的线程之后能够再次获取到该锁而不被阻塞
- 再次获取该锁:锁识别获取所得线程是否为占据当前锁的线程,如果是就成功获取
- 锁的释放:线程n次获取了锁,获取计数(这个计数就是同步状态值)自增,释放要求进行计数自减,计数等于0说明成功释放
公平锁与非公平锁的区别
公平锁:锁的获取顺序等于请求的绝对时间顺序,FIFO
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//如果当前节点没有前驱节点且CAS设置同步状态成功,就获取锁,返回true
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
公平锁里面的tryAcquire方法,增加了对是否有前驱节点的判断,如果有前驱节点,那么就要等待前驱节点释放锁才能继续获取
public ReentrantLock(boolean fair) {
//传入true就是公平锁,默认是非公平锁
sync = fair ? new FairSync() : new NonfairSync();
}
public ReentrantLock() {
sync = new NonfairSync();
}
非公平锁可能出现线程连续获得锁的情况,非公平锁本来就是一个线程请求锁只要获得同步状态就获得了锁,而刚刚释放锁的线程获取同步状态的几率更大,使得其他线程只能在队列中等待
总结:
- 公平锁保证锁得获取按照FIFO原则,而代价是进行大量的线程切换
- 非公平锁虽然可能造成线程“饥饿”,但极少的线程切换,保证了更大的吞吐量
5.4 读写锁
读写锁不是排他锁,在同一个时刻可以允许多个读线程访问,JDK1.5之前使用等待通知机制,写操作之间依赖synchronized关键字。改用读写锁,写锁获取到时,非当前操作线程的读写被阻塞;读写锁比排他锁提供更好的并发性和吞吐量,JUC的实现是ReentrantReadWriteLock
HashMap的存取是不安全的,但是我们可以为map.put加写锁,map.get加读锁
5.4.2读写锁的实现分析
读写状态的设计
读写状态就是其同步器的同步状态,同步状态表示锁被一个线程重复获取的次数
这个变量需要维护多个读线程和一个写线程的状态,如果要在一个整型变量上维护多种状态,就一定需要“按位切割”使用这个变量,于是读写锁这个变量高16位表示读,低16位表示写
假设当前同步状态为S,写状态等于 S&0x0000FFFF,读状态等于 S>>>16(无符号补0右移16位),S不为0,写为0,那么必然读不为0
写锁的获取与释放
写锁是一个支持重进入的排他锁
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
// 存在读锁或者当前获取线程不是已经获取写锁的线程
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires);
return true;
}
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
注释部分增加了一个读锁是否存在的判断,如果读锁存在就不会成功获取到写锁,因为读写锁要保证写锁的操作对读锁可见,如果允许读锁被获取的情况对写锁获取,那么正在运行的其他线程就无法感知写线程的操作;因此,只有等待其他线程都释放了锁,写锁才能被当前线程获取,写锁一旦获取其他线程读写均阻塞
释放过程同ReentrantLock
读锁的获取与释放
读锁是一个支持重进入的共享锁,没有其他写线程访问时,读锁总会被成功获取;如果当前线程获取读锁是写锁已被其他线程获取则进入等待状态;如果当前线程已经获取读锁,则获取读锁次数加1
锁降级
写锁降级称为读锁,把持住当前的写锁,获取到读锁,再释放写锁,ReentrantReadWriteLock不支持锁降级,为了保证数据的可见性
5.5 LockSupport 工具
这是一个工具类,可以完成阻塞或唤醒一个线程
public static void park() {
//阻塞当前线程
UNSAFE.park(false, 0L);
}
public static void parkNanos(long nanos) {
//阻塞当前线程,最长不超过nanos秒
if (nanos > 0)
UNSAFE.park(false, nanos);
}
public static void parkUntil(long deadline) {
//阻塞当前线程,直到dealline
UNSAFE.park(true, deadline);
}
public static void unpark(Thread thread) {
//唤醒当前线程
if (thread != null)
UNSAFE.unpark(thread);
}
5.6 Condition 接口
任何一个Java对象拥有一组监视器方法(定义在java.lang.Object中),wait, wait(long timeout) , notify() , notifyAll(); 配合synchronized使用,实现等待通知机制。
Condition 接口配合Lock使用也可以实现等待/通知机制,使用前需要获取锁,由Lock对象创建出来的
public class ConditionUseCase {
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void conditonWait() throws InterruptedException{
lock.lock();
try{
//当前线程释放锁,在此等待
condition.await();
}finally {
lock.unlock();
}
}
public void conditionSignal() throws InterruptedException{
lock.lock();
try{
//通知当前线程,当前线程才从await返回
condition.signal();
}finally {
lock.unlock();
}
}
}
5.6.2 condition 的实现分析
ConditionObject 类是AQS 的内部类,每个condition对象包含一个队列
等待队列
一个condition对象包含一个等待队列,拥有首节点firstWaiter 和 尾节点 lastWaiter ,对象调用await()方法,就将当前线程构造为节点,将节点从尾部加入等待队列,condition拥有尾节点的引用,这个过程没有使用CAS来保证,是因为调用await() 方法的线程就是获取了锁的,所以是安全的。
同步器拥有一个同步队列和多个等待队列,object的监视器模型一个对象拥有一个同步队列和等待队列
等待
调用condition 的 await() 方法,当前线程释放锁并进入等待队列,同时线程变为等待状态;相当于同步队列的首节点移动到condition的等待队列中,唤醒同步队列的后继节点;
等待队列节点被唤醒,开始获取同步状态,如果不是通过其他线程调用signal方法唤醒,而是对等待线程的中断,会抛出 InterruptException.
通知
调用condition的signal方法,唤醒等待队列中等待时间最长的节点(首节点),唤醒之前将节点移入到同步队列中
public final void signal() {
//当前线程必须是获取了锁的线程
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//获取等待队列的首节点
Node first = firstWaiter;
if (first != null)
//将其移动到同步队列中
doSignal(first);
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
使用enq方法将等待队列的头结点线程安全的移动到同步队列中
被唤醒后的线程调用同步器的acquireQueued() 方法加入到获取同步状态的竞争中
signalAll 就是将所有等待线程节点全部移动到同步队列中
第6章 Java并发容器和框架
6.1 ConcurrentHashMap 的实现原理与使用
ConcurrentHashMap是线程安全且高效的 hashmap
为什么使用ConcurrentHashMap
-
并发编程中,使用HashMap 执行put 可能导致死循环,因为多线程会导致HashMap 的 Entry 链表形成环形数据结构,一旦形成环形结构,Entry的next节点将永不为空,就会一致死循环获取
-
HashTable 使用synchronized保证线程安全,但是效率低下,线程1使用put/get时,其他线程只能阻塞或轮询
-
ConcurrentHashMap 使用锁分段技术提升并发访问率
ConcurrentHashMap 的结构
ConcurrentHashMap 由Segment 和 HashEntry 数组结构组成;
- Segment 是一种可重入锁,扮演锁的角色
static class Segment<K,V> extends ReentrantLock implements Serializable {
private static final long serialVersionUID = 2249069246763182397L;
final float loadFactor;
Segment(float lf) { this.loadFactor = lf; }
}
- HashEntry用于存储键值对数据
protected HashEntry(ExtendedType key, int value, int hash, HashEntry next)
{
this.key = key;
this.value = value;
this.hash = hash;
this.next = next;
}
ConcurrentHashMap 包含一个 Segment 数组,一个 Segment 包含一个 HashEntry 数组
ConcurrentHashMap 的初始化
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
private static final float LOAD_FACTOR = 0.75f;
private static final int DEFAULT_CAPACITY = 16;
//初始化的3个参数
public ConcurrentHashMap(int initialCapacity,//初始容量
//加载因子(0,75),并发用户数(指最多允许多少个线程并发修改,16)
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (initialCapacity < concurrencyLevel) // Use at least as many bins
initialCapacity = concurrencyLevel; // as estimated threads
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
int cap = (size >= (long)MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY : tableSizeFor((int)size);
this.sizeCtl = cap;
}
Segments 数组的长度ssize依赖 concurrentcyLevel计算得出,为了保证能通过按位与来定位segments数组的索引,必须保证其数组长度是2的N次方,所以假设concurrentcyLevel为14,15或16,ssize都会等于16,容器里的锁的个数就是16
int sshift = 0;
int ssize = 1;
while (ssize < DEFAULT_CONCURRENCY_LEVEL) {
++sshift;//ssize从1向左移位的次数,默认为4,因为level默认为16
ssize <<= 1;
}
初始化segmentShift、segmentMask
//定位参与散列运算的位数,默认为28,使用32是因为ConcurrentHashMap 里面的 hash 方法输出最多32位
int segmentShift = 32 - sshift;
//散列运算的掩码,每位都为1,保证尽可能地散列开
int segmentMask = ssize - 1;
初始化每个segment
Segment<K,V>[] segments = (Segment<K,V>[])
new Segment<?,?>[DEFAULT_CONCURRENCY_LEVEL];
for (int i = 0; i < segments.length; ++i)
segments[i] = new Segment<K,V>(LOAD_FACTOR);
定位segment
我们在插入和获取数据时需要先获得segment锁,那就要通过散列算法定位到segment
ConcurrentHashMap的操作
-
get
public V get(Object key) { Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek; //获得哈希值 int h = spread(key.hashCode()); if ((tab = table) != null && (n = tab.length) > 0 && (e = tabAt(tab, (n - 1) & h)) != null) { //判断哈希值是否相等 if ((eh = e.hash) == h) { //判断key是否相等 if ((ek = e.key) == key || (ek != null && key.equals(ek))) return e.val; } else if (eh < 0) return (p = e.find(h, key)) != null ? p.val : null; while ((e = e.next) != null) { if (e.hash == h && ((ek = e.key) == key || (ek != null && key.equals(ek)))) return e.val; } } return null; }
static final int spread(int h) { return (h ^ (h >>> 16)) & HASH_BITS; }
-
put
public V put(K key, V value) {return putVal(key, value, false);} final V putVal(K key, V value, boolean onlyIfAbsent) { if (key == null || value == null) throw new NullPointerException(); int hash = spread(key.hashCode()); int binCount = 0; for (Node<K,V>[] tab = table;;) { 。。。 else { V oldVal = null; //加锁 synchronized (f) { 。。。 } } } addCount(1L, binCount); return null; }
- 操作共享变量是加锁,上述get是不加锁的,先定位到segment, 然后在Segment 里面进行插入操作,两个步骤:
- 判断是否需要对segment里面的hash entry数组进行扩容
- 定位添加元素的位置,然后放在hash entry数组里面
- 如何扩容:
- 创建一个容量是原来容量两倍的数组,将原来的元素进行再散列后插入到新的数组里面,concurrenthashmap 只会对某个segment扩容,更加高效
- 操作共享变量是加锁,上述get是不加锁的,先定位到segment, 然后在Segment 里面进行插入操作,两个步骤:
-
size()
获取每个segment里面元素的大小之后求和,但是有可能累加时count发生了变化,最安全的做法是锁住segment的所有方法,但是低效;所以concurrenthashmap先尝试两次通过不锁住segment的方法来计算count,如果发生了变化再采用锁的方式
6.2 ConcurrentLinkedQueue
线程安全的队列,阻塞算法(出队和入对一把锁,或者各自持有一把锁);非阻塞:使用CAS
Node 使用volatile修饰
private transient volatile Node<E> head;
private transient volatile Node<E> tail;
入队列
将节点添加到队列的尾部
-
将入队节点设置为尾节点的下一个节点:tail.next = 入队节点
-
更新 tail 节点:
- tail.next != null —> tail = tail.next
- tail.next == null --> tail.next = 入队节点
tail不总是尾节点
并发编程中我们使用CAS入队:
public boolean offer(E e) {
checkNotNull(e);
//入队节点
final Node<E> newNode = new Node<E>(e);
for (Node<E> t = tail, p = t;;) {
Node<E> q = p.next;
//如果尾节点的下一个节点为null
if (q == null) {
// p 表示尾节点
//将入队节点设置为尾节点
if (p.casNext(null, newNode)) {
// Successful CAS is the linearization point
// for e to become an element of this queue,
// and for newNode to become "live".
if (p != t) // hop two nodes at a time
casTail(t, newNode); // Failure is OK.
return true;
}
// Lost CAS race to another thread; re-read next
}
else if (p == q)
// We have fallen off list. If tail is unchanged, it
// will also be off-list, in which case we need to
// jump to head, from which all live nodes are always
// reachable. Else the new tail is a better bet.
p = (t != (t = tail)) ? t : head;
else
// Check for tail updates after two hops.
p = (p != t && t != (t = tail)) ? t : q;
}
}
两件事情:定位尾节点,使用CAS算法将入队节点设置成尾节点的next节点
定位尾节点
tail节点来定位尾节点,可能是tail,也可能是tail.next
设置入队节点为尾节点
boolean casNext(Node<E> cmp, Node<E> val) {
return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
}
出队列
public E poll() {
restartFromHead:
for (;;) {
for (Node<E> h = head, p = h, q;;) {
E item = p.item;
//如果元素不为空就出队列
if (item != null && p.casItem(item, null)) {
// Successful CAS is the linearization point
// for item to be removed from this queue.
if (p != h) // hop two nodes at a time
updateHead(h, ((q = p.next) != null) ? q : p);
return item;
}
else if ((q = p.next) == null) {
updateHead(h, p);
return null;
}
else if (p == q)
continue restartFromHead;
else
p = q;
}
}
}
head里面有元素直接弹出,没有元素出队操作更新头节点,说明另外一个线程取走了,需要重新获取
6.3 Java中的阻塞队列
BlockingQueue 是一个支持两个附加操作的队列,支持阻塞的插入(队列满时阻塞添加)和移除(队列空时阻塞取)方法,常用于生产者和消费者的场景
阻塞队列的实现原理
如果队列为空,消费者会一直等待,当生产者生产元素,消费者是如何知道的呢?
-
使用通知模式
//ArrayBlockingQueue 的源码,使用condition来实现 private final Condition notEmpty; private final Condition notFull; public void put(E e) throws InterruptedException { checkNotNull(e); final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { while (count == items.length) //等待 notFull.await(); enqueue(e); } finally { lock.unlock(); } } public E take() throws InterruptedException { final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { while (count == 0) notEmpty.await(); return dequeue(); } finally { lock.unlock(); } } private void enqueue(E x) { // assert lock.getHoldCount() == 1; // assert items[putIndex] == null; final Object[] items = this.items; items[putIndex] = x; if (++putIndex == items.length) putIndex = 0; count++; //唤醒 notEmpty.signal(); }
//ConditionObject 继承 condition里实现的await方法 public final void await() throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); Node node = addConditionWaiter(); long savedState = fullyRelease(node); int interruptMode = 0; while (!isOnSyncQueue(node)) { //阻塞生产者的实现方式 LockSupport.park(this); if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break; } if (acquireQueued(node, savedState) && interruptMode != THROW_IE) interruptMode = REINTERRUPT; if (node.nextWaiter != null) // clean up if cancelled unlinkCancelledWaiters(); if (interruptMode != 0) reportInterruptAfterWait(interruptMode); }
阻塞生产者的实现方式LockSupport.park(this);
继续进入源码
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);//先保存一下要阻塞的线程
UNSAFE.park(false, 0L);//阻塞当前线程,native方法
setBlocker(t, null);
}
6.4 Fork/Join 框架
什么是Fork/Join 框架
Fork/Join 框架是JDK1.7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干小任务,最终汇总每个小任务结果后得到大任务结果的框架。
Fork: 大任务切分为若干子任务并行的执行
Join:合并这些子任务的执行结果,最终得到大任务的执行结果
工作窃取算法
工作窃取算法是指:某个线程从其他队列里窃取任务来执行
为了减少窃取任务线程与被窃取任务线程之间的竞争,通常使用双端队列,被窃取的线程从队列头拿任务,窃取的线程从队列尾拿任务
优点:
充分利用线程进行并行计算,减少了线程间的竞争
缺点:
某些情况存在竞争,比如双端队列只有一个任务,同时创建多个线程和双端队列消耗资源
Fork/Join 框架的设计
-
分割任务:fork类分割任务直到任务足够小
-
执行任务并合并结果:子任务放在双端队列中,启动几个线程分别从该队列里面获取任务执行;执行完的结果放在一个队列,启动一个线程从队列里面拿数据进行合并
- ForkJoinTask:创建一个ForkJoin任务,他提供了join 和 fork 操作机制,我们只需要继承其子类就可以(RecursiveAction(没有返回结果)、RecursiveTask(有返回结果))
- ForkJoinPool:ForkJoinTask需要ForkJoinPool来执行
使用:
//计算1+2+3+4
public class CountTask extends RecursiveTask<Integer> {
private static final int THRESHOLD = 2;//每个子任务最多执行两个数相加
private int start;
private int end;
public CountTask(int start,int end){
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
int sum = 0;
//任务足够小就执行
boolean canCompute = (end - start) <= THRESHOLD;
if(canCompute){
for(int i = start;i <= end;i++){
sum+=i;
}
} else {
//分解大任务
int middle = (start+end)/2;
CountTask leftTask = new CountTask(start,middle);
CountTask rightTask = new CountTask(middle+1,end);
//执行子任务
leftTask.fork();
rightTask.fork();
//合并结果
int leftRes = leftTask.join();
int rightRes = rightTask.join();
sum = leftRes+rightRes;
}
return sum;
}
public static void main(String[] args) {
ForkJoinPool forkJoinPool = new ForkJoinPool();
CountTask task = new CountTask(1,4);
Future<Integer> result = forkJoinPool.submit(task);
try{
System.out.println(result.get());
} catch (InterruptedException e){
} catch (ExecutionException e){
}
}
}
异常处理
//检查任务是否已经抛出异常或已经被取消了
public final boolean isCompletedAbnormally() {
return status < NORMAL;
}
Fork/Join框架的实现原理
-
ForkJoinPool 由 ForkJoinTask(将存放的程序提交) 和 ForkJoinWorkerThread(执行) 数组组成
-
ForkJoinTask 的fork方法调用ForkJoinWorkerThread的push方法
public final ForkJoinTask<V> fork() { Thread t; if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) ((ForkJoinWorkerThread)t).workQueue.push(this); else ForkJoinPool.common.externalPush(this); return this; }
-
push 方法将任务放在ForkJoinTask数组里面,然后调用 ForkJoinPool 的signalWork方法唤醒或创建一个线程来执行任务
final void push(ForkJoinTask<?> task) { ForkJoinTask<?>[] a; ForkJoinPool p; int b = base, s = top, n; if ((a = array) != null) { // ignore if queue removed int m = a.length - 1; // fenced write for task visibility U.putOrderedObject(a, ((m & s) << ASHIFT) + ABASE, task); U.putOrderedInt(this, QTOP, s + 1); if ((n = s - b) <= 1) { if ((p = pool) != null) //唤醒或创建一个线程来执行任务 p.signalWork(p.workQueues, this); } else if (n >= m) growArray(); } }
-
join 方法:阻塞当前线程并等待获取结果
调用 doJoin方法
public final V join() { int s; //doJoin()得到当前任务的状态来判断返回什么结果 if ((s = doJoin() & DONE_MASK) != NORMAL) reportException(s); return getRawResult(); }
任务状态:
static final int NORMAL = 0xf0000000; // must be negative 已完成 static final int CANCELLED = 0xc0000000; // must be < NORMAL 被取消 static final int EXCEPTIONAL = 0x80000000; // must be < CANCELLED 出现异常 static final int SIGNAL = 0x00010000; // must be >= 1 << 16 信号
分析doJoin方法
private int doJoin() { int s; Thread t; ForkJoinWorkerThread wt; ForkJoinPool.WorkQueue w; //如果任务执行完返回任务状态 return (s = status) < 0 ? s : //没有执行完任务 ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) ? (w = (wt = (ForkJoinWorkerThread)t).workQueue). //取出任务 tryUnpush(this) && (s = doExec()) < 0 ? s : wt.pool.awaitJoin(w, this, 0L) : externalAwaitDone(); }
-
第7章 Java中的13个原子类
四类:原子更新基本类型、原子更新数组、原子更新引用、原子更新属性(字段)
7.1 原子更新基本类型
AtomicBoolean、AtomicLong、AtomicInteger提供的方法几乎一致,以AtomicInteger为例进行分析
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
//CAS操作
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
unsafe的源码只提供了三种基本类型的原子CAS操作
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
AtomicBoolean 是先转成int再实现的,其他基本类型类推
7.2 原子更新数组
三类:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
public final int addAndGet(int i, int delta) {
return getAndAdd(i, delta) + delta;
}
//以原子方式将输入值和索引i的元素相加
public final int getAndAdd(int i, int delta) {
return unsafe.getAndAddInt(array, checkedByteOffset(i), delta);
}
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
/* @param i the index
* @param newValue the new value
* @return the previous value
*/
public final int getAndSet(int i, int newValue) {
//arr[i] = newValue
return unsafe.getAndSetInt(array, checkedByteOffset(i), newValue);
}
public AtomicIntegerArray(int[] array) {
// 构造方法传递数组,会将其进行拷贝,当原子数组类对数组进行修改时,不会影响传入的数组
this.array = array.clone();
}
7.3 原子更新引用类型
三类:AtomicReference、AtomicReferenceFieldUpdater、AtomicMarkableReference
AtomicReference为例,如何使用呢?
//设置对象,(要更改的旧对象)
public final void set(V newValue) {
value = newValue;
}
//利用CAS更新为新对象
public final boolean compareAndSet(V expect, V update) {
return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}
7.4 原子更新字段类
3类:AtomicLongFieldUpdater、AtomicIntegerFieldUpdater、AtomicStampedFieldUpdater(带有版本号的引用类型,可以解决ABA问题)
//使用静态方法创建一个更新器,设置想要更新的类和属性
//String fieldName 要更新的属性必须使用 volatile 修饰
public static <U> AtomicLongFieldUpdater<U> newUpdater(Class<U> tclass,String fieldName){..}
第8章 Java中的并发工具类
8.1 等待多线程完成的CountDownLatch
CountDownLatch允许一个或多个线程等待其他线程完成操作
使用方法:
//1.构造器里面传入倒计时count,必须大于0
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
//2.调用countDown()方法进行减一
public void countDown() {
sync.releaseShared(1);
}
//带超时的等待方法
public boolean await(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
示例
import java.util.concurrent.CountDownLatch;
/**
* 计数器
*/
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException{
//总数是6的倒计时,必须要执行任务的时候再使用
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 0; i <= 6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"Go Out");
countDownLatch.countDown();//-1
},String.valueOf(i)).start();
}
countDownLatch.await();//等待计数器归零再向下执行
System.out.println("close door");
}
}
8.2 同步屏障CyclicBarrier
作用: 让所有线程都等待完成后才会继续下一步行动
使用场景: 可以用于多线程计算数据,最后合并计算结果的场景
//构造方法中传入屏幕拦截的线程数量
public CyclicBarrier(int parties) {
this(parties, null);
}
//每个线程调用wait方法告诉CyclicBarrier已经到达了屏障
如果我们设置拦截数量为2,传入了一个子线程和main线程,那么最后输出结果可能先执行子线程也可能先执行主线程;传入了两个线程的话,就最后执行主线程
CyclicBarrier 与 CountDownLatch 区别
CountDownLatch 是一次性的,CyclicBarrier 是可循环利用的
CountDownLatch 参与的线程的职责是不一样的,有的在倒计时,有的在等待倒计时结束。CyclicBarrier 参与的线程职责是一样的。
CyclicBarrier 的源码实现和 CountDownLatch 大相径庭,CountDownLatch 基于 AQS 的共享模式的使用,而 CyclicBarrier 基于 Condition 来实现的。因为 CyclicBarrier 的源码相对来说简单许多,读者只要熟悉了前面关于 Condition 的分析,那么这里的源码是毫无压力的,就是几个特殊概念罢了。
8.3 控制并发线程数的Semaphore
是什么?
Semaphore管理一系列许可。每个acquire方法阻塞,直到有一个许可证可以获得然后拿走一个许可证;每个release方法增加一个许可,这可能会释放一个阻塞的acquire方法。然而,其实并没有实际的许可这个对象,Semaphore只是维持了一个可获得许可证的数量。
使用场景
用于那些资源有明确访问数量限制的场景,常用于限流 。
- 比如:数据库连接池,同时进行连接的线程有数量限制,连接不能超过一定的数量,当连接达到了限制数量后,后面的线程只能排队等前面的线程释放了数据库连接才能获得数据库连接。
- 比如:停车场场景,车位数量有限,同时只能容纳多少台车,车位满了之后只有等里面的车离开停车场外面的车才可以进入。
package juc.add;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public class SemaphoreDemo {
public static void main(String[] args) {
// 线程数量:停车位 限流
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i <= 6; i++) {
new Thread(()->{
//acquire() 得到
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"抢到车位");
//停一会儿
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName()+"离开车位");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//release() 释放
semaphore.release();
}
},String.valueOf(i)).start();
}
}
}
原理:
semaphore.acquire():获得,假设如果已经满了,就等待被释放为止
semaphore.release():释放,会将当前的的信号量释放+1,然后唤醒等待的线程
作用:多个共享资源互斥的使用,并发限量,控制最大的线程数
构造方法
//创建具有给定的许可数和非公平的公平设置的 Semaphore。
Semaphore(int permits)
//创建具有给定的许可数和给定的公平设置的 Semaphore。
Semaphore(int permits, boolean fair)
重要方法
1、acquire(int permits)
从此信号量获取给定数目的许可,在提供这些许可前一直将线程阻塞,或者线程已被中断。就好比是一个学生占两个窗口。这同时也对应了相应的release方法。
2、release(int permits)
释放给定数目的许可,将其返回到信号量。这个是对应于上面的方法,一个学生占几个窗口完事之后还要释放多少
3、availablePermits()
返回此信号量中当前可用的许可数。也就是返回当前还有多少个窗口可用。
4、reducePermits(int reduction)
根据指定的缩减量减小可用许可的数目。
5、hasQueuedThreads()
查询是否有线程正在等待获取资源。
6、getQueueLength()
返回正在等待获取的线程的估计数目。该值仅是估计的数字。
7、tryAcquire(int permits, long timeout, TimeUnit unit)
如果在给定的等待时间内此信号量有可用的所有许可,并且当前线程未被中断,则从此信号量获取给定数目的许可。
8、acquireUninterruptibly(int permits)
从此信号量获取给定数目的许可,在提供这些许可前一直将线程阻塞。
第9章 Java中的线程池
-
线程池:3大方法、7大参数、4种拒绝策略
池化技术
程序的运行,本质:占用系统的资源!优化资源的使用!
线程池、连接池、内存池、对象池。。。。。创建和销毁十分浪费资源
池化技术:
事先准备好一些资源,有人要用就从我这里拿,用完之后还给我
- 默认大小:
- max:
线程池的好处:
- 降低资源的消耗:创建和销毁的损耗
- 提高响应的速度:不需要等待线程创建
- 方便管理:线程不能无线创建
线程复用、可以控制最大并发数、管理线程
9.1 三大方法
线程三大方法
ExecutorService threadPool = Executors.newSingleThreadExecutor();//单个线程
Executors.newFixedThreadPool(5);//创建一个固定的线程池的大小
Executors.newCachedThreadPool();//可伸缩的,遇强则强,遇弱则弱
package juc.poll;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Executors 工具类,3大方法
*
*/
public class Demo01 {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newSingleThreadExecutor();//单个线程
//Executors.newFixedThreadPool(5);//创建一个固定的线程池的大小
//Executors.newCachedThreadPool();//可伸缩的,遇强则强,遇弱则弱
try{
for (int i = 0; i < 10; i++) {
//使用了线程池之后,使用线程池来创建线程
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+"ok");
});
}
} catch (Exception e){
e.printStackTrace();
} finally {
//线程池用完,程序结束,关闭线程池
threadPool.shutdown();
}
}
}
9.2 七大参数
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
//本质:ThreadPoolExecutor
public ThreadPoolExecutor(int corePoolSize, //核心线程池大小
int maximumPoolSize, //最大核心线程池大小
long keepAliveTime, //超时了没有人调用就会释放
TimeUnit unit, //超时单位
BlockingQueue<Runnable> workQueue, //阻塞队列
ThreadFactory threadFactory, //线程工厂,创建线程的,一般不用动
RejectedExecutionHandler handler ) {//拒绝策略
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
int corePoolSize, //核心线程池大小
int maximumPoolSize, //最大核心线程池大小
long keepAliveTime, //超时了没有人调用就会释放
TimeUnit unit, //超时单位
BlockingQueue<Runnable> workQueue, //阻塞队列
ThreadFactory threadFactory, //线程工厂,创建线程的,一般不用动
RejectedExecutionHandler handler //拒绝策略
(图片来自狂神的juc课程)
原本只开核心线程办理业务,直到候客区满了才会触发max线程打开进行业务办理
9.3四种拒绝策略
- AbortPolicy:直接抛出异常
- CallerRunsPolicy:只用调用者所在的线程来处理
- DiscardOldestPolicy:丢弃最早一个任务,执行该任务
- DiscardPolicy:不处理,丢弃
/**
* Executors 工具类,4种拒绝策略
* new ThreadPoolExecutor.AbortPolicy 银行满了,候客区也满了,但是还有人进来,就不处理这个人的了,并且抛出异常
* new ThreadPoolExecutor.CallerRunsPolicy() //哪儿来的去哪里
* new ThreadPoolExecutor.DiscardPolicy() //队列满了,丢掉任务不会抛出异常
* new ThreadPoolExecutor.DiscardOldestPolicy() //队列满了,尝试和最早的竞争,竞争失败就丢掉任务,不会抛出异常
*/
小结和拓展
池的最大大小如何去设置?
了解:IO密集型和CPU密集型(调优)
//最大线程到底该如何定义
//1. cpu 密集型 几核就定义为几,保证CPU效率最大
//2. IO 密集型 ,> 判断你程序中十分耗IO的线程
// 程序 15个大型任务, IO十分占用资源
9.4 手动创建一个线程池
package juc.poll;
import java.util.concurrent.*;
/**
* Executors 工具类
* new ThreadPoolExecutor.AbortPolicy 银行满了,候客区也满了,但是还有人进来,就不处理这个人的了,并且抛出异常
* new ThreadPoolExecutor.CallerRunsPolicy() //哪儿来的去哪里
* new ThreadPoolExecutor.DiscardPolicy() //队列满了,丢掉任务不会抛出异常
* new ThreadPoolExecutor.DiscardOldestPolicy() //队列满了,尝试和最早的竞争,竞争失败就丢掉任务,不会抛出异常
*/
public class Demo01 {
public static void main(String[] args) {
//自定义线程池
//最大线程到底该如何定义
//1. cpu 密集型 几核就定义为几,保证CPU效率最大
//2. IO 密集型 ,> 判断你程序中十分耗IO的线程
// 程序 15个大型任务, IO十分占用资源
//获取电脑的核数
System.out.println(Runtime.getRuntime().availableProcessors());//12
ExecutorService threadPool = new ThreadPoolExecutor(
2,
5,//替换为 Runtime.getRuntime().availableProcessors()就是CPU密集型的写法
3, //超时等待的时间,超过这个时间剩余3个线程就会关闭
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3),//候客区的大小
Executors.defaultThreadFactory(),
//new ThreadPoolExecutor.AbortPolicy 银行满了,候客区也满了,但是还有人进来,就不处理这个人的了,并且抛出异常
//new ThreadPoolExecutor.CallerRunsPolicy() //哪儿来的去哪里
//new ThreadPoolExecutor.DiscardPolicy() //队列满了,丢掉任务不会抛出异常
new ThreadPoolExecutor.DiscardOldestPolicy() //队列满了,尝试和最早的竞争,竞争失败就丢掉任务,不会抛出异常
);
try{
//最大承载 = queue + max
//AbortPolicy 超出可能抛出异常:java.util.concurrent.RejectedExecutionException
for (int i = 1; i < 10; i++) {
//使用了线程池之后,使用线程池来创建线程
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+"ok");
});
}
} catch (Exception e){
e.printStackTrace();
} finally {
//线程池用完,程序结束,关闭线程池
threadPool.shutdown();
}
}
}
提交方法
-
excute:没有返回值,传入一个runnable类型的实例
public void execute(Runnable command)
如果问到 submit : 有返回值
第10章 Executor框架
JDK5开始,将工作单元和执行机制分离开,工作单元包括Runnable和Callable,执行机制由Excutor框架提供
10.1 Executor框架简介
两级调度模型
Java线程被一对一映射到本地操作系统,Java多线程程序把任务分解为若干个任务,然后使用用户级的的调度器(Executor框架)将这些任务映射为固定数量的线程;底层,操作系统将这些线程映射到硬件处理器上
Executor框架的结构和成员
- 结构:三大类
- 任务,包括执行需要实现的接口,runnable,callable
- 任务的执行,核心接口Executor,关键子类(实现了ExecutorService):ThreadPoolExecutor、ScheduledThreadPoolExecutor
- 异步计算的结果:接口Future及其实现类FutureTask
Executor框架的成员
主要成员:ThreadPoolExecutor、ScheduledThreadPoolExecutor、Future、Runnable、Callable、Executors
-
ThreadPoolExecutor:通常使用工厂类Executors来创建
SingleThreadExecutor、FixedThreadPool(5)、CachedThreadPool()
-
ScheduledThreadPoolExecutor:通常使用工厂类Executors来创建
ScheduledThreadPoolExecutor(包含若干个线程)、SingleThreadScheduledExecutor(只包含一个线程)