并发编程juc笔记

并发编程juc笔记

3. Java 线程

3.1 创建和运行线程

方法一,直接使用 Thread

    // 创建线程对象
    Thread t = new Thread() {
        public void run() {
			// 要执行的任务
        }
    };
	// 启动线程
	t.start();

方法二,使用 Runnable 配合 Thread

把【线程】和【任务】(要执行的代码)分开

  • Thread 代表线程
  • Runnable 可运行的任务(线程要执行的代码)
Runnable runnable = new Runnable() {
	public void run(){
		// 要执行的任务
	}
};
// 创建线程对象
Thread t = new Thread( runnable );
// 启动线程
t.start();

方法三,FutureTask 配合 Thread

FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况

// 创建任务对象
FutureTask<Integer> task3 = new FutureTask<>(() -> {
	log.debug("hello");
	return 100;
});
// 参数1 是任务对象; 参数2 是线程名字,推荐
new Thread(task3, "t3").start();
// 主线程阻塞,同步等待 task 执行完毕的结果
Integer result = task3.get();
log.debug("结果是:{}", result);

3.3 查看进程线程的方法

windows

  • 任务管理器可以查看进程和线程数,也可以用来杀死进程
  • tasklist 查看进程
  • taskkill 杀死进程

linux

  • ps -fe 查看所有进程
  • ps -fT -p 查看某个进程(PID)的所有线程
  • kill 杀死进程
  • top 按大写 H 切换是否显示线程
  • top -H -p 查看某个进程(PID)的所有线程

Java

  • jps 命令查看所有 Java 进程
  • jstack 查看某个 Java 进程(PID)的所有线程状态
  • jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)

jconsole 远程监控配置

  • 需要以如下方式运行你的 java 类

java -Djava.rmi.server.hostname=ip地址 -Dcom.sun.management.jmxremote -
Dcom.sun.management.jmxremote.port=连接端口 -Dcom.sun.management.jmxremote.ssl=是否安全连接 -
Dcom.sun.management.jmxremote.authenticate=是否认证 java类

  • 修改 /etc/hosts 文件将 127.0.0.1 映射至主机名
    如果要认证访问,还需要做如下步骤
  • 复制 jmxremote.password 文件
  • 修改 jmxremote.password 和 jmxremote.access 文件的权限为 600 即文件所有者可读写
  • 连接时填入 controlRole(用户名),R&D(密码)

3.4 * 原理之线程运行

现代 CPU 支持多级指令流水线,例如支持同时执行 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回 的处理器,就可以称之为五级指令流水线。这时 CPU 可以在一个时钟周期内,同时运行五条指令的不同阶段(相当于一条执行时间最长的复杂指令),IPC = 1,本质上,流水线技术并不能缩短单条指令的执行时间,但它变相地提高了指令地吞吐率。

栈与栈帧

Java Virtual Machine Stacks (Java 虚拟机栈)
我们都知道 JVM 中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存。

  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

线程上下文切换(Thread Context Switch)

因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码

  • 线程的 cpu 时间片用完
  • 垃圾回收
  • 有更高优先级的线程需要运行
  • 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法
    当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念
    就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的
  • 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
  • Context Switch 频繁发生会影响性能

3.5 常见方法

方法名static功能说明注意
start()启动一个新线程,在新的线程运行 run 方法中的代码start 方法只是让线程进入就绪,里面代码不一定立刻运行(CPU 的时间片还没分给它)。每个线程对象的start方法只能调用一次,如果调用了多次会出现IllegalThreadStateException
run()新线程启动后会调用的方法如果在构造 Thread 对象时传递了 Runnable 参数,则线程启动后会调用 Runnable 中的 run 方法,否则默认不执行任何操作。但可以创建 Thread 的子类对象, 来覆盖默认行为
join()等待线程运行结束
join(long n)等待线程运行结束,最多等待 n 毫秒
getId()获取线程长整型的 idid 唯一
getName()获取线程名
setName(String)修改线程名
getPriority()获取线程优先级
setPriority(int)修改线程优先级java中规定线程优先级是1~10 的整数,较大的优先级能提高该线程被 CPU 调度的机率
getState()获取线程状态Java 中线程状态是用 6 个 enum 表示,分别为: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED
isInterrupted()判断是否被打断,不会清除 打断标记
isAlive()线程是否存活(还没有运行完毕)
interrupt()打断线程如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除 打断标记 ;如果打断的正在运行的线程,则会设置 打断标记 ;park 的线程被打断,也会设置 打断标记
interrupted()static判断当前线程是否被打断会清除 打断标记
currentThread()static获取当前正在执行的线程
sleep(long n)static让当前执行的线程休眠n毫秒, 休眠时让出 cpu 的时间片给其它线程
yield()static提示线程调度器让出当前线程对CPU的使用主要是为了测试和调试

3.7 sleep 与 yield

sleep

  1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
  2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
  3. 睡眠结束后的线程未必会立刻得到执行
  4. 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性

yield

  1. 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
  2. 具体的实现依赖于操作系统的任务调度器

线程优先级

  • 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
  • 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用

3.8 join 原理

是调用者轮询检查线程 alive 状态

   t1.join();

等价于下面的代码

    synchronized (t1) {
        // 调用者线程进入 t1 的 waitSet 等待, 直到 t1 运行结束
        while (t1.isAlive()) {
            t1.wait(0);
        }
    }

join 体现的是【保护性暂停】模式

3.9 interrupt 方法详解

打断 sleep,wait,join 的线程
这几个方法都会让线程进入阻塞状态
打断 sleep 的线程, 会清空打断状态并抛InterruptedException
打断正常运行的线程, 不会清空打断状态,也无事发生
打断 park 线程, 不会清空打断状态,如果打断标记已经是 true, 则 park 会失效

3.10 不推荐的方法

这些方法已过时,容易破坏同步代码块,造成线程死锁

方法名static功能说明
stop()停止线程运行
suspend()挂起(暂停)线程运行
resume()恢复线程运行

3.11 主线程与守护线程

默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
垃圾回收器线程就是一种守护线程
Tomcat 中的 Acceptor 爱克赛普特 接收请求 和 Poller 破了 分发请求 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求

// 设置该线程为守护线程
t1.setDaemon(true);
t1.start();

3.12 五种状态

这是从 操作系统 层面来描述的
在这里插入图片描述

  • 【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联
  • 【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
  • 【运行状态】指获取了 CPU 时间片运行中的状态
    • 当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换
  • 【阻塞状态】
    • 如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入【阻塞状态】
    • 等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
    • 与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们
  • 【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态

3.13 六种状态

这是从 Java API 层面来描述的
根据 Thread.State 枚举,分为六种状态
Running(run 宁,运行中)、NEW、RUNNABLE(run呢播,就绪)、BLOCKED(伯劳科特,阻塞)等待锁、WAITING(违停,等待)等join、TIMED_WAITING(太m的 违停,定时等待 )等sleep、TERMINATED(特么内带的,终止)
在这里插入图片描述

  • NEW 线程刚被创建,但是还没有调用 start() 方法
  • RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)
  • BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换一节详述
  • TERMINATED 当线程代码运行结束

本章小结

本章的重点在于掌握

  • 线程创建
  • 线程重要 api,如 start,run,sleep,join,interrupt 等
  • 线程状态
  • 应用方面
    • 异步调用:主线程执行期间,其它线程异步执行耗时操作
    • 提高效率:并行计算,缩短运算时间
    • 同步等待:join
    • 统筹规划:合理使用线程,得到最优效果
  • 原理方面
    • 线程运行流程:栈、栈帧、上下文切换、程序计数器
    • Thread 两种创建方式 的源码
  • 模式方面
    • 终止模式之两阶段终止

4. 共享模型之管程

常见线程安全类
String
Integer
StringBuffer
Random 软的木
Vector v爱科特
Hashtable 哈希忒播
java.util.concurrent 肯珂润特 包下的类
它们的每个方法是原子的,但它们多个方法的组合不是原子的

4.6 Monitor 概念

java对象再内存中由2部分组成:对象头Object Header 、成员变量 Object body
Object Header (64 bits) 对象头,在32位虚拟机是64位,8个字节。
Mark Word (32 bits) 32位,4个字节 ,
Normal (no莫)正常状态下:
hashcode:25位,每个对象都有自己的哈希码
age:4 位 垃圾回收的分代年龄,年龄到一定次数会从幸存区到老年代
biased_lock:1位,是不是偏向锁
01 最后2位,代表加锁状态

其他状态下,加了重量级锁,偏向锁,垃圾回收等等,Mark Word 会改变

Klass Word 是指针,指向对象所存储的class(是个什么类型的对象,比如学生类,老师类),简单理解就是找到类对象。

数组对象还多一个array length(32bits) 4个字节的数组长度

32位虚拟机里
int占4个字节
Integer 占 对象头8+ int 4 = 12个字节

Java 对象头
以 32 位虚拟机为例

普通对象
在这里插入图片描述
数组对象
在这里插入图片描述
其中 Mark Word 结构为
在这里插入图片描述
64 位虚拟机 Mark Word
在这里插入图片描述

Monitor 原理

Monitor 被翻译为监视器或管程
每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针
Monitor 结构如下
在这里插入图片描述

  • 刚开始 Monitor 中 Owner 为 null
  • 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一个 Owner
  • 在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入EntryList BLOCKED
  • Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的
  • 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面讲wait-notify 时会分析

注意:

  • synchronized 必须是进入同一个对象的 monitor 才有上述的效果
  • 不加 synchronized 的对象不会关联监视器,不遵从以上规则

synchronized 原理进阶

1. 轻量级锁

轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
轻量级锁对使用者是透明的,即语法仍然是 synchronized
假设有两个方法同步块,利用同一个对象加锁

创建锁记录(Lock Record)对象,每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word
在这里插入图片描述
让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录
在这里插入图片描述
如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁,这时图示如下
在这里插入图片描述
如果 cas 失败,有两种情况

  • 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
  • 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数
    在这里插入图片描述
    当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一

在这里插入图片描述
当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头

  • 成功,则解锁成功
  • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

2. 锁膨胀

如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
在这里插入图片描述
这时 Thread-1 加轻量级锁失败,进入锁膨胀流程

  • 即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址
  • 然后自己进入 Monitor 的 EntryList BLOCKED
    在这里插入图片描述
    当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程

3. 自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。

  • 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
  • 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
  • Java 7 之后不能控制是否开启自旋功能

4. 偏向锁

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有

在这里插入图片描述
一个对象创建时:

  • 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的thread、epoch、age 都为 0
  • 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟
  • 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、age 都为 0,第一次用到 hashcode 时才会赋值

添加 VM 参数 -XX:-UseBiasedLocking 禁用偏向锁
正常状态对象一开始是没有 hashCode 的,第一次调用才生成

撤销 - 调用对象 hashCode

调用了对象的 hashCode,但偏向锁的对象 MarkWord 中存储的是线程 id,如果调用 hashCode 会导致偏向锁被撤销

  • 轻量级锁会在锁记录中记录 hashCode
  • 重量级锁会在 Monitor 中记录 hashCode
撤销 - 其它线程使用对象

当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁

撤销 - 调用 wait/notify
批量重偏向

如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID
当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程

批量撤销

当撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的

5. 锁消除

压根没竞争

6. 锁粗化

对相同对象多次加锁,导致线程发生多次重入,可以使用锁粗化方式来优化,这不同于之前讲的细分锁的粒度。

其他

Monitor(c实现的) 锁(摸你头) 被翻译为监视器(字面翻译)或管程(操作系统)
每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针
monitor是操作系统提供的对象,
关联成功就会把Mark Word最后2位的01改成10,前面30位改成指向Monitor 对象的指针

Monitor 中有 waitset 等待列表, entrylist 等待队列(底层是个链表结构)、阻塞队列,owner(哦呢儿) 所有者 ,3个属性,线程获得锁会在owner记录线程。第二个线程进来也会指向这个monitor,检查owner如果有主人,就进入entrylist ,变成BLOCKED阻塞状态,上下文切换。后面的线程也一样。

字节码层面:将 lock对象 MarkWord 置为 Monitor 指针 , 执行后续操作,如果出现异常会找到lock引用,将 lock对象 MarkWord 重置, 唤醒 EntryList,保证异常还能解锁。

轻量级锁:进入synchronized这行代码的时候,方法的栈帧会创建一个lock record (唠嗑热扣的)锁记录对象(不是java层面的, 是jvm层面的),这个对象包含两部分:锁对象指针,锁对象的Mark Word

java运行时,有一个JIT即时编译器,对字节码进一步优化,解释+编译,热点代码会进一步优化

4.7 wait notify

没获得锁就调wait方法会报 illegalMonitorStateException 非法监视器状态异常

sleep(long n) 和 wait(long n) 的区别

  1. sleep 是 Thread 方法,而 wait 是 Object 的方法
  2. sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用
  3. sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁
  4. 它们状态 TIMED_WAITING

API 介绍

  • obj.wait() 让进入 object 监视器的线程到 waitSet 等待
  • obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒
  • obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒

它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁,才能调用这几个方法

wait notify 原理

在这里插入图片描述

  • Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
  • BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
  • BLOCKED 线程会在 Owner 线程释放锁时唤醒
  • WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList 重新竞争

4.9 Park & Unpark

Park & Unpark
实际是Unsafe.park()
Parker对象C代码实现的,Java看不到

// 暂停当前线程
LockSupport 色颇尔特.park();
// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象)

与 Object 的 wait & notify 相比
wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必
park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll
是唤醒所有等待线程,就不那么【精确】park & unpark 可以先 unpark,而 wait & notify 不能先 notify

4.12 活跃性

死锁 A等B,B等A

定位死锁:

检测死锁可以使用 jconsole工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁
另外如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 linux 下可以通过 top 先定位到CPU 占用高的 Java 进程,再利用 top -Hp 进程id 来定位是哪个线程,最后再用 jstack 排查

活锁

活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束

饥饿

很多教程中把饥饿定义为,一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束

4.13 ReentrantLock

相对于 synchronized 它具备如下特点

  • 可中断
  • 可以设置超时时间
  • 可以设置为公平锁
  • 支持多个条件变量
    与 synchronized 一样,都支持可重入
    基本语法
    lock()
    unlock()
    lockInterruptibly()
    注意如果是不可中断模式,那么即使使用了 interrupt 也不会让等待中断
    tryLock()
    lock.tryLock(1, TimeUnit.SECONDS)
    公平锁: new ReentrantLock(true);

本章小结

本章我们需要重点掌握的是

  • 分析多线程访问共享资源时,哪些代码片段属于临界区
  • 使用 synchronized 互斥解决临界区的线程安全问题
    • 掌握 synchronized 锁对象语法
    • 掌握 synchronzied 加载成员方法和静态方法语法
    • 掌握 wait/notify 同步方法
  • 使用 lock 互斥解决临界区的线程安全问题
    • 掌握 lock 的使用细节:可打断、锁超时、公平锁、条件变量
  • 学会分析变量的线程安全性、掌握常见线程安全类的使用
  • 了解线程活跃性问题:死锁、活锁、饥饿
  • 应用方面
    • 互斥:使用 synchronized 或 Lock 达到共享资源互斥效果
    • 同步:使用 wait/notify 或 Lock 的条件变量来达到线程间通信效果
  • 原理方面
    • monitor、synchronized 、wait/notify 原理
    • synchronized 进阶原理
    • park & unpark 原理
  • 模式方面
    • 同步模式之保护性暂停
    • 异步模式之生产者消费者
    • 同步模式之顺序控制

5. 共享模型之内存

5.1 Java 内存模型

JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等。
JMM 体现在以下几个方面

  • 原子性 - 保证指令不会受到线程上下文切换的影响
  • 可见性 - 保证指令不会受 cpu 缓存的影响
  • 有序性 - 保证指令不会受 cpu 指令并行优化的影响

内存屏障

Memory Barrier(Memory Fence)

  • 可见性
    • 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
    • 而读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据
  • 有序性
    • 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
    • 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前

volatile 原理

volatile 的底层实现原理是内存屏障,Memory Barrier(Memory Fence)

  • 对 volatile 变量的写指令后会加入写屏障
  • 对 volatile 变量的读指令前会加入读屏障

单例问题: t1 还未完全将构造方法执行完毕,如果在构造方法中要执行很多初始化操作,那么 t2 拿到的是将是一个未初始化完毕的单例

读写 volatile 变量时会加入内存屏障(Memory Barrier(Memory Fence)),保证下面两点:

  • 可见性
    • 写屏障(sfence)保证在该屏障之前的 t1 对共享变量的改动,都同步到主存当中
    • 而读屏障(lfence)保证在该屏障之后 t2 对共享变量的读取,加载的是主存中最新数据
  • 有序性
    • 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
    • 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
  • 更底层是读写变量时使用 lock 指令来多核 CPU 之间的可见性与有序性

final 原理

发现 final 变量的赋值也会通过 putfield 指令来完成,同样在这条指令之后也会加入写屏障,保证在其它线程读到它的值时不会出现为 0 的情况

6. 共享模型之无锁 CAS

Atomic 额掏没课 原子的
Reference 瑞福润次 引用
Markable 马克A播 被标记的
Stamped 斯塔姆普特 盖上邮戳的

6.3 原子整数

J.U.C 并发包提供了:
AtomicBoolean
AtomicInteger
AtomicLong
以 AtomicInteger 为例

AtomicInteger i = new AtomicInteger(0);
// 获取并自增(i = 0, 结果 i = 1, 返回 0),类似于 i++
System.out.println(i.getAndIncrement());
// 自增并获取(i = 1, 结果 i = 2, 返回 2),类似于 ++i
System.out.println(i.incrementAndGet());
// 自减并获取(i = 2, 结果 i = 1, 返回 1),类似于 --i
System.out.println(i.decrementAndGet());
// 获取并自减(i = 1, 结果 i = 0, 返回 1),类似于 i--
System.out.println(i.getAndDecrement());
// 获取并加值(i = 0, 结果 i = 5, 返回 0)
System.out.println(i.getAndAdd(5));
// 加值并获取(i = 5, 结果 i = 0, 返回 0)
System.out.println(i.addAndGet(-5));
// 获取并更新(i = 0, p 为 i 的当前值, 结果 i = -2, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.getAndUpdate(p -> p - 2));
// 更新并获取(i = -2, p 为 i 的当前值, 结果 i = 0, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.updateAndGet(p -> p + 2));
// 获取并计算(i = 0, p 为 i 的当前值, x 为参数1, 结果 i = 10, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
// getAndUpdate 如果在 lambda 中引用了外部的局部变量,要保证该局部变量是 final 的
// getAndAccumulate 可以通过 参数1 来引用外部的局部变量,但因为其不在 lambda 中因此不必是 final
System.out.println(i.getAndAccumulate(10, (p, x) -> p + x));
// 计算并获取(i = 10, p 为 i 的当前值, x 为参数1, 结果 i = 0, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.accumulateAndGet(-10, (p, x) -> p + x));

6.4 原子引用

AtomicReference
AtomicMarkableReference
AtomicStampedReference

6.5 原子数组

AtomicIntegerArray
AtomicLongArray
AtomicReferenceArray

6.6 字段更新器

AtomicReferenceFieldUpdater // 域 字段
AtomicIntegerFieldUpdater
AtomicLongFieldUpdater

LongAdder

LongAdder 是并发大师 @author Doug Lea (大哥李)的作品,设计的非常精巧
LongAdder 类有几个关键域
Contended 肯疼带的
成员变量也叫域

// 累加单元数组, 懒惰初始化
transient volatile Cell[] cells;
// 基础值, 如果没有竞争, 则用 cas 累加这个域
transient volatile long base;
// 在 cells 创建或扩容时, 置为 1, 表示加锁
transient volatile int cellsBusy;

cas 锁

  // 不要用于实践!!!
  public class LockCas {
      private AtomicInteger state = new AtomicInteger(0);
      public void lock() {
          while (true) {
              if (state.compareAndSet(0, 1)) {
                  break;
              }
          }
      }
      public void unlock() {
          log.debug("unlock...");
          state.set(0);
      }
  }

@sun.misc没死可.Contended 恳谈得的 防止缓存行伪共享
每个缓存行对应着一块内存,一般是 64 byte(8 个 long)
因为 Cell 是数组形式,在内存中是连续存储的,一个 Cell 为 24 字节(16 字节的对象头和 8 字节的 value),因此缓存行可以存下 2 个的 Cell 对象
@sun.misc.Contended 用来解决这个问题,它的原理是在使用此注解的对象或字段的前后各增加 128 字节大小的padding

原理之伪共享

其中 Cell 即为累加单元

   // 防止缓存行伪共享
   @sun.misc.Contended
   static final class Cell {
       volatile long value;
       Cell(long x) { value = x; }
       // 最重要的方法, 用来 cas 方式进行累加, prev 表示旧值, next 表示新值
       final boolean cas(long prev, long next) {
           return UNSAFE.compareAndSwapLong(this, valueOffset, prev, next);
       }
       // 省略不重要代码
   }

因为 CPU 与 内存的速度差异很大,需要靠预读数据至缓存来提升效率。
而缓存以缓存行为单位,每个缓存行对应着一块内存,一般是 64 byte(8 个 long)
缓存的加入会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中
CPU 要保证数据的一致性,如果某个 CPU 核心更改了数据,其它 CPU 核心对应的整个缓存行必须失效
在这里插入图片描述
因为 Cell 是数组形式,在内存中是连续存储的,一个 Cell 为 24 字节(16 字节的对象头和 8 字节的 value),因此缓存行可以存下 2 个的 Cell 对象。这样问题来了:

  • Core-0 要修改 Cell[0]
  • Core-1 要修改 Cell[1]

无论谁修改成功,都会导致对方 Core 的缓存行失效,比如 Core-0 中 Cell[0]=6000, Cell[1]=8000 要累加Cell[0]=6001, Cell[1]=8000 ,这时会让 Core-1 的缓存行失效
@sun.misc.Contended 用来解决这个问题,它的原理是在使用此注解的对象或字段的前后各增加 128 字节大小的padding,从而让 CPU 将对象预读至缓存时占用不同的缓存行,这样,不会造成对方缓存行的失效

在这里插入图片描述
累加主要调用下面的方法

   public void add ( long x){
       // as 为累加单元数组
       // b 为基础值
       // x 为累加值
       Cell[] as;
       long b, v;
       int m;
       Cell a;
       // 进入 if 的两个条件
       // 1. as 有值, 表示已经发生过竞争, 进入 if
       // 2. cas 给 base 累加时失败了, 表示 base 发生了竞争, 进入 if
       if ((as = cells) != null || !casBase(b = base, b + x)) {
           // uncontended 表示 cell 没有竞争
           boolean uncontended = true;
           if (
                   // as 还没有创建
                   as == null || (m = as.length - 1) < 0 ||
                           // 当前线程对应的 cell 还没有
                           (a = as[getProbe() & m]) == null ||
                           // cas 给当前线程的 cell 累加失败 uncontended=false ( a 为当前线程的 cell )
                           !(uncontended = a.cas(v = a.value, v + x))
           ) {
               // 进入 cell 数组创建、cell 创建的流程
               longAccumulate(x, null, uncontended);
           }
       }
   }

add 流程图
在这里插入图片描述

    final void longAccumulate(long x, LongBinaryOperator fn,
                              boolean wasUncontended) {
        int h;
// 当前线程还没有对应的 cell, 需要随机生成一个 h 值用来将当前线程绑定到 cell
        if ((h = getProbe()) == 0) {
// 初始化 probe
            ThreadLocalRandom.current();
// h 对应新的 probe 值, 用来对应 cell
            h = getProbe();
            wasUncontended = true;
        }
// collide 为 true 表示需要扩容
        boolean collide = false;
        for (; ; ) {
            Cell[] as;
            Cell a;
            int n;
            long v;
// 已经有了 cells
            if ((as = cells) != null && (n = as.length) > 0) {
// 还没有 cell
                if ((a = as[(n - 1) & h]) == null) {
// 为 cellsBusy 加锁, 创建 cell, cell 的初始累加值为 x
// 成功则 break, 否则继续 continue 循环
                }
// 有竞争, 改变线程对应的 cell 来重试 cas
                else if (!wasUncontended)
                    wasUncontended = true;
// cas 尝试累加, fn 配合 LongAccumulator 不为 null, 配合 LongAdder 为 null
                else if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
                    break;
// 如果 cells 长度已经超过了最大长度, 或者已经扩容, 改变线程对应的 cell 来重试 cas
                else if (n >= NCPU || cells != as)
                    collide = false;
// 确保 collide 为 false 进入此分支, 就不会进入下面的 else if 进行扩容了
                else if (!collide)
                    collide = true;
// 加锁
                else if (cellsBusy == 0 && casCellsBusy()) {
// 加锁成功, 扩容
                    continue;
                }
// 改变线程对应的 cell
                h = advanceProbe(h);
            }
// 还没有 cells, 尝试给 cellsBusy 加锁
            else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
// 加锁成功, 初始化 cells, 最开始长度为 2, 并填充一个 cell
// 成功则 break;
            }
// 上两种情况失败, 尝试给 base 累加
            else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
                break;
        }
    }

longAccumulate 流程图
在这里插入图片描述
在这里插入图片描述
每个线程刚进入 longAccumulate 时,会尝试对应一个 cell 对象(找到一个坑位)
在这里插入图片描述
获取最终结果通过 sum 方法

  public long sum() {
      Cell[] as = cells; Cell a;
      long sum = base;
      if (as != null) {
          for (int i = 0; i < as.length; ++i) {
              if ((a = as[i]) != null)
                  sum += a.value;
          }
      }
      return sum;
  }

在这里插入图片描述
getProbe:获得Thread.currentThread()中的threadLocalRandomProbe值,用于重新随机获取probe值

6.8 Unsafe

Unsafe 对象提供了非常底层的,操作内存、线程的方法,Unsafe 对象不能直接调用,只能通过反射获得

    public class UnsafeAccessor {
        static Unsafe unsafe;
        static {
            try {
                Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
                theUnsafe.setAccessible(true);
                unsafe = (Unsafe) theUnsafe.get(null);
            } catch (NoSuchFieldException | IllegalAccessException e) {
                throw new Error(e);
            }
        }
        static Unsafe getUnsafe() {
            return unsafe;
        }
    }

Unsafe CAS 操作

  @Data
  class Student {
      volatile int id;
      volatile String name;
  }
Unsafe unsafe = UnsafeAccessor.getUnsafe();
Field id = Student.class.getDeclaredField("id");
Field name = Student.class.getDeclaredField("name");
// 获得成员变量的偏移量
long idOffset = UnsafeAccessor.unsafe.objectFieldOffset(id);
long nameOffset = UnsafeAccessor.unsafe.objectFieldOffset(name);
Student student = new Student();
// 使用 cas 方法替换成员变量的值
UnsafeAccessor.unsafe.compareAndSwapInt(student, idOffset, 0, 20); // 返回 true
UnsafeAccessor.unsafe.compareAndSwapObject(student, nameOffset, null, "张三"); // 返回 true
System.out.println(student);

输出

Student(id=20, name=张三)

使用自定义的 AtomicData 实现之前线程安全的原子整数 Account 实现

    class AtomicData {
        private volatile int data;
        static final Unsafe unsafe;
        static final long DATA_OFFSET;
        static {
            unsafe = UnsafeAccessor.getUnsafe();
            try {
// data 属性在 DataContainer 对象中的偏移量,用于 Unsafe 直接访问该属性
                DATA_OFFSET = unsafe.objectFieldOffset(AtomicData.class.getDeclaredField("data"));
            } catch (NoSuchFieldException e) {
                throw new Error(e);
            }
        }
        public AtomicData(int data) {
            this.data = data;
        }
        public void decrease(int amount) {
            int oldValue;
            while(true) {
// 获取共享变量旧值,可以在这一行加入断点,修改 data 调试来加深理解
                oldValue = data;
// cas 尝试修改 data 为 旧值 + amount,如果期间旧值被别的线程改了,返回 false
                if (unsafe.compareAndSwapInt(this, DATA_OFFSET, oldValue, oldValue - amount)) {
                    return;
                }
            }
        }
        public int getData() {
            return data;
        }
    }

本章小结

  • CAS 与 volatile
  • API
    • 原子整数
    • 原子引用
    • 原子数组
    • 字段更新器
    • 原子累加器
  • Unsafe
    • 原理方面
      • LongAdder 源码
      • 伪共享

7. 共享模型之不可变

SimpleDateFormat不安全,DateTimeFormatter安全

7.3 无状态

因为成员变量保存的数据也可以称为状态信息,因此没有成员变量就称之为【无状态】

在 web 阶段学习时,设计 Servlet 时为了保证其线程安全,都会有这样的建议,不要为 Servlet 设置成员变量,这种没有任何成员变量的类是线程安全的

final
写屏障:
保证写屏障之前的指令不会被重排序到写屏障后面
写屏障之前的所有赋值操作,都会被同步到主存中去

如果不加final,分配空间 和 赋值 是两步操作,可能会被其他线程读取到默认值0

static final 是直接复制值到要读的线程的栈内存中 ,如果不加final会去对象中读,走的共享内存,性能会低,不加final就是读的堆

策略模式,把具体操作抽象成一个接口,具体实现由调用者传递

本章小结

  • 不可变类使用
  • 不可变类设计
    • 原理方面
      • final
  • 模式方面
    • 享元

8. 共享模型之工具

8.1 线程池

1. 自定义线程池

在这里插入图片描述
步骤1:自定义拒绝策略接口

    @FunctionalInterface // 拒绝策略
    interface RejectPolicy<T> {
        void reject(BlockingQueue<T> queue, T task);
    }

步骤2:自定义任务队列

    class BlockingQueue<T> {
        // 1. 任务队列
        private Deque<T> queue = new ArrayDeque<>();
        // 2. 锁
        private ReentrantLock lock = new ReentrantLock();
        // 3. 生产者条件变量
        private Condition fullWaitSet = lock.newCondition();
        // 4. 消费者条件变量
        private Condition emptyWaitSet = lock.newCondition();
        // 5. 容量
        private int capcity;

        public BlockingQueue(int capcity) {
            this.capcity = capcity;
        }

        // 带超时阻塞获取
        public T poll(long timeout, TimeUnit unit) {
            lock.lock();
            try {
// 将 timeout 统一转换为 纳秒
                long nanos = unit.toNanos(timeout);
                while (queue.isEmpty()) {
                    try {
// 返回值是剩余时间
                        if (nanos <= 0) {
                            return null;
                        }
                        nanos = emptyWaitSet.awaitNanos(nanos);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                T t = queue.removeFirst();
                fullWaitSet.signal();
                return t;
            } finally {
                lock.unlock();
            }
        }

        // 阻塞获取
        public T take() {
            lock.lock();
            try {
                while (queue.isEmpty()) {
                    try {
                        emptyWaitSet.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                T t = queue.removeFirst();
                fullWaitSet.signal();
                return t;
            } finally {
                lock.unlock();
            }
        }

        // 阻塞添加
        public void put(T task) {
            lock.lock();
            try {
                while (queue.size() == capcity) {
                    try {
                        log.debug("等待加入任务队列 {} ...", task);
                        fullWaitSet.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("加入任务队列 {}", task);
                queue.addLast(task);
                emptyWaitSet.signal();
            } finally {
                lock.unlock();
            }
        }

        // 带超时时间阻塞添加
        public boolean offer(T task, long timeout, TimeUnit timeUnit) {
            lock.lock();
            try {
                long nanos = timeUnit.toNanos(timeout);
                while (queue.size() == capcity) {
                    try {
                        if (nanos <= 0) {
                            return false;
                        }
                        log.debug("等待加入任务队列 {} ...", task);
                        nanos = fullWaitSet.awaitNanos(nanos);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("加入任务队列 {}", task);
                queue.addLast(task);
                emptyWaitSet.signal();
                return true;
            } finally {
                lock.unlock();
            }
        }

        public int size() {
            lock.lock();
            try {
                return queue.size();
            } finally {
                lock.unlock();
            }
        }

        public void tryPut(RejectPolicy<T> rejectPolicy, T task) {
            lock.lock();
            try {
// 判断队列是否满
                if (queue.size() == capcity) {
                    rejectPolicy.reject(this, task);
                } else { // 有空闲
                    log.debug("加入任务队列 {}", task);
                    queue.addLast(task);
                    emptyWaitSet.signal();
                }
            } finally {
                lock.unlock();
            }
        }
    }

步骤3:自定义线程池

    class ThreadPool {
        // 任务队列
        private BlockingQueue<Runnable> taskQueue;
        // 线程集合
        private HashSet<Worker> workers = new HashSet<>();
        // 核心线程数
        private int coreSize;
        // 获取任务时的超时时间
        private long timeout;
        private TimeUnit timeUnit;
        private RejectPolicy<Runnable> rejectPolicy;

        // 执行任务
        public void execute(Runnable task) {
// 当任务数没有超过 coreSize 时,直接交给 worker 对象执行
// 如果任务数超过 coreSize 时,加入任务队列暂存
            synchronized (workers) {
                if (workers.size() < coreSize) {
                    Worker worker = new Worker(task);
                    log.debug("新增 worker{}, {}", worker, task);
                    workers.add(worker);
                    worker.start();
                } else {
// taskQueue.put(task);
// 1) 死等
// 2) 带超时等待
// 3) 让调用者放弃任务执行
// 4) 让调用者抛出异常
// 5) 让调用者自己执行任务
                    taskQueue.tryPut(rejectPolicy, task);
                }
            }
        }

        public ThreadPool(int coreSize, long timeout, TimeUnit timeUnit, int queueCapcity,
                          RejectPolicy<Runnable> rejectPolicy) {
            this.coreSize = coreSize;
            this.timeout = timeout;
            this.timeUnit = timeUnit;
            this.taskQueue = new BlockingQueue<>(queueCapcity);
            this.rejectPolicy = rejectPolicy;
        }

        class Worker extends Thread {
            private Runnable task;

            public Worker(Runnable task) {
                this.task = task;
            }

            @Override
            public void run() {
// 执行任务
// 1) 当 task 不为空,执行任务
// 2) 当 task 执行完毕,再接着从任务队列获取任务并执行
// while(task != null || (task = taskQueue.take()) != null) {
                while (task != null || (task = taskQueue.poll(timeout, timeUnit)) != null) {
                    try {
                        log.debug("正在执行...{}", task);
                        task.run();
                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        task = null;
                    }
                }
                synchronized (workers) {
                    log.debug("worker 被移除{}", this);
                    workers.remove(this);
                }
            }
        }
    }

步骤4:测试

    public static void main(String[] args) {
        ThreadPool threadPool = new ThreadPool(1,
                1000, TimeUnit.MILLISECONDS, 1, (queue, task) -> {
// 1. 死等
// queue.put(task);
// 2) 带超时等待
// queue.offer(task, 1500, TimeUnit.MILLISECONDS);
// 3) 让调用者放弃任务执行
// log.debug("放弃{}", task);
// 4) 让调用者抛出异常
// throw new RuntimeException("任务执行失败 " + task);
// 5) 让调用者自己执行任务
            task.run();
        });
        for (int i = 0; i < 4; i++) {
            int j = i;
            threadPool.execute(() -> {
                try {
                    Thread.sleep(1000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("{}", j);
            });
        }
    }

2. ThreadPoolExecutor

ExecutorService 定义了基础方法,提交任务,关闭线程池等方法。

ScheduledExedcutorService任务调度功能,定时执行任务。
在这里插入图片描述

线程池状态

ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位表示线程数量

状态名 3 接收新任务处理阻塞队列任务说明
RUNNING111YY
SHUTDOWN000NY不会接收新任务,但会处理阻塞队列剩余任务
STOP001NN会中断正在执行的任务,并抛弃阻塞队列任务
TIDYING010--任务全执行完毕,活动线程为 0 即将进入终结
TERMINATED011--终结状态

从数字上比较,TERMINATED > TIDYING > STOP > SHUTDOWN > RUNNING
这些信息存储在一个原子变量 ctl 中,目的是将线程池状态与线程个数合二为一,这样就可以用一次 cas 原子操作进行赋值

// c 为旧值, ctlOf 返回结果为新值
ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c))));
// rs 为高 3 位代表线程池状态, wc 为低 29 位代表线程个数,ctl 是合并它们
private static int ctlOf(int rs, int wc) { return rs | wc; }

RUNNING 线程池对象创建后就是
SHUTDOWN 调用SHUTDOWN方法后
STOP 调用SHUTDOWNnow后 因特软普特打断
TIDYING 过度
TERMINATED 终结

构造方法
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
  • corePoolSize 核心线程数目 (最多保留的线程数)

  • maximumPoolSize 最大线程数目

  • keepAliveTime 生存时间 - 针对救急线程

  • unit 时间单位 - 针对救急线程

  • workQueue 阻塞队列

  • threadFactory 线程工厂 - 可以为线程创建时起个好名字

  • handler 拒绝策略
    工作方式:
    在这里插入图片描述
    在这里插入图片描述

  • 线程池中刚开始没有线程,当一个任务提交给线程池后,线程池会创建一个新线程来执行任务。

  • 当线程数达到 corePoolSize 并没有线程空闲,这时再加入任务,新加的任务会被加入workQueue 队列排队,直到有空闲的线程。

  • 如果队列选择了有界队列,那么任务超过了队列大小时,会创建 maximumPoolSize - corePoolSize 数目的线程来救急。

  • 如果线程到达 maximumPoolSize 仍然有新任务这时会执行拒绝策略。拒绝策略 jdk 提供了 4 种实现,其它著名框架也提供了实现

    • AbortPolicy 让调用者抛出 RejectedExecutionException 异常,这是默认策略
    • CallerRunsPolicy 让调用者运行任务
    • DiscardPolicy 放弃本次任务
    • DiscardOldestPolicy 放弃队列中最早的任务,本任务取而代之
    • Dubbo 的实现,在抛出 RejectedExecutionException 异常之前会记录日志,并 dump 线程栈信息,方便定位问题
    • Netty 的实现,是创建一个新线程来执行任务
    • ActiveMQ 的实现,带超时等待(60s)尝试放入队列,类似我们之前自定义的拒绝策略
    • PinPoint 的实现,它使用了一个拒绝策略链,会逐一尝试策略链中每种拒绝策略
  • 当高峰过去后,超过corePoolSize 的救急线程如果一段时间没有任务做,需要结束节省资源,这个时间由keepAliveTime 和 unit 来控制。
    在这里插入图片描述
    根据这个构造方法,JDK Executors 类中提供了众多工厂方法来创建各种用途的线程池
    不过实际开发一般不用
    newFixedThreadPool

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<Runnable>());
    }

特点

  • 核心线程数 == 最大线程数(没有救急线程被创建),因此也无需超时时间
  • 阻塞队列是无界的,可以放任意数量的任务

评价 适用于任务量已知,相对耗时的任务

newCachedThreadPool

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                60L, TimeUnit.SECONDS,
                new SynchronousQueue<Runnable>());
    }

特点

  • 核心线程数是 0, 最大线程数是 Integer.MAX_VALUE,救急线程的空闲生存时间是 60s,意味着
    • 全部都是救急线程(60s 后可以回收)
    • 救急线程可以无限创建
  • 队列采用了 SynchronousQueue 实现特点是,它没有容量,没有线程来取是放不进去的(一手交钱、一手交货)

评价 整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲 1分钟后释放线程。 适合任务数比较密集,但每个任务执行时间较短的情况

newSingleThreadExecutor

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
                (new ThreadPoolExecutor(1, 1,
                        0L, TimeUnit.MILLISECONDS,
                        new LinkedBlockingQueue<Runnable>()));
    }

使用场景:
希望多个任务排队执行。线程数固定为 1,任务数多于 1 时,会放入无界队列排队。任务执行完毕,这唯一的线程也不会被释放。
区别:

  • 自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而线程池还会新建一个线程,保证池的正常工作
  • Executors.newSingleThreadExecutor() 线程个数始终为1,不能修改
    • FinalizableDelegatedExecutorService 应用的是装饰器模式,只对外暴露了 ExecutorService 接口,因此不能调用 ThreadPoolExecutor 中特有的方法
  • Executors.newFixedThreadPool(1) 初始时为1,以后还可以修改
    • 对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改
提交任务
  // 执行任务
  void execute(Runnable command);

  // 提交任务 task,用返回值 Future 获得任务执行结果
  <T> Future<T> submit(Callable<T> task);

  // 提交 tasks 中所有任务
  <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
          throws InterruptedException;

  // 提交 tasks 中所有任务,带超时时间
  <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
                                long timeout, TimeUnit unit)
          throws InterruptedException;

  // 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消
  <T> T invokeAny(Collection<? extends Callable<T>> tasks)
          throws InterruptedException, ExecutionException;

  // 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消,带超时时间
  <T> T invokeAny(Collection<? extends Callable<T>> tasks,
                  long timeout, TimeUnit unit)
          throws InterruptedException, ExecutionException, TimeoutException;
关闭线程池

shutdown

    /*
    线程池状态变为 SHUTDOWN
    - 不会接收新任务
    - 但已提交任务会执行完
    - 此方法不会阻塞调用线程的执行
    */
    public void shutdown() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
// 修改线程池状态
            advanceRunState(SHUTDOWN);
// 仅会打断空闲线程
            interruptIdleWorkers();
            onShutdown(); // 扩展点 ScheduledThreadPoolExecutor
        } finally {
            mainLock.unlock();
        }
// 尝试终结(没有运行的线程可以立刻终结,如果还有运行的线程也不会等)
        tryTerminate();
    }

shutdownNow

    /*
    线程池状态变为 STOP
    - 不会接收新任务
    - 会将队列中的任务返回
    - 并用 interrupt 的方式中断正在执行的任务
    */
    public List<Runnable> shutdownNow() {
        List<Runnable> tasks;
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
// 修改线程池状态
            advanceRunState(STOP);
// 打断所有线程
            interruptWorkers();
// 获取队列中剩余任务
            tasks = drainQueue();
        } finally {
            mainLock.unlock();
        }
// 尝试终结
        tryTerminate();
        return tasks;
    }

其它方法

    // 不在 RUNNING 状态的线程池,此方法就返回 true
    boolean isShutdown();

    // 线程池状态是否是 TERMINATED
    boolean isTerminated();

    // 调用 shutdown 后,由于调用线程并不会等待所有任务运行结束,因此如果它想在线程池 TERMINATED 后做些事情,可以利用此方法等待
    boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;
任务调度线程池

在『任务调度线程池』功能加入之前,可以使用 java.util.Timer 来实现定时功能,Timer 的优点在于简单易用,但由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务。

    public static void main(String[] args) {
        Timer timer = new Timer();
        TimerTask task1 = new TimerTask() {
            @Override
            public void run() {
                log.debug("task 1");
                sleep(2);
            }
        };
        TimerTask task2 = new TimerTask() {
            @Override
            public void run() {
                log.debug("task 2");
            }
        };
// 使用 timer 添加两个任务,希望它们都在 1s 后执行
// 但由于 timer 内只有一个线程来顺序执行队列中的任务,因此『任务1』的延时,影响了『任务2』的执行
        timer.schedule(task1, 1000);
        timer.schedule(task2, 1000);
    }

使用 ScheduledExecutorService 改写:

   ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
// 添加两个任务,希望它们都在 1s 后执行
   executor.schedule(() -> {
       System.out.println("任务1,执行时间:" + new Date());
       try {
           Thread.sleep(2000);
       } catch (InterruptedException e) {
       }
   }, 1000, TimeUnit.MILLISECONDS);
   executor.schedule(() -> {
       System.out.println("任务2,执行时间:" + new Date());
   }, 1000, TimeUnit.MILLISECONDS);
正确处理执行任务异常

方法1:主动捉异常
方法2:使用 Future

  ExecutorService pool = Executors.newFixedThreadPool(1);
  Future<Boolean> f = pool.submit(() -> {
      log.debug("task1");
      int i = 1 / 0;
      return true;
  });
  log.debug("result:{}", f.get());

f.get() 会抛异常

Tomcat 线程池

在这里插入图片描述

  • LimitLatch 用来限流,可以控制最大连接个数,类似 J.U.C 中的 Semaphore 后面再讲
  • Acceptor 只负责【接收新的 socket 连接】
  • Poller 只负责监听 socket channel 是否有【可读的 I/O 事件】
  • 一旦可读,封装一个任务对象(socketProcessor),提交给 Executor 线程池处理
  • Executor 线程池中的工作线程最终负责【处理请求】

Tomcat 线程池扩展了 ThreadPoolExecutor,行为稍有不同

  • 如果总线程数达到 maximumPoolSize
    • 这时不会立刻抛 RejectedExecutionException 异常
    • 而是再次尝试将任务放入队列,如果还失败,才抛出 RejectedExecutionException 异常

在这里插入图片描述

Fork/Join

概念

Fork/Join 是 JDK 1.7 加入的新的线程池实现,它体现的是一种分治思想,适用于能够进行任务拆分的 cpu 密集型运算
所谓的任务拆分,是将一个大任务拆分为算法上相同的小任务,直至不能拆分可以直接求解。跟递归相关的一些计算,如归并排序、斐波那契数列、都可以用分治思想进行求解
Fork/Join 在分治的基础上加入了多线程,可以把每个任务的分解和合并交给不同的线程来完成,进一步提升了运算效率
Fork/Join 默认会创建与 cpu 核心数大小相同的线程池

使用

提交给 Fork/Join 线程池的任务需要继承 RecursiveTask(有返回值)或 RecursiveAction(没有返回值),例如下面定义了一个对 1~n 之间的整数求和的任务

    @Slf4j(topic = "c.AddTask")
    class AddTask1 extends RecursiveTask<Integer> {
        int n;

        public AddTask1(int n) {
            this.n = n;
        }

        @Override
        public String toString() {
            return "{" + n + '}';
        }

        @Override
        protected Integer compute() {
// 如果 n 已经为 1,可以求得结果了
            if (n == 1) {
                log.debug("join() {}", n);
                return n;
            }
// 将任务进行拆分(fork)
            AddTask1 t1 = new AddTask1(n - 1);
            t1.fork();
            log.debug("fork() {} + {}", n, t1);
// 合并(join)结果
            int result = n + t1.join();
            log.debug("join() {} + {} = {}", n, t1, result);
            return result;
        }
    }

然后提交给 ForkJoinPool 来执行

    public static void main(String[] args) {
        ForkJoinPool pool = new ForkJoinPool(4);
        System.out.println(pool.invoke(new AddTask1(5)));
    }

在这里插入图片描述
改进

    class AddTask3 extends RecursiveTask<Integer> {
        int begin;
        int end;

        public AddTask3(int begin, int end) {
            this.begin = begin;
            this.end = end;
        }

        @Override
        public String toString() {
            return "{" + begin + "," + end + '}';
        }

        @Override
        protected Integer compute() {
// 5, 5
            if (begin == end) {
                log.debug("join() {}", begin);
                return begin;
            }
// 4, 5
            if (end - begin == 1) {
                log.debug("join() {} + {} = {}", begin, end, end + begin);
                return end + begin;
            }
// 1 5
            int mid = (end + begin) / 2; // 3
            AddTask3 t1 = new AddTask3(begin, mid); // 1,3
            t1.fork();
            AddTask3 t2 = new AddTask3(mid + 1, end); // 4,5
            t2.fork();
            log.debug("fork() {} + {} = ?", t1, t2);
            int result = t1.join() + t2.join();
            log.debug("join() {} + {} = {}", t1, t2, result);
            return result;
        }
    }

在这里插入图片描述

8.2 J.U.C

概述

全称是 AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架
特点:

  • 用 state 属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取锁和释放锁
    • getState - 获取 state 状态
    • setState - 设置 state 状态
    • compareAndSetState - cas 机制设置 state 状态
    • 独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源
  • 提供了基于 FIFO 的等待队列,类似于 Monitor 的 EntryList
  • 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet
    子类主要实现这样一些方法(默认抛出 UnsupportedOperationException)
  • tryAcquire
  • tryRelease
  • tryAcquireShared
  • tryReleaseShared
  • isHeldExclusively

获取锁的姿势

// 如果获取锁失败
if (!tryAcquire(arg)) {
// 入队, 可以选择阻塞当前线程 park unpark
}

释放锁的姿势

// 如果释放锁成功
if (tryRelease(arg)) {
// 让阻塞线程恢复运行
}

实现不可重入锁

自定义同步器

    final class MySync extends AbstractQueuedSynchronizer {
        @Override
        protected boolean tryAcquire(int acquires) {
            if (acquires == 1) {
                if (compareAndSetState(0, 1)) {
                    setExclusiveOwnerThread(Thread.currentThread());
                    return true;
                }
            }
            return false;
        }

        @Override
        protected boolean tryRelease(int acquires) {
            if (acquires == 1) {
                if (getState() == 0) {
                    throw new IllegalMonitorStateException();
                }
                setExclusiveOwnerThread(null);
                setState(0);
                return true;
            }
            return false;
        }

        protected Condition newCondition() {
            return new ConditionObject();
        }

        @Override
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }
    }

自定义锁

有了自定义同步器,很容易复用 AQS ,实现一个功能完备的自定义锁

    class MyLock implements Lock {
        static MySync sync = new MySync();

        @Override
// 尝试,不成功,进入等待队列
        public void lock() {
            sync.acquire(1);
        }

        @Override
// 尝试,不成功,进入等待队列,可打断
        public void lockInterruptibly() throws InterruptedException {
            sync.acquireInterruptibly(1);
        }

        @Override
// 尝试一次,不成功返回,不进入队列
        public boolean tryLock() {
            return sync.tryAcquire(1);
        }

        @Override
// 尝试,不成功,进入等待队列,有时限
        public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
            return sync.tryAcquireNanos(1, unit.toNanos(time));
        }

        @Override
// 释放锁
        public void unlock() {
            sync.release(1);
        }

        @Override
// 生成条件变量
        public Condition newCondition() {
            return sync.newCondition();
        }
    }

心得

起源

早期程序员会自己通过一种同步器去实现另一种相近的同步器,例如用可重入锁去实现信号量,或反之。这显然不够优雅,于是在 JSR166(java 规范提案)中创建了 AQS,提供了这种通用的同步器机制。

目标

AQS 要实现的功能目标

  • 阻塞版本获取锁 acquire 和非阻塞的版本尝试获取锁 tryAcquire
  • 获取锁超时机制
  • 通过打断取消机制
  • 独占机制及共享机制
  • 条件不满足时的等待机制
设计

AQS 的基本思想其实很简单
获取锁的逻辑

  while(state 状态不允许获取) {
      if(队列中还没有此线程) {
          入队并阻塞
      }
  }
  当前线程出队

释放锁的逻辑

if(state 状态允许了) {
	恢复阻塞的线程(s)
}

要点

  • 原子维护 state 状态
  • 阻塞及恢复线程
  • 维护队列
  1. state 设计
  • state 使用 volatile 配合 cas 保证其修改时的原子性
  • state 使用了 32bit int 来维护同步状态,因为当时使用 long 在很多平台下测试的结果并不理想
  1. 阻塞恢复设计
  • 早期的控制线程暂停和恢复的 api 有 suspend 和 resume,但它们是不可用的,因为如果先调用的 resume那么 suspend 将感知不到
  • 解决方法是使用 park & unpark 来实现线程的暂停和恢复,具体原理在之前讲过了,先 unpark 再 park 也没问题
  • park & unpark 是针对线程的,而不是针对同步器的,因此控制粒度更为精细
  • park 线程还可以通过 interrupt 打断
  1. 队列设计
  • 使用了 FIFO 先入先出队列,并不支持优先级队列
  • 设计时借鉴了 CLH 队列,它是一种单向无锁队列
  • FIFO 先进先出 ,Java实现,类似于 Monitor 的 EntryList(C++实现)

队列中有 head 和 tail 两个指针节点,都用 volatile 修饰配合 cas 使用,每个节点有 state 维护节点状态
入队伪代码,只需要考虑 tail 赋值的原子性

        do {
// 原来的 tail
            Node prev = tail;
// 用 cas 在原来 tail 的基础上改为 node
        } while (tail.compareAndSet(prev, node))

出队伪代码

// prev 是上一个节点
        while((Node prev=node.prev).state != 唤醒状态) {
        }
// 设置头节点
        head = node;

CLH 好处:

  • 无锁,使用自旋
  • 快速,无阻塞
    AQS 在一些方面改进了 CLH
    private Node enq(final Node node) {
        for (; ; ) {
            Node t = tail;
// 队列中还没有元素 tail 为 null
            if (t == null) {
// 将 head 从 null -> dummy
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
// 将 node 的 prev 设置为原来的 tail
                node.prev = t;
// 将 tail 从原来的 tail 设置为 node
                if (compareAndSetTail(t, node)) {
// 原来 tail 的 next 设置为 node
                    t.next = node;
                    return t;
                }
            }
        }
    }

主要用到 AQS 的并发工具类
在这里插入图片描述

ReentrantLock 原理

在这里插入图片描述

非公平锁实现原理
加锁解锁流程

先从构造器开始看,默认为非公平锁实现

public ReentrantLock() {
	sync = new NonfairSync();
}

NonfairSync 继承自 AQS
没有竞争时
在这里插入图片描述
第一个竞争出现时
在这里插入图片描述
Thread-1 执行了

  1. CAS 尝试将 state 由 0 改为 1,结果失败
  2. 进入 tryAcquire 逻辑,这时 state 已经是1,结果仍然失败
  3. 接下来进入 addWaiter 逻辑,构造 Node 队列
    • 图中黄色三角表示该 Node 的 waitStatus 状态,其中 0 为默认正常状态
    • Node 的创建是懒惰的
    • 其中第一个 Node 称为 Dummy(哑元)或哨兵,用来占位,并不关联线程

在这里插入图片描述
当前线程进入 acquireQueued 逻辑
4. acquireQueued 会在一个死循环中不断尝试获得锁,失败后进入 park 阻塞
5. 如果自己是紧邻着 head(排第二位),那么再次 tryAcquire 尝试获取锁,当然这时 state 仍为 1,失败
6. 进入 shouldParkAfterFailedAcquire 逻辑,将前驱 node,即 head 的 waitStatus 改为 -1,这次返回 false

在这里插入图片描述
7. shouldParkAfterFailedAcquire 执行完毕回到 acquireQueued ,再次 tryAcquire 尝试获取锁,当然这时
state 仍为 1,失败
8. 当再次进入 shouldParkAfterFailedAcquire 时,这时因为其前驱 node 的 waitStatus 已经是 -1,这次返回
true
9. 进入 parkAndCheckInterrupt, Thread-1 park(灰色表示)

在这里插入图片描述
再次有多个线程经历上述过程竞争失败,变成这个样子在这里插入图片描述
Thread-0 释放锁,进入 tryRelease 流程,如果成功

  • 设置 exclusiveOwnerThread 为 null
  • state = 0
    在这里插入图片描述
    当前队列不为 null,并且 head 的 waitStatus = -1,进入 unparkSuccessor 流程
    找到队列中离 head 最近的一个 Node(没取消的),unpark 恢复其运行,本例中即为 Thread-1
    回到 Thread-1 的 acquireQueued 流程
    在这里插入图片描述
    如果加锁成功(没有竞争),会设置
  • exclusiveOwnerThread 为 Thread-1,state = 1
  • head 指向刚刚 Thread-1 所在的 Node,该 Node 清空 Thread
  • 原本的 head 因为从链表断开,而可被垃圾回收
    如果这时候有其它线程来竞争(非公平的体现),例如这时有 Thread-4 来了
    在这里插入图片描述
    如果不巧又被 Thread-4 占了先
  • Thread-4 被设置为 exclusiveOwnerThread,state = 1
  • Thread-1 再次进入 acquireQueued 流程,获取锁失败,重新进入 park 阻塞
加锁源码
    // Sync 继承自 AQS
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        // 加锁实现
        final void lock() {
// 首先用 cas 尝试(仅尝试一次)将 state 从 0 改为 1, 如果成功表示获得了独占锁
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
// 如果尝试失败,进入 ㈠
                acquire(1);
        }

        // ㈠ AQS 继承过来的方法, 方便阅读, 放在此处
        public final void acquire(int arg) {
// ㈡ tryAcquire
            if (
                    !tryAcquire(arg) &&
// 当 tryAcquire 返回为 false 时, 先调用 addWaiter ㈣, 接着 acquireQueued ㈤
                            acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
            ) {
                selfInterrupt();
            }
        }

        // ㈡ 进入 ㈢
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }

        // ㈢ Sync 继承过来的方法, 方便阅读, 放在此处
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
// 如果还没有获得锁
            if (c == 0) {
// 尝试用 cas 获得, 这里体现了非公平性: 不去检查 AQS 队列
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
// 如果已经获得了锁, 线程还是当前线程, 表示发生了锁重入
            else if (current == getExclusiveOwnerThread()) {
// state++
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
// 获取失败, 回到调用处
            return false;
        }

        // ㈣ AQS 继承过来的方法, 方便阅读, 放在此处
        private Node addWaiter(Node mode) {// 将当前线程关联到一个 Node 对象上, 模式为独占模式
            Node node = new Node(Thread.currentThread(), mode);
// 如果 tail 不为 null, cas 尝试将 Node 对象加入 AQS 队列尾部
            Node pred = tail;
            if (pred != null) {
                node.prev = pred;
                if (compareAndSetTail(pred, node)) {
// 双向链表
                    pred.next = node;
                    return node;
                }
            }
// 尝试将 Node 加入 AQS, 进入 ㈥
            enq(node);
            return node;
        }

        // ㈥ AQS 继承过来的方法, 方便阅读, 放在此处
        private Node enq(final Node node) {
            for (; ; ) {
                Node t = tail;
                if (t == null) {
// 还没有, 设置 head 为哨兵节点(不对应线程,状态为 0)
                    if (compareAndSetHead(new Node())) {
                        tail = head;
                    }
                } else {
// cas 尝试将 Node 对象加入 AQS 队列尾部
                    node.prev = t;
                    if (compareAndSetTail(t, node)) {
                        t.next = node;
                        return t;
                    }
                }
            }
        }

        // ㈤ AQS 继承过来的方法, 方便阅读, 放在此处
        final boolean acquireQueued(final Node node, int arg) {
            boolean failed = true;
            try {
                boolean interrupted = false;
                for (; ; ) {
                    final Node p = node.predecessor();
// 上一个节点是 head, 表示轮到自己(当前线程对应的 node)了, 尝试获取
                    if (p == head && tryAcquire(arg)) {
// 获取成功, 设置自己(当前线程对应的 node)为 head
                        setHead(node);
// 上一个节点 help GC
                        p.next = null;
                        failed = false;
// 返回中断标记 false
                        return interrupted;
                    }
                    if (
// 判断是否应当 park, 进入 ㈦
                            shouldParkAfterFailedAcquire(p, node) &&
// park 等待, 此时 Node 的状态被置为 Node.SIGNAL ㈧
                                    parkAndCheckInterrupt()
                    ) {
                        interrupted = true;
                    }
                }
            } finally {
                if (failed)
                    cancelAcquire(node);
            }
        }

        // ㈦ AQS 继承过来的方法, 方便阅读, 放在此处
        private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 获取上一个节点的状态
            int ws = pred.waitStatus;
            if (ws == Node.SIGNAL) {
// 上一个节点都在阻塞, 那么自己也阻塞好了
                return true;
            }
// > 0 表示取消状态
            if (ws > 0) {
// 上一个节点取消, 那么重构删除前面所有取消的节点, 返回到外层循环重试
                do {
                    node.prev = pred = pred.prev;
                } while (pred.waitStatus > 0);
                pred.next = node;
            } else {
// 这次还没有阻塞
// 但下次如果重试不成功, 则需要阻塞,这时需要设置上一个节点状态为 Node.SIGNAL
                compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
            }
            return false;
        }

        // ㈧ 阻塞当前线程
        private final boolean parkAndCheckInterrupt() {
            LockSupport.park(this);
            return Thread.interrupted();
        }
    }

是否需要 unpark 是由当前节点的前驱节点的 waitStatus == Node.SIGNAL 来决定,而不是本节点的waitStatus 决定

解锁源码
    // Sync 继承自 AQS
    static final class NonfairSync extends Sync {
        // 解锁实现
        public void unlock() {
            sync.release(1);
        }

        // AQS 继承过来的方法, 方便阅读, 放在此处
        public final boolean release(int arg) {
// 尝试释放锁, 进入 ㈠
            if (tryRelease(arg)) {
// 队列头节点 unpark
                Node h = head;
                if (
// 队列不为 null
                        h != null &&
// waitStatus == Node.SIGNAL 才需要 unpark
                                h.waitStatus != 0
                ) {
// unpark AQS 中等待的线程, 进入 ㈡
                    unparkSuccessor(h);
                }
                return true;
            }
            return false;
        }

        // ㈠ Sync 继承过来的方法, 方便阅读, 放在此处
        protected final boolean tryRelease(int releases) {
// state--
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
// 支持锁重入, 只有 state 减为 0, 才释放成功
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

        // ㈡ AQS 继承过来的方法, 方便阅读, 放在此处
        private void unparkSuccessor(Node node) {
// 如果状态为 Node.SIGNAL 尝试重置状态为 0
// 不成功也可以
            int ws = node.waitStatus;
            if (ws < 0) {
                compareAndSetWaitStatus(node, ws, 0);
            }
// 找到需要 unpark 的节点, 但本节点从 AQS 队列中脱离, 是由唤醒节点完成的
            Node s = node.next;
// 不考虑已取消的节点, 从 AQS 队列从后至前找到队列最前面需要 unpark 的节点
            if (s == null || s.waitStatus > 0) {
                s = null;
                for (Node t = tail; t != null && t != node; t = t.prev)
                    if (t.waitStatus <= 0)
                        s = t;
            }
            if (s != null)
                LockSupport.unpark(s.thread);
        }
    }
可重入原理
    static final class NonfairSync extends Sync {
        // ...
// Sync 继承过来的方法, 方便阅读, 放在此处
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
// 如果已经获得了锁, 线程还是当前线程, 表示发生了锁重入
            else if (current == getExclusiveOwnerThread()) {
// state++
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

        // Sync 继承过来的方法, 方便阅读, 放在此处
        protected final boolean tryRelease(int releases) {
// state--
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
// 支持锁重入, 只有 state 减为 0, 才释放成功
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }
    }
可打断原理
不可打断模式

在此模式下,即使它被打断,仍会驻留在 AQS 队列中,一直要等到获得锁后方能得知自己被打断了

    // Sync 继承自 AQS
    static final class NonfairSync extends Sync {
        // ...
        private final boolean parkAndCheckInterrupt() {
// 如果打断标记已经是 true, 则 park 会失效
            LockSupport.park(this);
// interrupted 会清除打断标记
            return Thread.interrupted();
        }

        final boolean acquireQueued(final Node node, int arg) {
            boolean failed = true;
            try {
                boolean interrupted = false;
                for (; ; ) {
                    final Node p = node.predecessor();
                    if (p == head && tryAcquire(arg)) {
                        setHead(node);
                        p.next = null;
                        failed = false;
// 还是需要获得锁后, 才能返回打断状态
                        return interrupted;
                    }
                    if (
                            shouldParkAfterFailedAcquire(p, node) &&
                                    parkAndCheckInterrupt()
                    ) {
// 如果是因为 interrupt 被唤醒, 返回打断状态为 true
                        interrupted = true;
                    }
                }
            } finally {
                if (failed)
                    cancelAcquire(node);
            }
        }

        public final void acquire(int arg) {
            if (
                    !tryAcquire(arg) &&
                            acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
            ) {
// 如果打断状态为 true
                selfInterrupt();
            }
        }

        static void selfInterrupt() {
// 重新产生一次中断
            Thread.currentThread().interrupt();
        }
    }
可打断模式
    static final class NonfairSync extends Sync {
        public final void acquireInterruptibly(int arg) throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
// 如果没有获得到锁, 进入 ㈠
            if (!tryAcquire(arg))
                doAcquireInterruptibly(arg);
        }

        // ㈠ 可打断的获取锁流程
        private void doAcquireInterruptibly(int arg) throws InterruptedException {
            final Node node = addWaiter(Node.EXCLUSIVE);
            boolean failed = true;
            try {
                for (; ; ) {
                    final Node p = node.predecessor();
                    if (p == head && tryAcquire(arg)) {
                        setHead(node);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                    if (shouldParkAfterFailedAcquire(p, node) &&
                            parkAndCheckInterrupt()) {
// 在 park 过程中如果被 interrupt 会进入此
// 这时候抛出异常, 而不会再次进入 for (;;)
                        throw new InterruptedException();
                    }
                }
            } finally {
                if (failed)
                    cancelAcquire(node);
            }
        }
    }
公平锁实现原理
    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);
        }

        // AQS 继承过来的方法, 方便阅读, 放在此处
        public final void acquire(int arg) {
            if (
                    !tryAcquire(arg) &&
                            acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
            ) {
                selfInterrupt();
            }
        }

        // 与非公平锁主要区别在于 tryAcquire 方法的实现
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
// 先检查 AQS 队列中是否有前驱节点, 没有才去竞争
                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;
        }

        // ㈠ AQS 继承过来的方法, 方便阅读, 放在此处
        public final boolean hasQueuedPredecessors() {
            Node t = tail;
            Node h = head;
            Node s;
// h != t 时表示队列中有 Node
            return h != t &&
                    (
// (s = h.next) == null 表示队列中还有没有老二
                            (s = h.next) == null ||// 或者队列中老二线程不是此线程
                                    s.thread != Thread.currentThread()
                    );
        }
    }
条件变量实现原理

每个条件变量其实就对应着一个等待队列,其实现类是 ConditionObject

await 流程

开始 Thread-0 持有锁,调用 await,进入 ConditionObject 的 addConditionWaiter 流程
创建新的 Node 状态为 -2(Node.CONDITION),关联 Thread-0,加入等待队列尾部
在这里插入图片描述
接下来进入 AQS 的 fullyRelease 流程,释放同步器上的锁
在这里插入图片描述
unpark AQS 队列中的下一个节点,竞争锁,假设没有其他竞争线程,那么 Thread-1 竞争成功
在这里插入图片描述
park 阻塞 Thread-0
在这里插入图片描述

signal 流程

假设 Thread-1 要来唤醒 Thread-0
在这里插入图片描述
进入 ConditionObject 的 doSignal 流程,取得等待队列中第一个 Node,即 Thread-0 所在 Node
在这里插入图片描述
执行 transferForSignal 流程,将该 Node 加入 AQS 队列尾部,将 Thread-0 的 waitStatus 改为 0,Thread-3 的waitStatus 改为 -1
在这里插入图片描述
Thread-1 释放锁,进入 unlock 流程,略

源码
    public class ConditionObject implements Condition, java.io.Serializable {
        private static final long serialVersionUID = 1173984872572414699L;
        // 第一个等待节点
        private transient Node firstWaiter;
        // 最后一个等待节点
        private transient Node lastWaiter;

        public ConditionObject() {
        }

        // ㈠ 添加一个 Node 至等待队列
        private Node addConditionWaiter() {
            Node t = lastWaiter;
// 所有已取消的 Node 从队列链表删除, 见 ㈡
            if (t != null && t.waitStatus != Node.CONDITION) {
                unlinkCancelledWaiters();
                t = lastWaiter;
            }
// 创建一个关联当前线程的新 Node, 添加至队列尾部
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            if (t == null)
                firstWaiter = node;
            else
                t.nextWaiter = node;
            lastWaiter = node;
            return node;
        }

        // 唤醒 - 将没取消的第一个节点转移至 AQS 队列
        private void doSignal(Node first) {
            do {
// 已经是尾节点了
                if ((firstWaiter = first.nextWaiter) == null) {
                    lastWaiter = null;
                }
                first.nextWaiter = null;
            } while (
// 将等待队列中的 Node 转移至 AQS 队列, 不成功且还有节点则继续循环 ㈢
                    !transferForSignal(first) &&
// 队列还有节点
                            (first = firstWaiter) != null
            );
        }

        // 外部类方法, 方便阅读, 放在此处
// ㈢ 如果节点状态是取消, 返回 false 表示转移失败, 否则转移成功
        final boolean transferForSignal(Node node) {
// 如果状态已经不是 Node.CONDITION, 说明被取消了
            if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
                return false;
// 加入 AQS 队列尾部
            Node p = enq(node);
            int ws = p.waitStatus;
            if (
// 上一个节点被取消
                    ws > 0 ||
// 上一个节点不能设置状态为 Node.SIGNAL
                            !compareAndSetWaitStatus(p, ws, Node.SIGNAL)
            ) {
// unpark 取消阻塞, 让线程重新同步状态
                LockSupport.unpark(node.thread);
            }
            return true;
        }

        // 全部唤醒 - 等待队列的所有节点转移至 AQS 队列
        private void doSignalAll(Node first) {
            lastWaiter = firstWaiter = null;
            do {
                Node next = first.nextWaiter;
                first.nextWaiter = null;
                transferForSignal(first);
                first = next;
            } while (first != null);
        }

        // ㈡
        private void unlinkCancelledWaiters() {
// ...
        }

        // 唤醒 - 必须持有锁才能唤醒, 因此 doSignal 内无需考虑加锁
        public final void signal() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignal(first);
        }

        // 全部唤醒 - 必须持有锁才能唤醒, 因此 doSignalAll 内无需考虑加锁
        public final void signalAll() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignalAll(first);
        }

        // 不可打断等待 - 直到被唤醒
        public final void awaitUninterruptibly() {
// 添加一个 Node 至等待队列, 见 ㈠
            Node node = addConditionWaiter();
// 释放节点持有的锁, 见 ㈣
            int savedState = fullyRelease(node);
            boolean interrupted = false;
// 如果该节点还没有转移至 AQS 队列, 阻塞
            while (!isOnSyncQueue(node)) {
// park 阻塞
                LockSupport.park(this);
// 如果被打断, 仅设置打断状态
                if (Thread.interrupted())
                    interrupted = true;
            }
// 唤醒后, 尝试竞争锁, 如果失败进入 AQS 队列
            if (acquireQueued(node, savedState) || interrupted)
                selfInterrupt();
        }

        // 外部类方法, 方便阅读, 放在此处
// ㈣ 因为某线程可能重入,需要将 state 全部释放
        final int fullyRelease(Node node) {
            boolean failed = true;
            try {
                int savedState = getState();
                if (release(savedState)) {
                    failed = false;
                    return savedState;
                } else {
                    throw new IllegalMonitorStateException();
                }
            } finally {
                if (failed)
                    node.waitStatus = Node.CANCELLED;
            }
        }

        // 打断模式 - 在退出等待时重新设置打断状态
        private static final int REINTERRUPT = 1;
        // 打断模式 - 在退出等待时抛出异常
        private static final int THROW_IE = -1;

        // 判断打断模式
        private int checkInterruptWhileWaiting(Node node) {
            return Thread.interrupted() ?
                    (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
                    0;
        }

        // ㈤ 应用打断模式
        private void reportInterruptAfterWait(int interruptMode)
                throws InterruptedException {
            if (interruptMode == THROW_IE)
                throw new InterruptedException();
            else if (interruptMode == REINTERRUPT)
                selfInterrupt();
        }

        // 等待 - 直到被唤醒或打断
        public final void await() throws InterruptedException {
            if (Thread.interrupted()) {
                throw new InterruptedException();
            }
// 添加一个 Node 至等待队列, 见 ㈠
            Node node = addConditionWaiter();
// 释放节点持有的锁
            int savedState = fullyRelease(node);
            int interruptMode = 0;
// 如果该节点还没有转移至 AQS 队列, 阻塞
            while (!isOnSyncQueue(node)) {
// park 阻塞
                LockSupport.park(this);
                // 如果被打断, 退出等待队列
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
// 退出等待队列后, 还需要获得 AQS 队列的锁
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
// 所有已取消的 Node 从队列链表删除, 见 ㈡
            if (node.nextWaiter != null)
                unlinkCancelledWaiters();
// 应用打断模式, 见 ㈤
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

        // 等待 - 直到被唤醒或打断或超时
        public final long awaitNanos(long nanosTimeout) throws InterruptedException {
            if (Thread.interrupted()) {
                throw new InterruptedException();
            }
// 添加一个 Node 至等待队列, 见 ㈠
            Node node = addConditionWaiter();
// 释放节点持有的锁
            int savedState = fullyRelease(node);
// 获得最后期限
            final long deadline = System.nanoTime() + nanosTimeout;
            int interruptMode = 0;
// 如果该节点还没有转移至 AQS 队列, 阻塞
            while (!isOnSyncQueue(node)) {
// 已超时, 退出等待队列
                if (nanosTimeout <= 0L) {
                    transferAfterCancelledWait(node);
                    break;
                }
// park 阻塞一定时间, spinForTimeoutThreshold 为 1000 ns
                if (nanosTimeout >= spinForTimeoutThreshold)
                    LockSupport.parkNanos(this, nanosTimeout);
// 如果被打断, 退出等待队列
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
                nanosTimeout = deadline - System.nanoTime();
            }
// 退出等待队列后, 还需要获得 AQS 队列的锁
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
// 所有已取消的 Node 从队列链表删除, 见 ㈡
            if (node.nextWaiter != null)
                unlinkCancelledWaiters();
// 应用打断模式, 见 ㈤
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
            return deadline - System.nanoTime();
        }

        // 等待 - 直到被唤醒或打断或超时, 逻辑类似于 awaitNanos
        public final boolean awaitUntil(Date deadline) throws InterruptedException {
// ...
        }

        // 等待 - 直到被唤醒或打断或超时, 逻辑类似于 awaitNanos
        public final boolean await(long time, TimeUnit unit) throws InterruptedException {
// ...
        }
// 工具方法 省略 ...
    }

tryAcquire 二块二

should 应该 Park 停放 After 之后 Failed 失败 Acquire 获得
shouldParkAfterFailedAcquire
瘦的 帕克 阿福特 菲儿的 二块二

AQS原理:
用了一个int state变量,通过cas改0,1来表示是否加锁
还有一个先进先出的队列 Node(里面有线程对象地址、前驱后继节点地址、state) ,实际就是用了一个 head和tail,把所有node连起来。
还有一个owner来存当前获取锁的thread

以ReentrantLock为例:
调用lock时,先cas修改sync的state,成功则把owner改为当前线程
失败则进入tryAcquire,tryAcquire调用nonfairTryAcquire,再次获取state状态,如果为0则其他线程已释放锁,再次cas修改state(这是非公平锁逻辑,如果是公平锁则需要判断AQS队列是否有node等待,如果有则加到队列尾部等待)。
这次修改成功则改owner,返回true
失败则继续判断owner是否为当前线程,如果是,state+1,表示锁重入,返回true
以上判断都不进入则返回false;

回到tryAcquire调用处,前面加了!,如果返回false则会继续判断&&后面的acquireQueued,acquireQueued之前还需要执行addWaiter,去创建AQS队列

先创建一个node,把当前线程内存地址通过构造方法存入node。
判断sync的pred是否为空,如果不为空,说明之前已经有其他线程创建过队列了,把新建的node加入尾部,原先的尾节点的后继节点改为这个node,然后返回node。

如果为空,则进入enq,里面是一个死循环,再次判断tail节点是否为空,还是为空,则cas创建node(这个node不关联线程,只做唤醒后继节点用)并修改头尾节点为都新node,进入下一个循环,此时tail已经不为空,尝试将当前线程的node改为tail节点,并把原先的tail节点的next改为当前node,return退出循环

enq结束,return node;addWaiter方法结束。将node传入acquireQueued方法。

acquireQueued里面又是一个死循环,先拿到node的前驱节点,如果前驱节点是head,则表示当前node是老二,可以被唤醒,尝试一次tryAcquire,如果成功获取锁,则把当前node改为head,原先的head断开进入GC。return退出循环

如果node前驱节点不是head,则进入shouldParkAfterFailedAcquire方法(传前驱节点和当前节点)(翻译的意思是,在获得锁失败后是否应该执行park),判断是否应该进入阻塞。

进入shouldParkAfterFailedAcquire,先获取前驱节点的状态,如果==-1(-1表示这个节点负责唤醒下一个节点),则直接返回true,shouldParkAfterFailedAcquire方法后面还有个&& parkAndCheckInterrupt方法用来park当前线程的,等下再说。

如果前驱node的status>0(status == 1 表示这个node线程被取消,可能是Interrupt或者别的什么原因),则do while 去删除前面所有>0的node,执行完return false ,进入上层方法的循环重试

如果前驱node==0则把前驱node的status改为-1,return false ,进入上层方法的循环重试

回到acquireQueued方法,当shouldParkAfterFailedAcquire返回true时继续执行&& 后的parkAndCheckInterrupt,执行当前线程的park,并return Thread.interrupted(); 返回打断状态,并清除标记。如果返回true,则说明当前线程是因为被interrupte才唤醒的,则把acquireQueued方法中的interrupted变量改为true,表示这个线程是被打断过的(当然,此时线程已经进入park阻塞了)
其他在shouldParkAfterFailedAcquire返回false的则不执行parkAndCheckInterrupt,重新进入循环。

再次检查前驱node,如果还不是head,则继续shouldParkAfterFailedAcquire,但此时前驱节点的status已经是-1,那么当前node线程执行park阻塞。

至此,加锁流程结束。

解锁流程:

调用unlock,执行sync.release(1);

进入tryRelease尝试释放锁

把state-1 不需要cas,因为只有拿到锁的可以执行,如果当前线程不等于owner线程则throw new IllegalMonitorStateException();

如果是锁重入,代码执行完毕会逐个unlock,直至state-成0,则改owner为null,并返回true,没减成0时,返回的是false。

回到release方法,当tryRelease返回true,则拿到头节点,如果头节点不是null,且status不是0,则进入unparkSuccessor

进入unparkSuccessor,如果node的status<0,则cas状态为0,拿到后继节点,如果后继节点是null,或者状态>0,则从尾节点从后向前找,找到队列最前面需要 unpark 的节点。去unpark

3. 读写锁

3.1 ReentrantReadWriteLock

当读操作远远高于写操作时,这时候使用 读写锁 让 读-读 可以并发,提高性能。 类似于数据库中的 select …from … lock in share mode
提供一个 数据容器类 内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法

    class DataContainer {
        private Object data;
        private ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
        private ReentrantReadWriteLock.ReadLock r = rw.readLock();
        private ReentrantReadWriteLock.WriteLock w = rw.writeLock();

        public Object read() {
            log.debug("获取读锁...");
            r.lock();
            try {
                log.debug("读取");
                sleep(1);
                return data;
            } finally {
                log.debug("释放读锁...");
                r.unlock();
            }
        }

        public void write() {
            log.debug("获取写锁...");
            w.lock();
            try {
                log.debug("写入");
                sleep(1);
            } finally {
                log.debug("释放写锁...");
                w.unlock();
            }
        }
    }

读锁-读锁 可以并发,读锁-写锁 相互阻塞,写锁-写锁 也是相互阻塞的

注意事项

  • 读锁不支持条件变量
  • 重入时升级不支持:即持有读锁的情况下去获取写锁,会导致获取写锁永久等待
  • 重入时降级支持:即持有写锁的情况下去获取读锁
    class CachedData {
        Object data;
        // 是否有效,如果失效,需要重新计算 data
        volatile boolean cacheValid;
        final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

        void processCachedData() {
            rwl.readLock().lock();
            if (!cacheValid) {
// 获取写锁前必须释放读锁
                rwl.readLock().unlock();
                rwl.writeLock().lock();
                try {
// 判断是否有其它线程已经获取了写锁、更新了缓存, 避免重复更新
                    if (!cacheValid) {
                        data = ...
                        cacheValid = true;
                    }
// 降级为读锁, 释放写锁, 这样能够让其它线程读取缓存
                    rwl.readLock().lock();
                } finally {
                    rwl.writeLock().unlock();
                }
            }
// 自己用完数据, 释放读锁
            try {
                use(data);
            } finally {
                rwl.readLock().unlock();
            }
        }
    }
读写锁原理
1. 图解流程

读写锁用的是同一个 Sycn 同步器,因此等待队列、state 等也是同一个

t1 w.lock,t2 r.lock

1) t1 成功上锁,流程与 ReentrantLock 加锁相比没有特殊之处,不同是写锁状态占了 state 的低 16 位,而读锁使用的是 state 的高 16 位
在这里插入图片描述
2)t2 执行 r.lock,这时进入读锁的 sync.acquireShared(1) 流程,首先会进入 tryAcquireShared 流程。如果有写锁占据,那么 tryAcquireShared 返回 -1 表示失败

tryAcquireShared 返回值表示

  • -1 表示失败
  • 0 表示成功,但后继节点不会继续唤醒
  • 正数表示成功,而且数值是还有几个后继节点需要唤醒,读写锁返回 1

在这里插入图片描述
3)这时会进入 sync.doAcquireShared(1) 流程,首先也是调用 addWaiter 添加节点,不同之处在于节点被设置为Node.SHARED 模式而非 Node.EXCLUSIVE 模式,注意此时 t2 仍处于活跃状态
在这里插入图片描述
4)t2 会看看自己的节点是不是老二,如果是,还会再次调用 tryAcquireShared(1) 来尝试获取锁
5)如果没有成功,在 doAcquireShared 内 for (;😉 循环一次,把前驱节点的 waitStatus 改为 -1,再 for (;😉 循环一次尝试 tryAcquireShared(1) 如果还不成功,那么在 parkAndCheckInterrupt() 处 park
在这里插入图片描述

t3 r.lock,t4 w.lock

这种状态下,假设又有 t3 加读锁和 t4 加写锁,这期间 t1 仍然持有锁,就变成了下面的样子
在这里插入图片描述

t1 w.unlock

这时会走到写锁的 sync.release(1) 流程,调用 sync.tryRelease(1) 成功,变成下面的样子
在这里插入图片描述
接下来执行唤醒流程 sync.unparkSuccessor,即让老二恢复运行,这时 t2 在 doAcquireShared 内parkAndCheckInterrupt() 处恢复运行
这回再来一次 for (;😉 执行 tryAcquireShared 成功则让读锁计数加一
在这里插入图片描述
这时 t2 已经恢复运行,接下来 t2 调用 setHeadAndPropagate(node, 1),它原本所在节点被置为头节点
在这里插入图片描述
事情还没完,在 setHeadAndPropagate 方法内还会检查下一个节点是否是 shared,如果是则调用doReleaseShared() 将 head 的状态从 -1 改为 0 并唤醒老二,这时 t3 在 doAcquireShared 内parkAndCheckInterrupt() 处恢复运行
在这里插入图片描述
这回再来一次 for (;😉 执行 tryAcquireShared 成功则让读锁计数加一
在这里插入图片描述
这时 t3 已经恢复运行,接下来 t3 调用 setHeadAndPropagate(node, 1),它原本所在节点被置为头节点
在这里插入图片描述
下一个节点不是 shared 了,因此不会继续唤醒 t4 所在节点

t2 r.unlock,t3 r.unlock

t2 进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让计数减一,但由于计数还不为零
在这里插入图片描述
t3 进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让计数减一,这回计数为零了,进入
doReleaseShared() 将头节点从 -1 改为 0 并唤醒老二,即
在这里插入图片描述
之后 t4 在 acquireQueued 中 parkAndCheckInterrupt 处恢复运行,再次 for (;😉 这次自己是老二,并且没有其他竞争,tryAcquire(1) 成功,修改头结点,流程结束
在这里插入图片描述

2. 源码分析
写锁上锁流程
    static final class NonfairSync extends Sync {
        // ... 省略无关代码
// 外部类 WriteLock 方法, 方便阅读, 放在此处
        public void lock() {
            sync.acquire(1);
        }

        // AQS 继承过来的方法, 方便阅读, 放在此处
        public final void acquire(int arg) {
            if (
// 尝试获得写锁失败
                    !tryAcquire(arg) &&
// 将当前线程关联到一个 Node 对象上, 模式为独占模式
// 进入 AQS 队列阻塞
                            acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
            ) {
                selfInterrupt();
            }
        }

        // Sync 继承过来的方法, 方便阅读, 放在此处
        protected final boolean tryAcquire(int acquires) {
// 获得低 16 位, 代表写锁的 state 计数
            Thread current = Thread.currentThread();
            int c = getState();
            int w = exclusiveCount(c);
            if (c != 0) {
                if (
// c != 0 and w == 0 表示有读锁, 或者
                        w == 0 ||
// 如果 exclusiveOwnerThread 不是自己
                                current != getExclusiveOwnerThread()
                ) {
// 获得锁失败
                    return false;
                }
// 写锁计数超过低 16 位, 报异常
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
// 写锁重入, 获得锁成功
                setState(c + acquires);
                return true;
            }
            if (
// 判断写锁是否该阻塞, 或者
                    writerShouldBlock() ||
// 尝试更改计数失败
                            !compareAndSetState(c, c + acquires)
            ) {
// 获得锁失败
                return false;
            }
// 获得锁成功
            setExclusiveOwnerThread(current);
            return true;
        }

        // 非公平锁 writerShouldBlock 总是返回 false, 无需阻塞
        final boolean writerShouldBlock() {
            return false;
        }
    }
写锁释放流程
    static final class NonfairSync extends Sync {
        // ... 省略无关代码
// WriteLock 方法, 方便阅读, 放在此处
        public void unlock() {
            sync.release(1);
        }

        // AQS 继承过来的方法, 方便阅读, 放在此处
        public final boolean release(int arg) {
// 尝试释放写锁成功
            if (tryRelease(arg)) {
// unpark AQS 中等待的线程
                Node h = head;
                if (h != null && h.waitStatus != 0)
                    unparkSuccessor(h);
                return true;
            }
            return false;
        }

        // Sync 继承过来的方法, 方便阅读, 放在此处
        protected final boolean tryRelease(int releases) {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            int nextc = getState() - releases;
// 因为可重入的原因, 写锁计数为 0, 才算释放成功
            boolean free = exclusiveCount(nextc) == 0;
            if (free) {
                setExclusiveOwnerThread(null);
            }
            setState(nextc);
            return free;
        }
    }
读锁上锁流程
    static final class NonfairSync extends Sync {
        // ReadLock 方法, 方便阅读, 放在此处
        public void lock() {
            sync.acquireShared(1);
        }

        // AQS 继承过来的方法, 方便阅读, 放在此处
        public final void acquireShared(int arg) {
// tryAcquireShared 返回负数, 表示获取读锁失败
            if (tryAcquireShared(arg) < 0) {
                doAcquireShared(arg);
            }
        }

        // Sync 继承过来的方法, 方便阅读, 放在此处
        protected final int tryAcquireShared(int unused) {
            Thread current = Thread.currentThread();
            int c = getState();
// 如果是其它线程持有写锁, 获取读锁失败
            if (
                    exclusiveCount(c) != 0 &&
                            getExclusiveOwnerThread() != current
            ) {
                return -1;
            }
            int r = sharedCount(c);
            if (
// 读锁不该阻塞(如果老二是写锁,读锁该阻塞), 并且
                    !readerShouldBlock() &&
// 小于读锁计数, 并且
                            r < MAX_COUNT &&
// 尝试增加计数成功
                            compareAndSetState(c, c + SHARED_UNIT)
            ) {
// ... 省略不重要的代码
                return 1;
            }
            return fullTryAcquireShared(current);
        }

        // 非公平锁 readerShouldBlock 看 AQS 队列中第一个节点是否是写锁
// true 则该阻塞, false 则不阻塞
        final boolean readerShouldBlock() {
            return apparentlyFirstQueuedIsExclusive();
        }

        // AQS 继承过来的方法, 方便阅读, 放在此处
// 与 tryAcquireShared 功能类似, 但会不断尝试 for (;;) 获取读锁, 执行过程中无阻塞
        final int fullTryAcquireShared(Thread current) {
            HoldCounter rh = null;
            for (; ; ) {
                int c = getState();
                if (exclusiveCount(c) != 0) {
                    if (getExclusiveOwnerThread() != current)
                        return -1;
                } else if (readerShouldBlock()) {
// ... 省略不重要的代码
                }
                if (sharedCount(c) == MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                if (compareAndSetState(c, c + SHARED_UNIT)) {
// ... 省略不重要的代码
                    return 1;
                }
            }
        }

        // AQS 继承过来的方法, 方便阅读, 放在此处
        private void doAcquireShared(int arg) {
// 将当前线程关联到一个 Node 对象上, 模式为共享模式
            final Node node = addWaiter(Node.SHARED);
            boolean failed = true;
            try {
                boolean interrupted = false;
                for (; ; ) {
                    final Node p = node.predecessor();
                    if (p == head) {// 再一次尝试获取读锁
                        int r = tryAcquireShared(arg);
// 成功
                        if (r >= 0) {
// ㈠
// r 表示可用资源数, 在这里总是 1 允许传播
//(唤醒 AQS 中下一个 Share 节点)
                            setHeadAndPropagate(node, r);
                            p.next = null; // help GC
                            if (interrupted)
                                selfInterrupt();
                            failed = false;
                            return;
                        }
                    }
                    if (
// 是否在获取读锁失败时阻塞(前一个阶段 waitStatus == Node.SIGNAL)
                            shouldParkAfterFailedAcquire(p, node) &&
// park 当前线程
                                    parkAndCheckInterrupt()
                    ) {
                        interrupted = true;
                    }
                }
            } finally {
                if (failed)
                    cancelAcquire(node);
            }
        }

        // ㈠ AQS 继承过来的方法, 方便阅读, 放在此处
        private void setHeadAndPropagate(Node node, int propagate) {
            Node h = head; // Record old head for check below
// 设置自己为 head
            setHead(node);
// propagate 表示有共享资源(例如共享读锁或信号量)
// 原 head waitStatus == Node.SIGNAL 或 Node.PROPAGATE
// 现在 head waitStatus == Node.SIGNAL 或 Node.PROPAGATE
            if (propagate > 0 || h == null || h.waitStatus < 0 ||
                    (h = head) == null || h.waitStatus < 0) {
                Node s = node.next;
// 如果是最后一个节点或者是等待共享读锁的节点
                if (s == null || s.isShared()) {
// 进入 ㈡
                    doReleaseShared();
                }
            }
        }

        // ㈡ AQS 继承过来的方法, 方便阅读, 放在此处
        private void doReleaseShared() {
// 如果 head.waitStatus == Node.SIGNAL ==> 0 成功, 下一个节点 unpark
            // 如果 head.waitStatus == 0 ==> Node.PROPAGATE, 为了解决 bug, 见后面分析
            for (; ; ) {
                Node h = head;
// 队列还有节点
                if (h != null && h != tail) {
                    int ws = h.waitStatus;
                    if (ws == Node.SIGNAL) {
                        if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                            continue; // loop to recheck cases
// 下一个节点 unpark 如果成功获取读锁
// 并且下下个节点还是 shared, 继续 doReleaseShared
                        unparkSuccessor(h);
                    } else if (ws == 0 &&
                            !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                        continue; // loop on failed CAS
                }
                if (h == head) // loop if head changed
                    break;
            }
        }
    }
读锁释放流程
    static final class NonfairSync extends Sync {
        // ReadLock 方法, 方便阅读, 放在此处
        public void unlock() {
            sync.releaseShared(1);
        }

        // AQS 继承过来的方法, 方便阅读, 放在此处
        public final boolean releaseShared(int arg) {
            if (tryReleaseShared(arg)) {
                doReleaseShared();
                return true;
            }
            return false;
        }

        // Sync 继承过来的方法, 方便阅读, 放在此处
        protected final boolean tryReleaseShared(int unused) {
// ... 省略不重要的代码
            for (; ; ) {
                int c = getState();
                int nextc = c - SHARED_UNIT;
                if (compareAndSetState(c, nextc)) {
// 读锁的计数不会影响其它获取读锁线程, 但会影响其它获取写锁线程
// 计数为 0 才是真正释放
                    return nextc == 0;
                }
            }
        }

        // AQS 继承过来的方法, 方便阅读, 放在此处
        private void doReleaseShared() {
// 如果 head.waitStatus == Node.SIGNAL ==> 0 成功, 下一个节点 unpark
// 如果 head.waitStatus == 0 ==> Node.PROPAGATE
            for (; ; ) {
                Node h = head;
                if (h != null && h != tail) {
                    int ws = h.waitStatus;
// 如果有其它线程也在释放读锁,那么需要将 waitStatus 先改为 0
// 防止 unparkSuccessor 被多次执行
                    if (ws == Node.SIGNAL) {
                        if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                            continue; // loop to recheck cases
                        unparkSuccessor(h);
                    }
// 如果已经是 0 了,改为 -3,用来解决传播性,见后文信号量 bug 分析
                    else if (ws == 0 &&
                            !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                        continue; // loop on failed CAS
                }
                if (h == head) // loop if head changed
                    break;
            }
        }
    }

读写锁也有公平非公平

读写锁,先从写锁的lock.lock开始,如果状态不等于0,则判断低16位,再判断是不是锁重入,16位int最大值65535,超过抛异常
加锁成功就state从0改成1

进入加读锁状态,先判断写锁是不是不为0,在判断写锁是不是自己,如果是,可以锁降级。失败返回-1,成功返回1
加读锁添加的node类型 SHARED共享类型 不是独占类型了

解锁,读锁解锁会循环唤醒下一个读锁,state的高位+1,

遇到Ex独占节点,就不会继续了

读读可以并发就是因为只要唤醒读节点,后面连着的读节点都会唤醒,直到遇到一个独占节点

读锁解锁,就把高位-1,-到0就唤醒 ,读锁获得锁,owner是不会有线程记录的。

3.2 StampedLock

该类自 JDK 8 加入,是为了进一步优化读性能,它的特点是在使用读锁、写锁时都必须配合【戳】使用

StampedLock作者 大哥李
读写锁读读并发还是不够快,每次都要用cas修改state,性能比不上不加锁

加解读锁

long stamp = lock.readLock();
lock.unlockRead(stamp);

加解写锁

long stamp = lock.writeLock();
lock.unlockWrite(stamp);

乐观读,StampedLock 支持 tryOptimisticRead() 方法(乐观读),读取完毕后需要做一次 戳校验 如果校验通过,表示这期间确实没有写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据安全。

long stamp = lock.tryOptimisticRead();
// 验戳
if(!lock.validate(stamp)){
// 锁升级
}

提供一个 数据容器类 内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法

    class DataContainerStamped {
        private int data;
        private final StampedLock lock = new StampedLock();

        public DataContainerStamped(int data) {
            this.data = data;
        }

        public int read(int readTime) {
            long stamp = lock.tryOptimisticRead();
            log.debug("optimistic read locking...{}", stamp);
            sleep(readTime);
            if (lock.validate(stamp)) {
                log.debug("read finish...{}, data:{}", stamp, data);
                return data;
            }
// 锁升级 - 读锁
            log.debug("updating to read lock... {}", stamp);
            try {
                stamp = lock.readLock();
                log.debug("read lock {}", stamp);
                sleep(readTime);
                log.debug("read finish...{}, data:{}", stamp, data);
                return data;
            } finally {
                log.debug("read unlock {}", stamp);
                lock.unlockRead(stamp);
            }
        }

        public void write(int newData) {
            long stamp = lock.writeLock();
            log.debug("write lock {}", stamp);
            try {
                sleep(2);
                this.data = newData;
            } finally {
                log.debug("write unlock {}", stamp);
                lock.unlockWrite(stamp);
            }
        }
    }

4. Semaphore

信号量,用来限制能同时访问共享资源的线程上限。

    public static void main(String[] args) {
// 1. 创建 semaphore 对象
        Semaphore semaphore = new Semaphore(3);
// 2. 10个线程同时运行
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
// 3. 获取许可
                try {
                    semaphore.acquire();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                try {
                    log.debug("running...");
                    sleep(1);
                    log.debug("end...");
                } finally {
// 4. 释放许可
                    semaphore.release();
                }
            }).start();
        }
    }
Semaphore 原理
1. 加锁解锁流程

Semaphore 有点像一个停车场,permits 就好像停车位数量,当线程获得了 permits 就像是获得了停车位,然后停车场显示空余车位减一
刚开始,permits(state)为 3,这时 5 个线程来获取资源

在这里插入图片描述
假设其中 Thread-1,Thread-2,Thread-4 cas 竞争成功,而 Thread-0 和 Thread-3 竞争失败,进入 AQS 队列park 阻塞
在这里插入图片描述
这时 Thread-4 释放了 permits,状态如下
在这里插入图片描述
接下来 Thread-0 竞争成功,permits 再次设置为 0,设置自己为 head 节点,断开原来的 head 节点,unpark 接下来的 Thread-3 节点,但由于 permits 是 0,因此 Thread-3 在尝试不成功后再次进入 park 状态
在这里插入图片描述

2. 源码分析
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = -2694183684443567898L;

        NonfairSync(int permits) {
// permits 即 state
            super(permits);
        }

        // Semaphore 方法, 方便阅读, 放在此处
        public void acquire() throws InterruptedException {
            sync.acquireSharedInterruptibly(1);
        }

        // AQS 继承过来的方法, 方便阅读, 放在此处
        public final void acquireSharedInterruptibly(int arg)
                throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            if (tryAcquireShared(arg) < 0)
                doAcquireSharedInterruptibly(arg);
        }

        // 尝试获得共享锁
        protected int tryAcquireShared(int acquires) {
            return nonfairTryAcquireShared(acquires);
        }

        // Sync 继承过来的方法, 方便阅读, 放在此处
        final int nonfairTryAcquireShared(int acquires) {
            for (; ; ) {
                int available = getState();
                int remaining = available - acquires;
                if (
// 如果许可已经用完, 返回负数, 表示获取失败, 进入 doAcquireSharedInterruptibly
                        remaining < 0 ||
// 如果 cas 重试成功, 返回正数, 表示获取成功
                                compareAndSetState(available, remaining)) {
                    return remaining;
                }
            }
        }

        // AQS 继承过来的方法, 方便阅读, 放在此处
        private void doAcquireSharedInterruptibly(int arg) throws InterruptedException {
            final Node node = addWaiter(Node.SHARED);
            boolean failed = true;
            try {
                for (; ; ) {
                    final Node p = node.predecessor();
                    if (p == head) {
// 再次尝试获取许可
                        int r = tryAcquireShared(arg);
                        if (r >= 0) {
// 成功后本线程出队(AQS), 所在 Node设置为 head
// 如果 head.waitStatus == Node.SIGNAL ==> 0 成功, 下一个节点 unpark
// 如果 head.waitStatus == 0 ==> Node.PROPAGATE
// r 表示可用资源数, 为 0 则不会继续传播
                            setHeadAndPropagate(node, r);
                            p.next = null; // help GC
                            failed = false;
                            return;
                        }
                    }
// 不成功, 设置上一个节点 waitStatus = Node.SIGNAL, 下轮进入 park 阻塞
                    if (shouldParkAfterFailedAcquire(p, node) &&
                            parkAndCheckInterrupt())
                        throw new InterruptedException();
                }
            } finally {
                if (failed)
                    cancelAcquire(node);
            }
        }

        // Semaphore 方法, 方便阅读, 放在此处
        public void release() {
            sync.releaseShared(1);
        }

        // AQS 继承过来的方法, 方便阅读, 放在此处
        public final boolean releaseShared(int arg) {
            if (tryReleaseShared(arg)) {
                doReleaseShared();
                return true;
            }
            return false;
        }

        // Sync 继承过来的方法, 方便阅读, 放在此处
        protected final boolean tryReleaseShared(int releases) {
            for (; ; ) {
                int current = getState();
                int next = current + releases;
                if (next < current) // overflow
                    throw new Error("Maximum permit count exceeded");
                if (compareAndSetState(current, next))
                    return true;
            }
        }
    }
3. 为什么要有 PROPAGATE

早期有 bug

  • releaseShared 方法
    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
  • doAcquireShared 方法
    private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (; ; ) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
// 这里会有空档
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
  • setHeadAndPropagate 方法
    private void setHeadAndPropagate(Node node, int propagate) {
        setHead(node);
// 有空闲资源
        if (propagate > 0 && node.waitStatus != 0) {
            Node s = node.next;
// 下一个
            if (s == null || s.isShared())
                unparkSuccessor(node);
        }
    }
  • 假设存在某次循环中队列里排队的结点情况为 head(-1)->t1(-1)->t2(-1)
  • 假设存在将要信号量释放的 T3 和 T4,释放顺序为先 T3 后 T4
    正常流程
    在这里插入图片描述
    产生 bug 的情况
    在这里插入图片描述
    修复前版本执行流程
    1. T3 调用 releaseShared(1),直接调用了 unparkSuccessor(head),head 的等待状态从 -1 变为 0
    2. T1 由于 T3 释放信号量被唤醒,调用 tryAcquireShared,假设返回值为 0(获取锁成功,但没有剩余资源量)
    3. T4 调用 releaseShared(1),此时 head.waitStatus 为 0(此时读到的 head 和 1 中为同一个head),不满足条件,因此不调用 unparkSuccessor(head)
    4. T1 获取信号量成功,调用 setHeadAndPropagate 时,因为不满足 propagate > 0(2 的返回值也就是propagate(剩余资源量) == 0),从而不会唤醒后继结点, T2 线程得不到唤醒

bug 修复后

    private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
// 设置自己为 head
        setHead(node);
// propagate 表示有共享资源(例如共享读锁或信号量)
// 原 head waitStatus == Node.SIGNAL 或 Node.PROPAGATE
// 现在 head waitStatus == Node.SIGNAL 或 Node.PROPAGATE
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
                (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
// 如果是最后一个节点或者是等待共享读锁的节点
            if (s == null || s.isShared()) {
                doReleaseShared();
            }
        }
    }

    private void doReleaseShared() {
// 如果 head.waitStatus == Node.SIGNAL ==> 0 成功, 下一个节点 unpark
// 如果 head.waitStatus == 0 ==> Node.PROPAGATE
        for (; ; ) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue; // loop to recheck cases
                    unparkSuccessor(h);
                } else if (ws == 0 &&
                        !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue; // loop on failed CAS
            }
            if (h == head) // loop if head changed
                break;
        }
    }

在这里插入图片描述
1. T3 调用 releaseShared(),直接调用了 unparkSuccessor(head),head 的等待状态从 -1 变为 0
2. T1 由于 T3 释放信号量被唤醒,调用 tryAcquireShared,假设返回值为 0(获取锁成功,但没有剩余资源量)
3. T4 调用 releaseShared(),此时 head.waitStatus 为 0(此时读到的 head 和 1 中为同一个 head),调用doReleaseShared() 将等待状态置为 PROPAGATE(-3)
4. T1 获取信号量成功,调用 setHeadAndPropagate 时,读到 h.waitStatus < 0,从而调用doReleaseShared() 唤醒 T2

5. CountdownLatch

用来进行线程同步协作,等待所有线程完成倒计时。
其中构造参数用来初始化等待计数值,await() 用来等待计数归零,countDown() 用来让计数减一

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(3);
        new Thread(() -> {
            log.debug("begin...");
            sleep(1);
            latch.countDown();
            log.debug("end...{}", latch.getCount());
        }).start();
        new Thread(() -> {
            log.debug("begin...");
            sleep(2);
            latch.countDown();
            log.debug("end...{}", latch.getCount());
        }).start();
        new Thread(() -> {
            log.debug("begin...");
            sleep(1.5);
            latch.countDown();
            log.debug("end...{}", latch.getCount());
        }).start();
        log.debug("waiting...");
        latch.await();
        log.debug("wait end...");
    }

可以配合线程池使用,改进如下
CyclicBarrier 线程池大小要和栅栏计数一致,否则比如线程池3个,栅栏2个,有可能t1 t3,先运行完,把计数减完了,就不是想要的1 2去减完的效果

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(3);
        ExecutorService service = Executors.newFixedThreadPool(4);
        service.submit(() -> {
            log.debug("begin...");
            sleep(1);
            latch.countDown();
            log.debug("end...{}", latch.getCount());
        });
        service.submit(() -> {
            log.debug("begin...");
            sleep(1.5);
            latch.countDown();
            log.debug("end...{}", latch.getCount());
        });
        service.submit(() -> {
            log.debug("begin...");
            sleep(2);
            latch.countDown();
            log.debug("end...{}", latch.getCount());
        });
        service.submit(() -> {
            try {
                log.debug("waiting...");
                latch.await();
                log.debug("wait end...");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
    }

6. CyclicBarrier

[ˈsaɪklɪk ˈbæriɚ] 循环栅栏,用来进行线程协作,等待线程满足某个计数。构造时设置『计数个数』,每个线程执行到某个需要“同步”的时刻调用 await() 方法进行等待,当等待的线程数满足『计数个数』时,继续执行

   CyclicBarrier cb = new CyclicBarrier(2); // 个数为2时才会继续执行
   new Thread(() -> {
       System.out.println("线程1开始.." + new Date());
       try {
           cb.await(); // 当个数不足时,等待
       } catch (InterruptedException | BrokenBarrierException e) {
           e.printStackTrace();
       }
       System.out.println("线程1继续向下运行..." + new Date());
   }).start();
   new Thread(() -> {
       System.out.println("线程2开始.." + new Date());
       try {
           Thread.sleep(2000);
       } catch (InterruptedException e) {
       }
       try {
           cb.await(); // 2 秒后,线程个数够2,继续运行
       } catch (InterruptedException | BrokenBarrierException e) {
           e.printStackTrace();
       }
       System.out.println("线程2继续向下运行..." + new Date());
   }).start();

注意 CyclicBarrier 与 CountDownLatch 的主要区别在于 CyclicBarrier 是可以重用的 CyclicBarrier 可以被比喻为『人满发车』

7. 线程安全集合类概述

在这里插入图片描述
线程安全集合类可以分为三大类:

  • 遗留的线程安全集合如 Hashtable , Vector
  • 使用 Collections 装饰的线程安全集合,如:
    • Collections.synchronizedCollection
    • Collections.synchronizedList
    • Collections.synchronizedMap
    • Collections.synchronizedSet
    • Collections.synchronizedNavigableMap
    • Collections.synchronizedNavigableSet
    • Collections.synchronizedSortedMap
    • Collections.synchronizedSortedSet
  • java.util.concurrent.*
    重点介绍 java.util.concurrent.* 下的线程安全集合类,可以发现它们有规律,里面包含三类关键词:Blocking、CopyOnWrite、Concurrent
  • Blocking 大部分实现基于锁,并提供用来阻塞的方法
  • CopyOnWrite 之类容器修改开销相对较重
  • Concurrent 类型的容器
    • 内部很多操作使用 cas 优化,一般可以提供较高吞吐量
    • 弱一致性
      • 遍历时弱一致性,例如,当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍历,这时内容是旧的
      • 求大小弱一致性,size 操作未必是 100% 准确
      • 读取弱一致性

遍历时如果发生了修改,对于非安全容器来讲,使用 fail-fast 机制也就是让遍历立刻失败,抛出ConcurrentModificationException,不再继续遍历

LinkedBlockingQueue

1. 基本的入队出队

  public class LinkedBlockingQueue<E> extends AbstractQueue<E>
          implements BlockingQueue<E>, java.io.Serializable {
      static class Node<E> {
          E item;
          /**
           * 下列三种情况之一
           * - 真正的后继节点
           * - 自己, 发生在出队时
           * - null, 表示是没有后继节点, 是最后了
           */
          Node<E> next;

          Node(E x) {
              item = x;
          }
      }
  }

初始化链表 last = head = new Node(null); Dummy 节点用来占位,item 为 null在这里插入图片描述
当一个节点入队 last = last.next = node;在这里插入图片描述
再来一个节点入队 last = last.next = node;在这里插入图片描述
出队

Node<E> h = head;
Node<E> first = h.next;
h.next = h; // help GC
head = first;
E x = first.item;
first.item = null;
return x;

h = head
在这里插入图片描述
first = h.next在这里插入图片描述
h.next = h在这里插入图片描述
head = first在这里插入图片描述

E x = first.item;
first.item = null;
return x;

在这里插入图片描述

2. 加锁分析

高明之处在于用了两把锁和 dummy 节点

  • 用一把锁,同一时刻,最多只允许有一个线程(生产者或消费者,二选一)执行
  • 用两把锁,同一时刻,可以允许两个线程同时(一个生产者与一个消费者)执行
    • 消费者与消费者线程仍然串行
    • 生产者与生产者线程仍然串行

线程安全分析

  • 当节点总数大于 2 时(包括 dummy 节点),putLock 保证的是 last 节点的线程安全,takeLock 保证的是head 节点的线程安全。两把锁保证了入队和出队没有竞争
  • 当节点总数等于 2 时(即一个 dummy 节点,一个正常节点)这时候,仍然是两把锁锁两个对象,不会竞争
  • 当节点总数等于 1 时(就一个 dummy 节点)这时 take 线程会被 notEmpty 条件阻塞,有竞争,会阻塞
// 用于 put(阻塞) offer(非阻塞)
private final ReentrantLock putLock = new ReentrantLock();
// 用户 take(阻塞) poll(非阻塞)
private final ReentrantLock takeLock = new ReentrantLock();

put 操作

   public void put(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();
        int c = -1;
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
// count 用来维护元素计数
        final AtomicInteger count = this.count;
        putLock.lockInterruptibly();
        try {
// 满了等待
            while (count.get() == capacity) {
// 倒过来读就好: 等待 notFull
                notFull.await();
            }
// 有空位, 入队且计数加一
            enqueue(node);
            c = count.getAndIncrement();
// 除了自己 put 以外, 队列还有空位, 由自己叫醒其他 put 线程
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock();
        }
// 如果队列中有一个元素, 叫醒 take 线程
        if (c == 0)
// 这里调用的是 notEmpty.signal() 而不是 notEmpty.signalAll() 是为了减少竞争
            signalNotEmpty();
    }

take 操作

    public E take() throws InterruptedException {
        E x;
        int c = -1;
        final AtomicInteger count = this.count;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lockInterruptibly();
        try {
            while (count.get() == 0) {
                notEmpty.await();
            }
            x = dequeue();
            c = count.getAndDecrement();
            if (c > 1)
                notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
// 如果队列中只有一个空位时, 叫醒 put 线程
// 如果有多个线程进行出队, 第一个线程满足 c == capacity, 但后续线程 c < capacity
        if (c == capacity)
// 这里调用的是 notFull.signal() 而不是 notFull.signalAll() 是为了减少竞争
            signalNotFull()
        return x;
    }

由 put 唤醒 put 是为了避免信号不足

3. 性能比较

主要列举 LinkedBlockingQueue 与 ArrayBlockingQueue 的性能比较

  • Linked 支持有界,Array 强制有界
  • Linked 实现是链表,Array 实现是数组
  • Linked 是懒惰的,而 Array 需要提前初始化 Node 数组
  • Linked 每次入队会生成新 Node,而 Array 的 Node 是提前创建好的
  • Linked 两把锁,Array 一把锁

10. ConcurrentLinkedQueue

ConcurrentLinkedQueue 的设计与 LinkedBlockingQueue 非常像,也是

  • 两把【锁】,同一时刻,可以允许两个线程同时(一个生产者与一个消费者)执行
  • dummy 节点的引入让两把【锁】将来锁住的是不同对象,避免竞争
  • 只是这【锁】使用了 cas 来实现

事实上,ConcurrentLinkedQueue 应用还是非常广泛的
例如之前讲的 Tomcat 的 Connector 结构时,Acceptor 作为生产者向 Poller 消费者传递事件信息时,正是采用了ConcurrentLinkedQueue 将 SocketChannel 给 Poller 使用

ConcurrentLinkedQueue 原理

  1. 模仿 ConcurrentLinkedQueue
    初始代码
package cn.itcast.concurrent.thirdpart.test;

import java.util.Collection;
import java.util.Iterator;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicReference;

public class Test3 {
    public static void main(String[] args) {
        MyQueue<String> queue = new MyQueue<>();
        queue.offer("1");
        queue.offer("2");
        queue.offer("3");
        System.out.println(queue);
    }
}

class MyQueue<E> implements Queue<E> {
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        for (Node<E> p = head; p != null; p = p.next.get()) {
            E item = p.item;
            if (item != null) {
                sb.append(item).append("->");
            }
        }
        sb.append("null");
        return sb.toString();
    }

    @Override
    public int size() {
        return 0;
    }

    @Override
    public boolean isEmpty() {
        return false;
    }

    @Override
    public boolean contains(Object o) {
        return false;
    }

    @Override
    public Iterator<E> iterator() {
        return null;
    }

    @Override
    public Object[] toArray() {
        return new Object[0];
    }

    @Override
    public <T> T[] toArray(T[] a) {
        return null;
    }

    @Override
    public boolean add(E e) {
        return false;
    }

    @Override
    public boolean remove(Object o) {
        return false;
    }

    @Override
    public boolean containsAll(Collection<?> c) {
        return false;
    }

    @Override
    public boolean addAll(Collection<? extends E> c) {
        return false;
    }

    @Override
    public boolean removeAll(Collection<?> c) {
        return false;
    }

    @Override
    public boolean retainAll(Collection<?> c) {
        return false;
    }

    @Override
    public void clear() {
    }

    @Override
    public E remove() {
        return null;
    }

    @Override
    public E element() {
        return null;
    }

    @Override
    public E peek() {
        return null;
    }

    public MyQueue() {
        head = last = new Node<>(null, null);
    }

    private volatile Node<E> last;
    private volatile Node<E> head;

    private E dequeue() {
        /*Node<E> h = head;
        Node<E> first = h.next;
        h.next = h;
        head = first;
        E x = first.item;
        first.item = null;
        return x;*/
        return null;
    }

    @Override
    public E poll() {
        return null;
    }

    @Override
    public boolean offer(E e) {
        return true;
    }

    static class Node<E> {
        volatile E item;

        public Node(E item, Node<E> next) {
            this.item = item;
            this.next = new AtomicReference<>(next);
        }

        AtomicReference<Node<E>> next;
    }
}

offer

    public boolean offer(E e) {
        Node<E> n = new Node<>(e, null);
        while(true) {
// 获取尾节点
            AtomicReference<Node<E>> next = last.next;
// S1: 真正尾节点的 next 是 null, cas 从 null 到新节点
            if(next.compareAndSet(null, n)) {
// 这时的 last 已经是倒数第二, next 不为空了, 其它线程的 cas 肯定失败
// S2: 更新 last 为倒数第一的节点
                last = n;
                return true;
            }
        }
    }

11. CopyOnWriteArrayList

CopyOnWriteArraySet 是它的马甲 底层实现采用了 写入时拷贝 的思想,增删改操作会将底层数组拷贝一份,更改操作在新数组上执行,这时不影响其它线程的并发读,读写分离。 以新增为例:

    public boolean add(E e) {
        synchronized (lock) {
// 获取旧的数组
            Object[] es = getArray();
            int len = es.length;
// 拷贝新的数组(这里是比较耗时的操作,但不影响其它读线程)
            es = Arrays.copyOf(es, len + 1);
// 添加新元素
            es[len] = e;
// 替换旧的数组
            setArray(es);
            return true;
        }
    }

这里的源码版本是 Java 11,在 Java 1.8 中使用的是可重入锁而不是 synchronized

其它读操作并未加锁,例如:

    public void forEach(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        for (Object x : getArray()) {
            @SuppressWarnings("unchecked") E e = (E) x;
            action.accept(e);
        }
    }

适合『读多写少』的应用场景
get 弱一致性在这里插入图片描述

时间点操作
1Thread-0 getArray()
2Thread-1 getArray()
3Thread-1 setArray(arrayCopy)
4Thread-0 array[index]

不容易测试,但问题确实存在

迭代器弱一致性

   CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
   list.add(1);
   list.add(2);
   list.add(3);
   Iterator<Integer> iter = list.iterator();
   new Thread(() -> {
       list.remove(0);
       System.out.println(list);
   }).start();
   sleep1s();
   while (iter.hasNext()) {
       System.out.println(iter.next());
   }

不要觉得弱一致性就不好

  • 数据库的 MVCC 都是弱一致性的表现
  • 并发高和一致性是矛盾的,需要权衡
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值