文章目录
Java并发编程视频
BlockingQueue深入解析-BlockingQueue看这一篇就够了
一 概述
1.1 进程与线程
进程
进程由数据和指令组成,运行时需要将这些指令加载到CPU,数据加载到内存,进程用于加载指令、管理内存、管理I/O的。
进程可看做时程序的一个实例。大部分进程可以运行多个实例(记事本、画图、浏览器等),少数程序仅支持一个实例进程(网易云音乐、360安全浏览器等)
线程
线程依赖于进程,一个进程中可以有多个线程,一个线程就是一个指令流,将指令流中的指令以一定的顺序交给CPU执行
Java中,线程是最小的调度单位,进程是资源分配的最小单位,在windows中进程是不活动的,只是作为线程的容器
进程 VS 线程
- 进程基本上是相互独立的,而线程依赖于进程,是进程的一个子集
- 进程拥有共享的资源,如内存空间等,内存的线程共享内存空间
- 进程间通信较为复杂,可通过TCP/IP、IPC、管道、共享内存等通信
- 线程通信较为简单
- 线程更轻量级,线程上下文切换一般要比进程上下文切换效率高
1.2 并发与并行
单核CPU下,线程实际上还是串行执行的,操作系统的任务管理器负责为不同的程序分派时间片,只是cpu在切换时间片的时间非常短,感觉是同时运行的,微观串行、宏观并行。将线程轮流使用CPU称为并发
- 并发是同一时间处理多件事情的能力
- 并行是同一时间动手做多件事情的能力
二 Java线程
2.1 创建线程方式
直接使用Thread
Thread t1 = new Thread("t1") {
@Override
public void run() {
}
};
t1.start();
实现Runnable
Runnable runnable = new Runnable() {
@Override
public void run() {
log.debug("");
}
};
// Java8之后可使用lamba表达式
Runnable runnable = () => log.debug("");
Thread t2 = new Thread(runnable,"t2");
t2.start();
实现FutureTask
FutureTask<Integer> futureTask = new FutureTask<>(
() => log.debug();
return 100;
);
new Thread(futureTask, "t3").start();
// 获取返回结果
Integer reture = futureTask.get();
常用的线程命令
windows
- 任务管理器可以查看进程和线程数,也可以手动终止进程
- tasklist 查看进程
- taskkill 终止进程
linux
- ps -ef 查看所有进程
- ps -fT -p pid 查看某个进程的所有线程
- kill pid 终止指定进程
- top 查看线程
Java
- jps 查看所有Java进程
- jstack pid 查看java进程的所有线程状态
- jconsole 具体查看某个java进程中线程的运行状态(图形化界面)
Java中的线程的上下文切换
-
线程的时间片用完
-
垃圾回收
-
有更高优先级的线程需要运行
-
线程自己调用的sleep、yield、wait、join、park、synchronized、lock等方法
2.2 线程状态
从操作系统层面划分有五种状态:初始状态、可运行状态、运行中状态、阻塞状态、结束状态
从Java层面划分有六种状态
public enum State {
/**
* 仅仅创建了线程,尚未调用start
*/
NEW,
/**
* 调用了start之后的状态
*/
RUNNABLE,
/**
* 调用同步锁后的状态,如wait
*/
BLOCKED,
/**
* 阻塞等待状态
* 1)调用无参wait()
* 2)调用无参join()
* 3) 调用LockSupport.park()
*/
WAITING,
/**
* 带有超时时间的阻塞状态
* 1)调用无参wait(timeout)
* 2)调用无参join(timeout)
* 3) 调用LockSupport.parkNanos(),LockSupport.parkUntil
* 4) 调用sleep(timeout)
*/
TIMED_WAITING,
/**
* 终止状态.
* 线程正常结束后的状态,或者手动调用terminate()
*/
TERMINATED;
}
sleep
调用Sleep会让当前线程从Running切换到Timed Waiting状态
其他线程可以使用Interrupt方法打断正在睡眠的线程,这时sleep方法会抛出InterruptedException
睡眠结束后线程进入Runnable列表,并不保证一定会被执行
优先使用TimeUnit而不是sleep 如:TimeUnit.SECONDS.sleep(2);
yield
调用此方法会将当前线程从Running进入Runnable状态,然后执行其他相同优先级的线程,如果这时没有相同优先级的线程,那么不保证让当前线程暂停,具体的实现依赖于操作系统的任务调度器
join
让出当前执行线程的CPU,等待被调用线程执行结束
private static void testJoin() throws InterruptedException {
log.debug("开始");
Thread r1 = new Thread(() -> {
log.debug("开始");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("结束");
r = 10;
}, "t1");
r1.start();
r1.join();
log.debug("结果为: {}", r);
log.debug("结束");
}
主线程必须等待子线程t1结束后才能继续执行
Interrupt方法
Interrupt方法用于打断处于休眠状态的线程如调用了sleep、join、wait等方法,注意调用Interrupt方法后会清空线程的中断状态
可用于优雅的停止线程
private static void interruptTest() {
Thread t1 = new Thread(() -> {
while (true) {
if (Thread.currentThread().isInterrupted()) {
log.info("我被打断了,退出代码块");
break;
}
}
}, "t1");
t1.start();
sleep(1);
log.info("开始打断其他线程");
t1.interrupt();
}
23:35:11.226 [main] INFO jincong.practice.thread.status.JoinTest - 开始打断其他线程
23:35:11.228 [t1] INFO jincong.practice.thread.status.JoinTest - 我被打断了,退出代码块
park
线程停留在指定代码处,可调用interrupt方法打断park住的线程,使之继续向下运行
注意:park方法只会生效一次
public static void parkTest() throws InterruptedException{
Thread thread = new Thread(() -> {
log.info("park....");
LockSupport.park();
log.info("unPark...");
log.info("线程打断状态 {}", Thread.currentThread().isInterrupted());
}, "parkThread");
thread.start();
sleep(1);
thread.interrupt();
}
三 共享模型
3.1 临界区
一段代码块存在对共享资源的多线程读写操作
@Slf4j
public class CriticalSectionTest {
private static int counter = 0;
// 方案1 加对象锁
private static final Object ROOM = new Object();
public static void main(String[] args) throws InterruptedException{
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (ROOM) {
counter++;
}
}
}, "thread1");
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (ROOM) {
counter--;
}
}
}, "thread2");
thread1.start();
thread2.start();
thread1.join();
thread2.join();
log.info("counter = {}", counter);
}
}
使用对象锁synchronized
synchronized实际上是用对象锁 保证了临界区内代码的原子性,临界区内的代码是不可分割的,不会被线程切换打断。
// synchronized 加在普通方法上锁住的是该对象
class Test {
public synchronized void test(){
}
}
// 两者等价
class Test {
public void test(){
synchronized(this) {
}
}
}
// synchronized 加在静态方法上锁住的是该类
class Test {
public synchronized static void test(){
}
}
// 等价于
class Test {
public static void test(){
synchronized(Test.class) {
}
}
}
3.2 变量的线程安全性分析
成员变量
- 如果没有被共享,则线程安全
- 如果被共享了,有以下两种情况
- 如果只有只读操作,则线程安全
- 如果有读写操作,线程不安全
局部变量
- 局限变量是线程安全的
- 局部变量引用的对象不一定线程安全,需要判断是否发生了逃逸
- 如果发生引用逃逸则线程不安全
- 否则线程安全
线程安全类
- String 类被标注为final,内部数据 char[] 也被标注为final
- Integer 类被标注为final,内部数据 value 也被标注为final
- StringBuffer
- Random
- Vector
- Hashtable
- java.util.concurrrent包下的所有类
3.3 Synchronized底层原理
Monitor
操作系统层面有Monitor监控器,表示锁结构。如果使用synchronized给对象上锁之后,该对象头的Mark Word中就被设置为指向Monitor对象的指针。Java层面每个对象都可以作为一个锁对象
- 一开始Monitor中的Owner为空
- 当thread2进入同步代码块中时就会将Owner设置为thread2,
- 与此同时,thread3、thread4进入同步代码块,发现Owner已经有值且不是自身,就会进入EntryList的阻塞队列中
- thread2执行结束,释放owner,同时以非公平的方式唤醒EntryList中的一个线程继续执行同步代码块
- thread0、thread1是之前获得过锁,但是不满足waiting状态,进入等待队列中
- Blocked和Waiting状态的线程都处于阻塞状态,不占用CPU时间片
- Blocked线程在Owner线程释放锁时唤醒
- Waiting线程会在Owner线程调用notify或notifyAll时唤醒,唤醒后不会立即获取锁,而是进入EntryList队列中竞争锁
public class JvmTest {
private final static Object lock = new Object();
private static int counter = 0;
public static void main(String[] args) {
synchronized(lock){
counter++;
}
}
}
形象化理解synchronized
- 老王 JVM
- 小南 线程
- 小女 线程
- 房间 锁对象
- 房间门上的防盗锁 Monitor对象
- 房间门上放书包 轻量级锁
- 房间门上刻上名字 偏向锁
- 批量刻名字 一个类的偏向锁撤销达到20次
轻量级锁
如果一个对象虽然有多个线程并发访问,但是各个线程加锁的时间是错开的,那么可以使用轻量级锁来进行优化。轻量级锁仍然使用synchronized,对使用者是透明的。
public class JvmTest {
private final static Object obj = new Object();
public static void method1() {
synchronized(obj) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized(obj) {
// 同步块 B
}
}
}
-
创建锁记录(Lock Record),每个线程的栈帧中都包含一个锁记录的结构,内部保存锁定对象的Mark Record
-
锁记录的Object Reference指向锁对象,并尝试用CAS替换Object的Mark Word,之后将Mark Word的值存入锁记录
-
如果CAS替换成功,对象头中存储了锁记录的地址和状态00,表示该线程成功加了轻量级锁
-
如果CAS失败,此时有两种情况
-
如果是因为其他线程已经拥有了该Object的轻量级锁,表明有竞争,进入锁膨胀过程
-
如果是自己重入了synchronized代码块,则再增加一条Lock Record作为重入的次数
-
-
当退出synchronized代码块时,如果有null的锁记录,表示有重入线程,直接将当前锁记录删除,并将重入计数减一
-
当退出synchronized代码块,锁记录的值不为null,这是使用cas将Mark Word的值恢复给对象头
- 成功,则解锁成功
- 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁的解锁流程
锁膨胀
如果在尝试加轻量级锁过程中,CAS失败,可能因为其他线程为此对象加了轻量级锁,这时需要进行锁膨胀,将轻量级锁升级为重量级锁
public class JvmTest {
private final static Object lock = new Object();
private static int counter = 0;
public static void main(String[] args) {
synchronized(lock){
counter++;
}
}
}
-
当Thread1想加轻量级锁时,发现Thread0已经对此对象加了轻量级锁
-
这时Thread1加锁失败,进入锁膨胀流程
- 为Object对象申请Monitor锁,让Object指向重量级锁
- 然后自己进入Monitor的EntryList 阻塞对列中
-
当Thread0退出同步代码块时,使用CAS将MarkWord的值恢复给对象头,这时会进入重量级锁流程,即按照Monitor地址找到Monitor对象,设置Owner为null,唤醒EntryList中的阻塞线程
自旋锁
重量级锁的加锁和解锁都会消耗一定的性能,因而引入了自旋锁。当一个线程申请轻量级锁失败后,并不会直接进入重量级锁过程,而是自己自旋等待一定的次数,如果自旋过程成功获取到了锁,则正常访问同步代码块,否则进入重量级锁的流程。
- 自旋会占用CPU时间,因而仅适用于多核CPU场景。
- JVM内部会自适应自旋次数,上一次自旋成功后下一次自旋次数会提高,反之会减少甚至不自旋
- Java7 之后无法控制是否开启自旋锁
偏向锁
在轻量级锁中,如果同一线程反复多次进入同步代码块,也需要CAS操作,这一步是多余的操作完全可以不进行。Java6之后,只有第一次使用CAS将对象的Mark Word头设置为锁线程的ID,之后这个线程再次进入同步代码块后,无需CAS操作
总结
可见性
- 线程解锁时,必须将共享变量的值刷新到主内存中
- 加锁时,首先清空工作内存中的共享变量的值,然后从主内存中重新读取最新的值
3.4 wait notify
wait vs sleep
- sleep 是Thread的静态方法,wait是Object中的方法
- sleep可以任意使用,wait必须配合synchronized使用
- sleep睡眠时不会释放锁,wait会释放锁
- sleep睡眠结束后或者被打断后醒来,wait调用notify或者interrupt方法唤醒
/**
* 测试多个线程轮流等待
*
*/
@Slf4j
public class NotifyAllWithSynchronizedTest {
private static final Object LOCK = new Object();
private static boolean hasCigarette = false;
private static boolean hasTakeout = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (LOCK) {
log.info("有烟没?[{}]", hasCigarette);
// 这里使用while而不是if来解决虚假唤醒的问题
while (!hasCigarette) {
log.info("没烟,先休息一会!");
try {
LOCK.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.info("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.info("烟来了,可以继续干活了!");
} else {
log.info("没干成活!");
}
}
}, "小南").start();
new Thread(() -> {
synchronized (LOCK) {
log.info("外卖到没?[{}]", hasTakeout);
while (!hasTakeout) {
log.info("外卖没到,先看会剧!");
try {
LOCK.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.info("外卖到没?[{}]", hasTakeout);
if (hasTakeout) {
log.info("外卖到了,可以继续干活了!");
} else {
log.info("没干成活!");
}
}
}, "小丽").start();
Thread.sleep(1000);
new Thread(() -> {
synchronized (LOCK) {
hasTakeout = true;
log.info("外卖到了!");
// nofityAll唤醒所有阻塞的线程,增加并行度
LOCK.notifyAll();
}
}, "外卖员").start();
new Thread(() -> {
synchronized (LOCK) {
hasCigarette = true;
log.info("烟到了!");
LOCK.notifyAll();
}
}, "送烟").start();
}
}
23:34:54.613 [小南] INFO jincong.practice.thread.demo.NotifyAllWithSynchronizedTest - 有烟没?[false]
23:34:54.618 [小南] INFO jincong.practice.thread.demo.NotifyAllWithSynchronizedTest - 没烟,先休息一会!
23:34:54.618 [小丽] INFO jincong.practice.thread.demo.NotifyAllWithSynchronizedTest - 外卖到没?[false]
23:34:54.618 [小丽] INFO jincong.practice.thread.demo.NotifyAllWithSynchronizedTest - 外卖没到,先看会剧!
23:34:55.616 [外卖员] INFO jincong.practice.thread.demo.NotifyAllWithSynchronizedTest - 外卖到了!
23:34:55.617 [小丽] INFO jincong.practice.thread.demo.NotifyAllWithSynchronizedTest - 外卖到没?[true]
23:34:55.617 [小丽] INFO jincong.practice.thread.demo.NotifyAllWithSynchronizedTest - 外卖到了,可以继续干活了!
23:34:55.617 [小南] INFO jincong.practice.thread.demo.NotifyAllWithSynchronizedTest - 没烟,先休息一会!
23:34:55.617 [送烟] INFO jincong.practice.thread.demo.NotifyAllWithSynchronizedTest - 烟到了!
23:34:55.617 [小南] INFO jincong.practice.thread.demo.NotifyAllWithSynchronizedTest - 有烟没?[true]
23:34:55.617 [小南] INFO jincong.practice.thread.demo.NotifyAllWithSynchronizedTest - 烟来了,可以继续干活了!
常用的语法
synchronized(lock) {
while(条件不成立) {
lock.wait();
}
// 执行业务代码
doService();
}
synchronized(lock) {
// 使用notifyAll而不会notify来唤醒阻塞线程
lock.notifyAll();
}
保护性暂停
/**
* 使用GuardObject解决一个线程依赖另一个线程执行结果的场景
* 也就是join底层实现机制
*
*/
@Slf4j
public class GuardObjectTest {
public static void main(String[] args) {
GuardedObject guardedObject = new GuardedObject();
new Thread(()-> {
log.info("消费者 开始消费");
try {
Object response = guardedObject.get(2000);
log.info("结果是: {}", response);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "消费线程").start();
new Thread(()-> {
try {
log.info("生产者 开始生产");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
guardedObject.complete(null);
}, "生产线程").start();
}
static class GuardedObject {
// 受保护的对象,也是线程间传递的结果
private Object response;
/**
* 获取线程结果,如果超出时间直接返回结果
* @param timeout 超时时间
* @return
* @throws InterruptedException
*/
public synchronized Object get(int timeout) throws InterruptedException {
// 注意,这里一定要维护passTime,防止该方法被虚假唤醒,破坏超时时间
// 15:00:00
long begin = System.currentTimeMillis();
long passTime = 0;
while (response == null) {
// 如果此处直接设置为timeout,则被虚假唤醒时有问题
long waitTime = timeout - passTime;
if (waitTime <= 0) {
break;
}
this.wait(waitTime);
// 15:00:02
passTime = System.currentTimeMillis() - begin;
}
return response;
}
/**
* 生产结果方法,完成后唤醒等待的线程
* @param response
*/
public synchronized void complete(Object response) {
this.response = response;
this.notifyAll();
}
}
}
join原理
join表示一个线程等待另一个线程的结束,内部使用了上述的保护性暂停模式
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
// 如果此处直接设置为timeout,则被虚假唤醒时有问题
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
生产者消费者模式
/**
* 自定义消息队列,实现take和put方法
*
*/
@Slf4j
public class CustomMessageQueueTest {
private static MessageQueue queue = new MessageQueue(2);
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
// lambda表达式中的变量必须不能改变
int id= i;
new Thread(() -> {
try {
queue.put(new Message(id, "值" + id));
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "生产者" + id).start();
}
new Thread(()-> {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
Message message = queue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "消费者").start();
}
static final class MessageQueue {
// 消息队列集合
private final LinkedList<Message> list = new LinkedList<>();
// 消息队列容量
private int capacity;
MessageQueue(int capacity) {
this.capacity = capacity;
}
Message take() throws InterruptedException {
synchronized (list) {
// 检查队列是否为空
while (list.isEmpty()) {
log.debug("队列为空,消费者进入阻塞状态!");
list.wait();
}
// 从队列头部获取消息并返回
Message message = list.removeFirst();
log.debug("消费者已消费消息 {}!", message);
// 唤醒生产者线程
list.notifyAll();
return message;
}
}
public void put(Message message) throws InterruptedException {
synchronized (list) {
// 检查队列是否已满
while (list.size() == capacity) {
log.debug("队列已满,生产者进入阻塞状态!");
list.wait();
}
// 在尾部添加消息
list.addLast(message);
log.debug("生产者已生产消息 {}!", message);
// 唤醒消费者线程
list.notifyAll();
}
}
}
static class Message {
private int id;
private Object value;
Message(int id, Object value) {
this.id = id;
this.value = value;
}
public int getId() {
return id;
}
public Object getValue() {
return value;
}
@Override
public String toString() {
return "Message{" +
"id=" + id +
", value=" + value +
'}';
}
}
}
3.5 park unpark
// 暂停当前线程
LockSupport.park();
// 恢复某个线程的运行
LockSupport.unpark(threadName);
- wait、notify、notifyAll必须与synchronized配合使用,而park、unpark不必
- park、unpark是以线程为单位阻塞或唤醒线程,而notify是随机唤醒一个阻塞线程,notifyAll是唤醒所有阻塞线程
- park、unpark无先后顺序,wait和notify有先后顺序
原理
每个线程都有有一个Parker对象,包含_counter, cond和 _mutex
每个线程可以比作一个旅行者,Parker是他随身携带的背包,cond是背包中的帐篷,counter是背包中的干粮(0为耗尽,1位充足)
- 调用park就是看需不需要停下来休息
- 干粮耗尽(counter=0),钻进帐篷中休息
- 干粮充足(counter=1),无需停留,继续前行
- 调用unpark就是补充干粮
- 如果线程还在帐篷中休息,唤醒他继续前行
- 如果线程正在运行,则下次调用park时,仅消耗干粮,无需停留(多次调用unpark仅会补充一份干粮)
先park 后unpark
调用park
- 当前线程调用Unsafe.park()
- 检查counter=0? ,获得mutex对象锁(对象锁中有condition阻塞对列)
- 线程进入condition阻塞对列中等待
- 设置counter=0
调用unpark
- 调用Unsafe.unpark(threadName), 设置counter=1
- 唤醒condition对列中的thread
- thread恢复运行
- 设置counter=0
先unpark 后park
- 调用Unsafe.unpark(threadName)方法,设置counter=1
- 调用当前线程的park方法
- 检查counter=0? ,此时线程无需阻塞,继续运行
- 设置counter=0
四 线程
4.1 活跃性
死锁
死锁,由于多个线程在持有锁的同时又去申请另一把锁,导致线程相互等待无法继续运行的现象。
- 互斥条件
- 请求与保持条件
- 不可剥夺条件
- 循环等待条件
package jincong.practice.thread.deadlock;
import jincong.practice.thread.Sleeper;
import lombok.extern.slf4j.Slf4j;
/**
* DeadLockTest 测试死锁
*
*/
@Slf4j
public class DeadLockTest {
public static void main(String[] args) {
Object objA = new Object();
Object objB = new Object();
Thread thread1 = new Thread(() -> {
synchronized (objA) {
log.info("lock A");
Sleeper.sleep(1);
synchronized (objB) {
log.info("lock B");
log.info("执行业务代码");
}
}
}, "thread1");
Thread thread2 = new Thread(() -> {
synchronized (objB) {
log.info("lock B");
Sleeper.sleep(2);
synchronized (objA) {
log.info("lock A");
log.info("执行业务代码");
}
}
}, "thread2");
thread1.start();
thread2.start();
}
}
如何排查死锁
jstack
-
首先使用jps 获取java线程
E:\git_repository\intervirw_test>jps 21760 Jps 41536 KotlinCompileDaemon 14660 23416 DeadLockTest 49580 Launcher
-
使用jstack pid 获取详情
E:\git_repository\intervirw_test>jstack 23416 2021-02-15 20:39:42 Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.144-b01 mixed mode): "DestroyJavaVM" #16 prio=5 os_prio=0 tid=0x0000000003562800 nid=0xbe28 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "thread2" #15 prio=5 os_prio=0 tid=0x0000000020956000 nid=0xb010 waiting for monitor entry [0x000000002129f000] java.lang.Thread.State: BLOCKED (on object monitor) at jincong.practice.thread.deadlock.DeadLockTest.lambda$main$1(DeadLockTest.java:39) - waiting to lock <0x000000076c05d6c8> (a java.lang.Object) - locked <0x000000076c05d6d8> (a java.lang.Object) at jincong.practice.thread.deadlock.DeadLockTest$$Lambda$2/1454127753.run(Unknown Source) at java.lang.Thread.run(Thread.java:748) "thread1" #14 prio=5 os_prio=0 tid=0x0000000020950800 nid=0x87f8 waiting for monitor entry [0x000000002119f000] java.lang.Thread.State: BLOCKED (on object monitor) at jincong.practice.thread.deadlock.DeadLockTest.lambda$main$0(DeadLockTest.java:26) - waiting to lock <0x000000076c05d6d8> (a java.lang.Object) - locked <0x000000076c05d6c8> (a java.lang.Object) at jincong.practice.thread.deadlock.DeadLockTest$$Lambda$1/1061804750.run(Unknown Source) at java.lang.Thread.run(Thread.java:748) Found one Java-level deadlock: ============================= "thread2": waiting to lock monitor 0x0000000020a8c438 (object 0x000000076c05d6c8, a java.lang.Object), which is held by "thread1" "thread1": waiting to lock monitor 0x000000001d222758 (object 0x000000076c05d6d8, a java.lang.Object), which is held by "thread2" Java stack information for the threads listed above: =================================================== "thread2": at jincong.practice.thread.deadlock.DeadLockTest.lambda$main$1(DeadLockTest.java:39) - waiting to lock <0x000000076c05d6c8> (a java.lang.Object) - locked <0x000000076c05d6d8> (a java.lang.Object) at jincong.practice.thread.deadlock.DeadLockTest$$Lambda$2/1454127753.run(Unknown Source) at java.lang.Thread.run(Thread.java:748) "thread1": at jincong.practice.thread.deadlock.DeadLockTest.lambda$main$0(DeadLockTest.java:26) - waiting to lock <0x000000076c05d6d8> (a java.lang.Object) - locked <0x000000076c05d6c8> (a java.lang.Object) at jincong.practice.thread.deadlock.DeadLockTest$$Lambda$1/1061804750.run(Unknown Source) at java.lang.Thread.run(Thread.java:748) Found 1 deadlock.
使用jconsole图形化界面
-
命令行输入jconsole
-
切换到线程—>点击检测死锁
解决方案
原则是破坏死锁产生的四个必要条件
- 一次性获取所有的资源 (破坏请求保持条件)
- 获取不到资源时,释放已经占有的资源,比如加上超时时间(破坏不可剥夺条件)
- 将共享资源编号,顺序获取共享资源(破坏循环等待条件)
活锁
多个线程互相更改对方的结束条件,导致最终都无法正常结束
@Slf4j
public class LiveLockTest {
private static volatile int counter = 10;
private static Object lock = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
// 期望减到0时停止
while (counter > 0) {
Sleeper.sleep(0.5);
counter--;
log.debug("counter={}", counter);
}
}, "thread1");
Thread thread2 = new Thread(() -> {
// 期望超过20时停止
while (counter < 20) {
Sleeper.sleep(0.5);
counter++;
log.debug("counter={}", counter);
}
}, "thread2");
thread1.start();
thread2.start();
}
}
- 时间上错开,使多个线程交错执行
4.2 ReentrantLock
-
可中断
synchronized无法打断,lock可以调用interrupt方法打断
-
可设置超时时间
synchronized无法设置超时时间,lock可以自定义超时时间
-
支持公平锁
synchronized为非公平锁,lock可以设置公平锁与非公平锁
-
支持多个条件变量
synchronized相当于只要一个休息室,而lock可设置多个不同的休息室
-
支持可重入
-
可重入锁的lock()与unlock()必须成对出现,且unlock必须放到finally语句块中
reentrantLock.lock();
try {
// 临界区
} finally {
reentrantLock.unlock();
}
@Slf4j
public class NotifyAllWithReentrantLockTest {
private static boolean hasCigarette = false;
private static boolean hasTakeout = false;
/**
* 定义一个可重复的支持多条件的锁
*/
private static final ReentrantLock ROOM = new ReentrantLock();
/**
* 吸烟室
*/
private static Condition cigaretteWaitingSet = ROOM.newCondition();
/**
* 外卖室
*/
private static Condition takeoutWaitingSet = ROOM.newCondition();
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
ROOM.lock();
try {
log.info("有烟没?[{}]", hasCigarette);
while (!hasCigarette) {
log.info("没烟,先休息一会!");
try {
cigaretteWaitingSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.info("烟来了,可以继续干活了!");
} finally {
ROOM.unlock();
}
}, "小南").start();
new Thread(() -> {
ROOM.lock();
try {
log.info("外卖到没?[{}]", hasTakeout);
while (!hasTakeout) {
log.info("外卖没到,先看会剧!");
try {
takeoutWaitingSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.info("外卖到了,可以继续干活了!");
} finally {
ROOM.unlock();
}
}, "小丽").start();
Thread.sleep(1000);
// 外卖员线程
new Thread(() -> {
ROOM.lock();
try {
hasTakeout = true;
log.info("外卖到了!");
takeoutWaitingSet.signalAll();
} finally {
ROOM.unlock();
}
}, "外卖员").start();
// 送烟线程
new Thread(() -> {
ROOM.lock();
try {
hasCigarette = true;
log.info("烟到了!");
cigaretteWaitingSet.signalAll();
} finally {
ROOM.unlock();
}
}, "送烟").start();
}
}
交替输出ABC
wait && notifyAll
/**
* 使用wait和notify方法实现多线程交替打印ABC
* 思路:
* Thread curState nextState output
* 1 1 2 A
* 2 2 3 B
* 3 3 1 C
*
*/
public class PrintABCWithWaitAndNotifyTest {
public static void main(String[] args) {
WaitNotify waitNotify = new WaitNotify(1, 10);
Thread threadA = new Thread(() -> {
try {
waitNotify.print(1, 2, "A");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "ThreadA");
Thread threadB = new Thread(() -> {
try {
waitNotify.print(2, 3, "B");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "ThreadB");
Thread threadC = new Thread(() -> {
try {
waitNotify.print(3, 1, "C");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "ThreadC");
threadA.start();
threadB.start();
threadC.start();
}
static class WaitNotify {
/**
* 线程当前状态
*/
private int curState;
/**
* 下一状态
*/
private int nextState;
/**
* 循环次数
*/
private int loopCounter;
WaitNotify(int curState, int loopCounter) {
this.curState = curState;
this.loopCounter = loopCounter;
}
/**
* 交替打印ABC
* @param curStateFlag
* @param nextState
* @param output
* @throws InterruptedException
*/
public synchronized void print(int curStateFlag, int nextState, String output) throws InterruptedException {
for (int i = 0; i < loopCounter; i++) {
while (curStateFlag != curState) {
this.wait();
}
System.out.print(output);
curState = nextState;
this.notifyAll();
}
}
}
}
await && signal
/**
* 使用ReentrantLock的await和signal方法实现多线程交替打印ABC
* 思路:
* Thread curCondition nextCondition output
* 1 1 2 A
* 2 2 3 B
* 3 3 1 C
*
*/
@Slf4j
public class PrintABCWithAwaitAndSignalTest {
public static void main(String[] args) throws InterruptedException {
AwaitSignal awaitSignal = new AwaitSignal(10);
Condition aCondition = awaitSignal.newCondition();
Condition bCondition = awaitSignal.newCondition();
Condition cCondition = awaitSignal.newCondition();
Thread threadA = new Thread(() -> {
try {
awaitSignal.print(aCondition, bCondition, "A");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "ThreadA");
Thread threadB = new Thread(() -> {
try {
awaitSignal.print(bCondition, cCondition, "B");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "ThreadB");
Thread threadC = new Thread(() -> {
try {
awaitSignal.print(cCondition, aCondition, "C");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "ThreadC");
threadA.start();
threadB.start();
threadC.start();
Thread.sleep(1000);
// 主线程唤醒aCondition
awaitSignal.lock();
try {
log.debug("开始。。。。");
aCondition.signal();
} finally {
awaitSignal.unlock();
}
}
static class AwaitSignal extends ReentrantLock {
private static final long serialVersionUID = 8629940498386583541L;
/**
* 循环次数
*/
private int loopCounter;
AwaitSignal(int loopCounter) {
this.loopCounter = loopCounter;
}
/**
* 交替打印ABC
* @param curCondition 当前休息室
* @param nextCondition 下一间休息室
* @param output 输出内容
* @throws InterruptedException
*/
public void print(Condition curCondition, Condition nextCondition, String output) throws InterruptedException {
for (int i = 0; i < loopCounter; i++) {
lock();
try {
// 每次进入首先将当前线程置为等待状态,然后唤醒下一个线程
curCondition.await();
System.out.print(output);
nextCondition.signal();
} finally {
unlock();
}
}
}
}
}
park && unpark
/**
* 使用LockSupport的Park和UnPark方法实现多线程交替打印ABC
* 思路:
* Thread Park UnPark output
* 1 1 2 A
* 2 2 3 B
* 3 3 1 C
*
*/
@Slf4j
public class PrintABCWithParkAndUnParkTest {
/**
* A线程
*/
private static Thread aThread;
/**
* B线程
*/
private static Thread bThread;
/**
* C线程
*/
private static Thread cThread;
public static void main(String[] args) {
ParkUnPark parkUnpark = new ParkUnPark(10);
aThread = new Thread(() -> parkUnpark.print(bThread, "A"));
bThread = new Thread(() -> parkUnpark.print(cThread, "B"));
cThread = new Thread(() -> parkUnpark.print(aThread, "C"));
aThread.start();
bThread.start();
cThread.start();
LockSupport.unpark(aThread);
}
static class ParkUnPark {
/**
* 循环次数
*/
private int loopCounter;
ParkUnPark(int loopCounter) {
this.loopCounter = loopCounter;
}
public void print(Thread next, String output) {
for (int i = 0; i < loopCounter; i++) {
LockSupport.park();
System.out.print(output);
LockSupport.unpark(next);
}
}
}
}
五 Java内存模型
- 原子性 保证指令不会受到线程上下文切换的影响
- 可见性 保证指令不会受到CPU缓存的影响
- 有序性 保证指令不会受到CPU执行并行优化、重排序等的影响
5.1 JMM特性
可见性
下面这段代码,即使主线程改变了终止条件,thread1仍然无法停止
/**
* VolatileTerminatedTest
* 测试使用Volatile终止线程
*
*/
@Slf4j
public class VolatileTerminatedTest {
private static boolean run = true;
// private static volatile boolean run = true;
public static void main(String[] args) {
new Thread(() -> {
while (run) {
//如果加上这个代码就会停下来
System.out.println(2323);
}
log.info("thread1 停止了。。。。。");
}, "thread1").start();
Sleeper.sleep(1);
log.info("主线程更改条件,是线程thread1停止。。。。。");
run = false;
}
}
-
初始状态,theread1线程从主内存中读取run的值到自己的工作内存中
-
因为thread1频繁得从主内存中取值,JIT会将run的值缓存到自己的工作内存中,减少对主内存的频繁访问
-
虽然1s后主线程更改了run的值,并同步到主内存中。但是thread1仍然读取的时自己工作内存中的run值,导致无法正常结束
解决方案
- 使用volatile,可用来修饰成员变量或者静态成员变量,避免线程从自己的工作缓存中取数。
- 使用synchronized,线程加锁时先清空工作内存=>在主内存中拷贝最新变量的副本到工作内存中=>执行代码=>将更改后的共享变量的值刷新到主内存中=>释放锁
@Slf4j
class TwoPhaseTermination {
/**
* 监控线程
*/
private Thread monitor;
/**
* 线程中断标志
*/
private volatile boolean stop = false;
/**
* 监控线程开启标志(确保只会开启一个监控线程)
*/
private boolean starting = false;
// 启动监控线程
public void start() {
// 尽量同步代码块中的代码
synchronized (this) {
if (starting) {
return;
}
starting = true;
}
monitor = new Thread(()-> {
while (true) {
if (stop) {
log.info("料理后事,优雅停机");
break;
}
try {
// 情况1-线程在睡眠状态被中断
Thread.sleep(1000);
// 情况2-线程在执行监控任务时被中断
log.info("监控线程执行监控任务");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "monitorThread");
monitor.start();
}
// 停止监控线程
public void stop() {
stop = true;
// 及时中断睡眠中的线程
monitor.interrupt();
}
}
原子性
多个代码要不同时执行,要不都不会执行,不会出现中间结果
- synchronized天然支持原子性
- AtomicXX类中均支持原子性
有序性
在不影响代码执行结果的前提下,JVM会将不同的指令进行重新排序,然后再执行以此来提高吞吐量
5.2 volatile
volatile
是Java中的关键字,可以保证变量的可见性、有序性、无法确保语句的原子性。底层通过内存屏障(Memory Fence)实现
- volatile 变量的写指令后加入写屏障
- volatile变量的读指令前加入读屏障
保证可见性
-
写屏障保证在该屏障之前对共享变量的改动都会同步到主内存中
static int num = 0; static volatile boolean ready = false; public void method(Result result) { num = 2; ready = true; // 写屏障,确保7行之上所有对共享变量的改动都同步到主内存中 }
-
读屏障保证在该屏障后,对共享变量的读取来自于主内存
static int num = 0; static volatile boolean ready = false; public void method(Result result) { // 读屏障,确保6行以下所有对共享变量的读取都来自于主内存 if(ready) { r = num + num; } else{ r = 1; } }
保证有序性
-
写屏障保证指令重排序时,不会将写屏障之前的代码排在写屏障之后
static int num = 0; static volatile boolean ready = false; public void method(Result result) { num = 2; ready = true; // 写屏障,确保7行之上优先执行 }
-
读屏障保证指令重排序时,不会将读屏障之后的代码排在写屏障之前
static int num = 0; static volatile boolean ready = false; public void method(Result result) { // 读屏障,确保6行以下代码执行顺序不会被重排序到之前 if(ready) { r = num + num; } else{ r = 1; } }
应用之DCL
public class DCLSingleton {
//不加volatile无法保证线程安全,详见Java并发编程实战P286
private static volatile DCLSingleton instance;
private DCLSingleton() {}
public static DCLSingleton getInstance() {
if (instance == null) {
synchronized (DCLSingleton.class) {
// 问题根源分析
// JVM可能先给引用变量赋值,然后再去实例化对象
// 在此过程中,可能会有另一个线程获取到了未初始的对象
if (instance == null) {
// 此操作并不是原子的
// 如果共享变量完全交给synchronized管理可以保证有序性,否则无法保证
// 本案例的instance在同步代码块之外仍然被多线程访问,因而无法保证有序性
instance = new DCLSingleton();
}
}
}
return instance;
}
}
实例化一个对象需要三个步骤:
- 分配内存空间
- 初始化对象
- 将内存空间的地址赋值给对应的引用
但是由于重排序的缘故,步骤2、3可能回发生指令重排序,导致出现一个未完全初始化的对象,volatile可以禁止指令重排序,从而规避以上的问题
5.3 CAS
基于Compare And Swap无锁模式解决并发资源的访问问题,一定程度上可以提高效率。内部使用volatile来确保读取主内存中的最新值
- 乐观锁,不怕别的线程修改共享变量,通过不断重试来获取正确的结果
- 无锁并发,线程不会阻塞,但是在线程数较多竞争激烈的场景下效率会低
@Override
public void withdraw(Integer amount) {
// 核心代码
// 需要不断尝试,直到成功为止
while (true){
// 比如拿到了旧值 1000
int pre = getBalance();
// 在这个基础上 1000-10 = 990
int next = pre - amount;
/*
compareAndSet 正是做这个检查,在 set 前,先比较 prev 与当前值
- 不一致了,next 作废,返回 false 表示失败
比如,别的线程已经做了减法,当前值已经被减成了 990
那么本线程的这次 990 就作废了,进入 while 下次循环重试
- 一致,以 next 设置为新值,返回 true 表示成功
*/
if (atomicInteger.compareAndSet(pre,next)){
break;
}
}
}
5.4 ABA
CAS仅会判断最近一次修改是否与之前值相同,不能判断出是否发生过改变,如A=>B=>A问题
每次修改增加版本号
static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A",0);
public static void main(String[] args) throws InterruptedException {
log.debug("main start...");
// 获取值 A
int stamp = ref.getStamp();
log.info("{}",stamp);
String prev = ref.getReference();
other();
utils.sleep(1);
// 尝试改为 C
log.debug("change A->C {}", ref.compareAndSet(prev, "C",stamp,stamp+1));
}
private static void other() {
new Thread(() -> {
int stamp = ref.getStamp();
log.info("{}",stamp);
log.debug("change A->B {}", ref.compareAndSet(ref.getReference(), "B",stamp,stamp+1));
}, "t1").start();
utils.sleep(1);
new Thread(() -> {
int stamp = ref.getStamp();
log.info("{}",stamp);
log.debug("change B->A {}", ref.compareAndSet(ref.getReference(), "A",stamp,stamp+1));
}, "t2").start();
}
六 不可变类
对象一旦被创建后,对象所有的状态及属性在其生命周期内不会发生任何变化,不可变类天生线程安全。
如:String、包装类,DateTimeFormatter等
通过以下几种方式确保不可变
1、该类被final修饰,防止子类覆写其中的方法
2、内部值使用final修饰,无法更改引用地址
3、内部hashcode私有化,不提供setter方法
4、内部方法使用保护性拷贝方式
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
// ...
}
6.1 final语句
final可以用来修饰类、方法、变量
1)修饰类时,表明该类不可被继承,同时该类中的private方法也隐式被标识为final,如String类、基本数据类型的包装类型
2)修饰方法时,表明该方法不可被重写,但是不影响本类的重载以及重载函数的重写
3)修饰静态变量,相当于一个常量,如果是基本数据类型,其值不可改变,如果修饰的是引用类 型,引用地址不可改变,可以改变引用所指向的内容;
修饰静态变量时,可以在声明时,构造函数,或者代码块中赋值,且只能被赋值一次;
修饰局部变量可以不进行赋值
其内部实现为在final语句中加入写屏障
6.2 享元模式
不可变对象虽然解决了并发访问问题,但是频繁得创建新对象也会降低性能,因而提出一种能够重复利用已有对象的模式
比如Byte、Short、或Long的valueOf方法
Integer最低值默认为-128,最高值可通过属性设置
public static Integer valueOf(int i) {
[-128, 127]
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
七 线程池
7.1 线程池状态
-
RUNNING(-1):可以接收新任务,以及对新添加的任务进行处理
-
SHUTDOWN(0):不能接受新任务,但是可以对已经添加的任务进行处理
-
STOP(1):不能接受新任务,不处理已添加的任务,并且会中断正在处理的任务
-
TIDYING(2):当所有的任务已终止,ctr记录的任务数量为0,线程池变为此状态,此时会执行钩子 函数terminated()
-
TERMINATED(3):线程池彻底终止的状态
线程池状态和线程数量保存在同一个原子变量ctr中,高三位表示线程池状态,低29位表示线程数量
-
shutDown():不接受新任务,可以继续执行队列中的任务
-
shutDownNow():不接受任务,停止所有活动的任务,返回等待处理的任务列表
7.2 ThreadPoolExecutor
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- corePoolSize 核心线程数(最多保留的线程数)
- maximumPoolSize 最大线程数
- keepAliveTime 救急线程的生存时间(最大线程数-核心线程数)
- unit 时间单位
- workQueue 阻塞队列
- threadFactory 线程工厂,可自定义线程名称
- handler 拒绝策略
- AbortPolicy 调用者直接抛出RejectedExecutionException异常(默认策略)
- CallerRunsPollicy 调用者运行线程
- DiscardPolicy 放弃执行本次任务
- DiscardOldestPolicy 放弃队列中最早的任务,本任务取而代之
newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
- 核心线程数等于最大线程数,无救急线程也无需超时时间
- 阻塞队列是无界的,可以接受任意线程
- 适用于任务量已知,相对耗时的任务
newCachedThreadPool
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
- 核心线程数为0,最大线程数为2^31-1,救急线程在空闲60s后被回收,也就是全部是救急线程,可以被无限创建
- 采用SynchronousQueue队列,此队列无容量,仅是一个即时中转站
- 适合任务密集,但每个任务执行时间较短的场景
newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
- 仅有一个线程,无救急线程
- 可以增加无限任务
7.3 线程池的主要方法
提交方法
-
Future submit(Callable task)
接收一个任务,并且返回任务的结果,主线程调用future.get方法时会阻塞
-
List<Future> invokeAll(Collection<? extends Callable> tasks)
throws InterruptedException;接收任务列表,待任务列表中所有任务均执行结束才返回结果
-
T invokeAny(Collection<? extends Callable> tasks)
throws InterruptedException, ExecutionException;返回任务列表中第一个执行结束的线程结果,放弃执行其他线程
关闭方法
/*
1 不接受新任务
2 中断空闲的线程
3 不会中断正在执行的任务
*/
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
// 修改线程池状态(SHUTDOWN)
advanceRunState(SHUTDOWN);
// 中断空闲线程(内部采用tryLock方式判断是否空闲)
interruptIdleWorkers();
// 模板方法模式,子类ScheduledThreadPoolExecutor的钩子函数
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
// 尝试终结线程池(没有执行的线程会立刻终止,如果有运行的线程调用线程也不会被阻塞)
tryTerminate();
}
/*
1 不接受新任务
2 终止正在运行的任务
3 返回队列中尚未执行的任务
*/
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
// 修改线程池状态(STOP)
advanceRunState(STOP);
// 中断所有线程(
interruptWorkers();
// 释放为执行的任务到新的集合中,并返回
tasks = drainQueue();
} finally {
mainLock.unlock();
}
tryTerminate();
return tasks;
}
线程池异常
- 自己在任务中添加捕获异常的代码,如try catch
- 调用future.get方法后,如果正常结束,则返回结果,否则返回异常的堆栈信息
线程池的应用场景
不同类型的任务应使用不同的线程池,防止单个线程池陷入饥饿
CPU密集型一般设置核心线程数为CPU+1
IO密集型一般设置核心线程数为CPU*20
异步任务
JDK模式
Future可以获取到线程的执行结果,调用get函数获取线程返回内容,在此过程中会阻塞调用者线程,不是真正意义的异步调用
1.8之后新增了ConpletableFuture,不但可以获取线程的执行结果,还不阻塞调用者线程,时真正意义的异步调用
Guava模式
Guava提供了ListenableFuture,实现了类似CompletableFuture方法,使用起来方便
@Slf4j
public class JdkThreadPoolExecutorTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//jdkExecutor();
jdkCompletableFuture();
//guavaExecutor();
//guavaCallbackExecutor();
}
/**
* Jdk 自带的Future测试,调用get方法时仍然会阻塞,不是真正的异步执行
* @throws InterruptedException
* @throws ExecutionException
*/
private static void jdkExecutor() throws InterruptedException, ExecutionException {
ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 5, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10));
Future<String> future = executor.submit(() -> {
Sleeper.sleep(2);
log.info("Hello World");
return "end";
});
log.info("主线程获取future内容:{}", future.get());
Thread.currentThread().join();
executor.shutdown();
}
/**
* 使用CompletableFuture实现真正的异步执行任务
* @throws InterruptedException
*/
private static void jdkCompletableFuture() throws InterruptedException {
CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {
log.info("女神:我开始化妆了,好了通知你");
Sleeper.sleep(5);
return "女神:我画好了";
});
completableFuture.whenComplete((result, exception) -> {
if (exception == null) {
log.info("future的内容:{}", result);
} else{
log.info("女神临时有事,婉拒");
exception.printStackTrace();
}
});
log.info("等待女神的过程玩会游戏");
Thread.currentThread().join();
}
/**
* 使用CompletableFuture实现真正的异步执行任务
* @throws InterruptedException
*/
private static void jdkCompletableFuture2() throws InterruptedException {
CompletableFuture.supplyAsync(() -> {
log.info("女神:我开始化妆了,好了通知你");
Sleeper.sleep(5);
return "女神:我画好了";
}).handleAsync(
(result, exception) -> {
if (exception == null) {
log.info("future的内容:{}", result);
} else{
log.info("女神临时有事,婉拒");
exception.printStackTrace();
}
return null;
}
).thenAcceptAsync(result -> log.info("future的内容:{}", result));
log.info("等待女神的过程玩会游戏");
Thread.currentThread().join();
}
/**
* 使用Guava的异步回调机制实现不阻塞的异步任务
*
* @throws InterruptedException
*/
private static void guavaExecutor() throws InterruptedException {
ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 5, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10));
ListeningExecutorService executorService = MoreExecutors.listeningDecorator(executor);
ListenableFuture<String> listenableFuture = executorService.submit(() -> {
log.info("女神:我开始化妆了,好了通知你");
Sleeper.sleep(5);
return "女神:我画好了";
});
listenableFuture.addListener(() -> {
try {
log.info("future的内容:{}", listenableFuture.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}, executorService);
log.info("等待女神的过程玩会游戏");
Thread.currentThread().join();
executor.shutdown();
}
/**
* 使用Guava的异步回调机制实现不阻塞的异步任务
*
* @throws InterruptedException
*/
private static void guavaCallbackExecutor() throws InterruptedException {
ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 5, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10));
ListeningExecutorService executorService = MoreExecutors.listeningDecorator(executor);
ListenableFuture<String> listenableFuture = executorService.submit(() -> {
log.info("女神:我开始化妆了,好了通知你");
Sleeper.sleep(5);
//return "女神:我画好了";
throw new Exception("男神约我看电影,就不和你吃饭了。");
});
Futures.addCallback(listenableFuture, new FutureCallback<String>() {
@Override
public void onSuccess(@Nullable String s) {
log.info("future的内容:{}", s);
}
@Override
public void onFailure(Throwable throwable) {
log.info("女神临时有事,去不了了");
throwable.printStackTrace();
}
},
executor);
log.info("等待女神的过程玩会游戏");
Thread.currentThread().join();
executor.shutdown();
}
}
7.4 常见的线程池问题
7.4.1 corePoolSize=0会怎样?
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
// 注意这一行代码,添加到等待队列成功后,判断当前池内线程数是否为0,
// 如果是则创建一个firstTask为null的worker,这个worker会从等待队列中获取任务并执行。
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
JDK1.6之后,如果corePoolSize=0,提交任务时如果线程池为空,则会立即创建一个线程来执行任务(先排队后获取),
如果提交任务时,线程池非空,则先在等待队列中排队,只有队列满了之后才会创建新线程。也就是说,1.6之后在队列没有满的这段时间内会有一个线程在消费提交的任务
7.4.2 线程池创建之后,会立即创建核心线程吗?
不会,只有等到有任务提交时才会启动
7.4.3 核心线程数永远不会销毁吗?
JDK1.6之后,提供了allowsCoreThreadTimeOut参数,如果为true,则允许核心线程被销毁
- corePoolSize=0 在一般情况下只使用一个线程消费任务,只要当并发请求特别多,等待队列满了之后才会用多线程
- allowsCoreThreadTimeOut=true && corePoolSize > 1 一开始就使用多线程执行,当请求并发特别多,等待队列满了之后继续加大线程数,但是当请求没有时,允许核心线程终止
7.4.4 如何保证线程不被销毁
通过workQueue.take() 核心线程一直空闲也不会销毁,它会一直在阻塞从等待队列中获取新任务
非核心线程指定时间后通过workQueue.poll(keepliveTime,TimeUnit.NanoSSeconds)实现的,一个空闲的woker只会等待keepLivaTime,如果还没有获取到任务,停止运行
7.4.5 空闲线程过多会有什么问题?
线程池保存空闲的核心线程是它的默认配置,一般是没有问题的。如果业务代码中使用ThreadLocal缓存的数据过大又没有及时清理可能内存泄漏
如果线程数始终处于高位,则需要观察Young GC,适当增加Eden区的大小
7.4.6 keepAliveTime=0?
JDK1.8中,keepAliveTime=0表示非核心线程执行结束后立即终止
7.4.7 线程池中如何处理异常
如果是executor()提交任务,在Runable代码中加try-catch手动捕获异常
如果是submit()提交任务,在主线程中对Future.get()进行try-catch进行异常处理
不管是execute还是submit,都可以在业务代码中加try-catch手动捕获异常。
如果是execute,还可以自定义线程池,继承ThreadPoolExecutor覆写afterSubmit(Runnable r, Throwable t)方法
- 自己在任务中添加捕获异常的代码,如try catch
- 调用future.get方法后,如果正常结束,则返回结果,否则返回异常的堆栈信息
7.4.8 线程池需要关闭吗
一般来讲,线程池的生命周期跟随服务的生命周期,如果一个服务停止了,那么需要调用shutdown()关闭线程池,因而ExecutorService.shutdown()方法在Java以及一些中间件服务中,是封装在业务方法中的shutdown方法中
八 AQS
AbstractQueuedSynchronizer抽象队列同步器,简称同步器,它是同步器的基础框架,如ReentrantLock、CountDownLatch、Semaphore等都是基于它实现的。内部实现了对同步状态的管理、线程的排队,等待通知等底层实现,外部使用时仅需继承它并实现模板方法
8.1 概述
AQD内部维护一个FIFO的队列来管理多线程的排队。公平锁情况下,无法获取同步状态的线程会被封装成一个节点置于队列尾部。入队的线程将会通过自旋的方式获取同步状态,若在有限次的尝试后,仍未获取成功,线程则会被阻塞住。
当头结点释放同步状态后,且后继节点对应的线程被阻塞,此时头节点线程将会去唤醒后继节点,后继节点恢复运行并获取同步状态后会将旧的头节点从队列中移除,并将自己设为头节点,
8.2 源码分析
节点结构Node
static final class Node {
/** 共享类型节点,标记节点在共享模式下等待 */
static final Node SHARED = new Node();
/** 独占类型节点,标记节点在独占模式下等待 */
static final Node EXCLUSIVE = null;
/** 等待状态 - 取消 */
static final int CANCELLED = 1;
/**
* 等待状态 - 通知。某个节点处于该状态,当该节点释放同步状态后,
* 会通知后继节点线程,使之恢复运行
*/
static final int SIGNAL = -1;
/** 等待状态 - 条件等待。表明节点等待在 Condition 上 */
static final int CONDITION = -2;
/**
* 等待状态 - 传播。表示无条件向后传播唤醒动作,详细分析请看第五章
*/
static final int PROPAGATE = -3;
/**
* 等待状态,取值如下:
* SIGNAL,
* CANCELLED,
* CONDITION,
* PROPAGATE,
* 0
*
* 初始情况下,waitStatus = 0
*/
volatile int waitStatus;
/**
* 前驱节点
*/
volatile Node prev;
/**
* 后继节点
*/
volatile Node next;
/**
* 对应的线程
*/
volatile Thread thread;
/**
* 下一个等待节点,用在 ConditionObject 中
*/
Node nextWaiter;
/**
* 判断节点是否是共享节点
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
/**
* 获取前驱节点
*/
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // Used to establish initial head or SHARED marker
}
/** addWaiter 方法会调用该构造方法 */
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}
/** Condition 中会用到此构造方法 */
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
等待状态 | 取值 | 备注 |
---|---|---|
CANCELED | 1 | 取消状态 |
SIGNAL | -1 | 通知。某个节点处于该状态,当该节点释放同步状态后,会通知后继节点 |
CONDITION | -2 | 条件等待,表明该节点等待在Condition上 |
PROPAGATE | -3 | 传播,表示无条件向后传播唤醒动作 |
INIT | 0 | 初始状态(默认值) |
独占模式
获取同步状态
- 调用tryAcquire方法尝试获取同步状态
- 获取成功,直接返回
- 获取失败,将线程封装到节点中,并将节点入队
- 入队节点在acquireQueued方法中自旋获取同步状态
- 若节点的前驱节点是头节点,则再次调用tryAcquire尝试获取同步状态
- 获取成功,当前节点设置为头节点并返回
- 获取失败,视情况可能再次重试也可能被阻塞
/**
* 该方法将会调用子类复写的 tryAcquire 方法获取同步状态,
* - 获取成功:直接返回
* - 获取失败:将线程封装在节点中,并将节点置于同步队列尾部,
* 通过自旋尝试获取同步状态。如果在有限次内仍无法获取同步状态,
* 该线程将会被 LockSupport.park 方法阻塞住,直到被前驱节点唤醒
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
/** 向同步队列尾部添加一个节点 */
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// 尝试以快速将节点添加到队列尾部
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 快速插入节点失败,调用 enq 方法,不停的尝试插入节点
enq(node);
return node;
}
/**
* 通过 CAS + 自旋的方式插入节点到队尾
*/
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
// 设置头结点,初始情况下,头结点是一个空节点
if (compareAndSetHead(new Node()))
tail = head;
} else {
/*
* 将节点插入队列尾部。这里先将新节点的前驱设为尾节点,之后再尝试将新节点设为尾节
* 点,最后再将原尾节点的后继节点指向新的尾节点。除了这种方式,我们还先设置尾节点,
* 之后再设置前驱和后继,即:
*
* if (compareAndSetTail(t, node)) {
* node.prev = t;
* t.next = node;
* }
*
* 但如果是这样做,会导致一个问题,即短时间内,队列结构会遭到破坏。考虑这种情况,
* 某个线程在调用 compareAndSetTail(t, node)成功后,该线程被 CPU 切换了。此时
* 设置前驱和后继的代码还没来得及及执行,但尾节点指针却设置成功,导致队列结构短时内会
* 出现如下情况:
*
* +------+ prev +-----+ +-----+
* head | | <---- | | | | tail
* | | ----> | | | |
* +------+ next +-----+ +-----+
*
* tail 节点完全脱离了队列,这样导致一些队列遍历代码出错。如果先设置
* 前驱,再设置尾节点。即使线程被切换,队列结构短时可能如下:
*
* +------+ prev +-----+ prev +-----+
* head | | <---- | | <---- | | tail
* | | ----> | | | |
* +------+ next +-----+ +-----+
*
* 这样并不会影响从后向前遍历,不会导致遍历逻辑出错。
*
* 参考:
* https://www.cnblogs.com/micrari/p/6937995.html
*/
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
/**
* 同步队列中的线程在此方法中以循环尝试获取同步状态,在有限次的尝试后,
* 若仍未获取锁,线程将会被阻塞,直至被前驱节点的线程唤醒。
*/
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
// 循环获取同步状态
for (;;) {
final Node p = node.predecessor();
/*
* 前驱节点如果是头结点,表明前驱节点已经获取了同步状态。前驱节点释放同步状态后,
* 在不出异常的情况下, tryAcquire(arg) 应返回 true。此时节点就成功获取了同
* 步状态,并将自己设为头节点,原头节点出队。
*/
if (p == head && tryAcquire(arg)) {
// 成功获取同步状态,设置自己为头节点
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
/*
* 如果获取同步状态失败,则根据条件判断是否应该阻塞自己。
* 如果不阻塞,CPU 就会处于忙等状态,这样会浪费 CPU 资源
*/
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
/*
* 如果在获取同步状态中出现异常,failed = true,cancelAcquire 方法会被执行。
* tryAcquire 需同步组件开发者覆写,难免不了会出现异常。
*/
if (failed)
cancelAcquire(node);
}
}
/** 设置头节点 */
private void setHead(Node node) {
// 仅有一个线程可以成功获取同步状态,所以这里不需要进行同步控制
head = node;
node.thread = null;
node.prev = null;
}
/**
* 该方法主要用途是,当线程在获取同步状态失败时,根据前驱节点的等待状态,决定后续的动作。比如前驱
* 节点等待状态为 SIGNAL,表明当前节点线程应该被阻塞住了。不能老是尝试,避免 CPU 忙等。
* —————————————————————————————————————————————————————————————————
* | 前驱节点等待状态 | 相应动作 |
* —————————————————————————————————————————————————————————————————
* | SIGNAL | 阻塞 |
* | CANCELLED | 向前遍历, 移除前面所有为该状态的节点 |
* | waitStatus < 0 | 将前驱节点状态设为 SIGNAL, 并再次尝试获取同步状态 |
* —————————————————————————————————————————————————————————————————
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
/*
* 前驱节点等待状态为 SIGNAL,表示当前线程应该被阻塞。
* 线程阻塞后,会在前驱节点释放同步状态后被前驱节点线程唤醒
*/
if (ws == Node.SIGNAL)
return true;
/*
* 前驱节点等待状态为 CANCELLED,则以前驱节点为起点向前遍历,
* 移除其他等待状态为 CANCELLED 的节点。
*/
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* 等待状态为 0 或 PROPAGATE,设置前驱节点等待状态为 SIGNAL,
* 并再次尝试获取同步状态。
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
private final boolean parkAndCheckInterrupt() {
// 调用 LockSupport.park 阻塞自己
LockSupport.park(this);
return Thread.interrupted();
}
/**
* 取消获取同步状态
*/
private void cancelAcquire(Node node) {
if (node == null)
return;
node.thread = null;
// 前驱节点等待状态为 CANCELLED,则向前遍历并移除其他为该状态的节点
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// 记录 pred 的后继节点,后面会用到
Node predNext = pred.next;
// 将当前节点等待状态设为 CANCELLED
node.waitStatus = Node.CANCELLED;
/*
* 如果当前节点是尾节点,则通过 CAS 设置前驱节点 prev 为尾节点。设置成功后,再利用 CAS 将
* prev 的 next 引用置空,断开与后继节点的联系,完成清理工作。
*/
if (node == tail && compareAndSetTail(node, pred)) {
/*
* 执行到这里,表明 pred 节点被成功设为了尾节点,这里通过 CAS 将 pred 节点的后继节点
* 设为 null。注意这里的 CAS 即使失败了,也没关系。失败了,表明 pred 的后继节点更新
* 了。pred 此时已经是尾节点了,若后继节点被更新,则是有新节点入队了。这种情况下,CAS
* 会失败,但失败不会影响同步队列的结构。
*/
compareAndSetNext(pred, predNext, null);
} else {
int ws;
// 根据条件判断是唤醒后继节点,还是将前驱节点和后继节点连接到一起
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
if (next != null && next.waitStatus <= 0)
/*
* 这里使用 CAS 设置 pred 的 next,表明多个线程同时在取消,这里存在竞争。
* 不过此处没针对 compareAndSetNext 方法失败后做一些处理,表明即使失败了也
* 没关系。实际上,多个线程同时设置 pred 的 next 引用时,只要有一个能设置成
* 功即可。
*/
compareAndSetNext(pred, predNext, next);
} else {
/*
* 唤醒后继节点对应的线程。这里简单讲一下为什么要唤醒后继线程,考虑下面一种情况:
* head node1 node2 tail
* ws=0 ws=1 ws=-1 ws=0
* +------+ prev +-----+ prev +-----+ prev +-----+
* | | <---- | | <---- | | <---- | |
* | | ----> | | ----> | | ----> | |
* +------+ next +-----+ next +-----+ next +-----+
*
* 头结点初始状态为 0,node1、node2 和 tail 节点依次入队。node1 自旋过程中调用
* tryAcquire 出现异常,进入 cancelAcquire。head 节点此时等待状态仍然是 0,它
* 会认为后继节点还在运行中,所它在释放同步状态后,不会去唤醒后继等待状态为非取消的
* 节点 node2。如果 node1 再不唤醒 node2 的线程,该线程面临无法被唤醒的情况。此
* 时,整个同步队列就回全部阻塞住。
*/
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
/*
* 通过 CAS 将等待状态设为 0,让后继节点线程多一次
* 尝试获取同步状态的机会
*/
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
/*
* 这里如果 s == null 处理,是不是表明 node 是尾节点?答案是不一定。原因之前在分析
* enq 方法时说过。这里再啰嗦一遍,新节点入队时,队列瞬时结构可能如下:
* node1 node2
* +------+ prev +-----+ prev +-----+
* head | | <---- | | <---- | | tail
* | | ----> | | | |
* +------+ next +-----+ +-----+
*
* node2 节点为新入队节点,此时 tail 已经指向了它,但 node1 后继引用还未设置。
* 这里 node1 就是 node 参数,s = node1.next = null,但此时 node1 并不是尾
* 节点。所以这里不能从前向后遍历同步队列,应该从后向前。
*/
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
// 唤醒 node 的后继节点线程
LockSupport.unpark(s.thread);
}
释放同步状态
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
/*
* 这里简单列举条件分支的可能性,如下:
* 1. head = null
* head 还未初始化。初始情况下,head = null,当第一个节点入队后,head 会被初始
* 为一个虚拟(dummy)节点。这里,如果还没节点入队就调用 release 释放同步状态,
* 就会出现 h = null 的情况。
*
* 2. head != null && waitStatus = 0
* 表明后继节点对应的线程仍在运行中,不需要唤醒
*
* 3. head != null && waitStatus < 0
* 后继节点对应的线程可能被阻塞了,需要唤醒
*/
if (h != null && h.waitStatus != 0)
// 唤醒后继节点,上面分析过了,这里不再赘述
unparkSuccessor(h);
return true;
}
return false;
}
共享模式
共享模式下,同一时刻会有多个线程获取共享同步状态,此模式是读写锁中的读锁、CountDownLatch、Semaphore等同步组件的基础
获取同步状态
public final void acquireShared(int arg) {
// 尝试获取共享同步状态,tryAcquireShared 返回的是整型
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
// 这里和前面一样,也是通过有限次自旋的方式获取同步状态
for (;;) {
final Node p = node.predecessor();
/*
* 前驱是头结点,其类型可能是 EXCLUSIVE,也可能是 SHARED.
* 如果是 EXCLUSIVE,线程无法获取共享同步状态。
* 如果是 SHARED,线程则可获取共享同步状态。
* 能不能获取共享同步状态要看 tryAcquireShared 具体的实现。比如多个线程竞争读写
* 锁的中的读锁时,均能成功获取读锁。但多个线程同时竞争信号量时,可能就会有一部分线
* 程因无法竞争到信号量资源而阻塞。
*/
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);
}
}
/**
* 这个方法做了两件事情:
* 1. 设置自身为头结点
* 2. 根据条件判断是否要唤醒后继节点
*/
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
// 设置头结点
setHead(node);
/*
* 这个条件分支由 propagate > 0 和 h.waitStatus < 0 两部分组成。
* h.waitStatus < 0 时,waitStatus = SIGNAL 或 PROPAGATE。这里仅依赖
* 条件 propagate > 0 判断是否唤醒后继节点是不充分的,至于原因请参考第五章
*/
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
/*
* 节点 s 如果是共享类型节点,则应该唤醒该节点
* 至于 s == null 的情况前面分析过,这里不在赘述。
*/
if (s == null || s.isShared())
doReleaseShared();
}
}
/**
* 该方法用于在 acquires/releases 存在竞争的情况下,确保唤醒动作向后传播。
*/
private void doReleaseShared() {
/*
* 下面的循环在 head 节点存在后继节点的情况下,做了两件事情:
* 1. 如果 head 节点等待状态为 SIGNAL,则将 head 节点状态设为 0,并唤醒后继节点
* 2. 如果 head 节点等待状态为 0,则将 head 节点状态设为 PROPAGATE,保证唤醒能够正
* 常传播下去。关于 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);
}
/*
* ws = 0 的情况下,这里要尝试将状态从 0 设为 PROPAGATE,保证唤醒向后
* 传播。setHeadAndPropagate 在读到 h.waitStatus < 0 时,可以继续唤醒
* 后面的节点。
*/
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
- 获取同步共享状态
- 如果获取成功,则生成节点并入列
- 否则如果前驱为头节点,再次尝试获取同步状态
- 获取成功则将自己设为头节点,如果后继节点是共享类型的,则唤醒
- 若失败,将节点状态设为SIGNAL,再次尝试,若再次失败,则进入等待状态
释放共享状态
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
唤醒线程有两种方式:unpark()和interrupt()
Condition条件
AQS中使用Condition来管理各种等待队列,内部封装了ConditionObject对象,扩展了响应中断、超时获取等功能
ConditionObject是基于单链表的条件队列来管理等待线程的,线程在调用await方法释放同步状态后,会被封装到一个等待节点中,并将该节点置为等待队列的尾部。当有线程在获取独占锁的情况下调用signal或signalAll时,队列中的等待线程会被叫醒,重新竞争锁。同时一个锁对象可同时创建多个ConditionObject对象,意味着多个竞争同一把独占锁的线程可在不同的条件队列中等待,在唤醒时,也可以唤醒指定条件队列中的线程。
signal
/**
* await 是一个响应中断的等待方法,主要逻辑流程如下:
* 1. 如果线程中断了,抛出 InterruptedException 异常
* 2. 将线程封装到节点对象里,并将节点添加到条件队列尾部
* 3. 保存并完全释放同步状态,保存下来的同步状态在重新竞争锁时会用到
* 4. 线程进入等待状态,直到被通知或中断才会恢复运行
* 5. 使用第3步保存的同步状态去竞争独占锁
*/
public final void await() throws InterruptedException {
// 线程中断,则抛出中断异常,对应步骤1
if (Thread.interrupted())
throw new InterruptedException();
// 添加等待节点到条件队列尾部,对应步骤2
Node node = addConditionWaiter();
// 保存并完全释放同步状态,对应步骤3。此方法的意义会在后面详细说明。
int savedState = fullyRelease(node);
int interruptMode = 0;
/*
* 判断节点是否在同步队列上,如果不在则阻塞线程。
* 循环结束的条件:
* 1. 其他线程调用 singal/singalAll,node 将会被转移到同步队列上。node 对应线程将
* 会在获取同步状态的过程中被唤醒,并走出 while 循环。
* 2. 线程在阻塞过程中产生中断
*/
while (!isOnSyncQueue(node)) {
// 调用 LockSupport.park 阻塞当前线程,对应步骤4
LockSupport.park(this);
/*
* 检测中断模式,这里有两种中断模式,如下:
* THROW_IE:
* 中断在 node 转移到同步队列“前”发生,需要当前线程自行将 node 转移到同步队
* 列中,并在随后抛出 InterruptedException 异常。
*
* REINTERRUPT:
* 中断在 node 转移到同步队列“期间”或“之后”发生,此时表明有线程正在调用
* singal/singalAll 转移节点。在该种中断模式下,再次设置线程的中断状态。
* 向后传递中断标志,由后续代码去处理中断。
*/
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
/*
* 被转移到同步队列的节点 node 将在 acquireQueued 方法中重新获取同步状态,注意这里
* 的这里的 savedState 是上面调用 fullyRelease 所返回的值,与此对应,可以把这里的
* acquireQueued 作用理解为 fullyAcquire(并不存在这个方法)。
*
* 如果上面的 while 循环没有产生中断,则 interruptMode = 0。但 acquireQueued 方法
* 可能会产生中断,产生中断时返回 true。这里仍将 interruptMode 设为 REINTERRUPT,
* 目的是继续向后传递中断,acquireQueued 不会处理中断。
*/
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
/*
* 正常通过 singal/singalAll 转移节点到同步队列时,nextWaiter 引用会被置空。
* 若发生线程产生中断(THROW_IE)或 fullyRelease 方法出现错误等异常情况,
* 该引用则不会被置空
*/
if (node.nextWaiter != null) // clean up if cancelled
// 清理等待状态非 CONDITION 的节点
unlinkCancelledWaiters();
if (interruptMode != 0)
/*
* 根据 interruptMode 觉得中断的处理方式:
* THROW_IE:抛出 InterruptedException 异常
* REINTERRUPT:重新设置线程中断标志
*/
reportInterruptAfterWait(interruptMode);
}
/** 将当先线程封装成节点,并将节点添加到条件队列尾部 */
private Node addConditionWaiter() {
Node t = lastWaiter;
/*
* 清理等待状态为 CANCELLED 的节点。fullyRelease 内部调用 release 发生异常或释放同步状
* 态失败时,节点的等待状态会被设置为 CANCELLED。所以这里要清理一下已取消的节点
*/
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
// 创建节点,并将节点置于队列尾部
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
/** 清理等待状态为 CANCELLED 的节点 */
private void unlinkCancelledWaiters() {
Node t = firstWaiter;
// 指向上一个等待状态为非 CANCELLED 的节点
Node trail = null;
while (t != null) {
Node next = t.nextWaiter;
if (t.waitStatus != Node.CONDITION) {
t.nextWaiter = null;
/*
* trail 为 null,表明 next 之前的节点等待状态均为 CANCELLED,此时更新
* firstWaiter 引用的指向。
* trail 不为 null,表明 next 之前有节点的等待状态为 CONDITION,这时将
* trail.nextWaiter 指向 next 节点。
*/
if (trail == null)
firstWaiter = next;
else
trail.nextWaiter = next;
// next 为 null,表明遍历到条件队列尾部了,此时将 lastWaiter 指向 trail
if (next == null)
lastWaiter = trail;
}
else
// t.waitStatus = Node.CONDITION,则将 trail 指向 t
trail = t;
t = next;
}
}
/**
* 这个方法用于完全释放同步状态。这里解释一下完全释放的原因:为了避免死锁的产生,锁的实现上
* 一般应该支持重入功能。对应的场景就是一个线程在不释放锁的情况下可以多次调用同一把锁的
* lock 方法进行加锁,且不会加锁失败,如失败必然导致导致死锁。锁的实现类可通过 AQS 中的整型成员
* 变量 state 记录加锁次数,每次加锁,将 state++。每次 unlock 方法释放锁时,则将 state--,
* 直至 state = 0,线程完全释放锁。用这种方式即可实现了锁的重入功能。
*/
final int fullyRelease(Node node) {
boolean failed = true;
try {
// 获取同步状态数值
int savedState = getState();
// 调用 release 释放指定数量的同步状态
if (release(savedState)) {
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
// 如果 relase 出现异常或释放同步状态失败,此处将 node 的等待状态设为 CANCELLED
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
/** 该方法用于判断节点 node 是否在同步队列上 */
final boolean isOnSyncQueue(Node node) {
/*
* 节点在同步队列上时,其状态可能为 0、SIGNAL、PROPAGATE 和 CANCELLED 其中之一,
* 但不会为 CONDITION,所以可已通过节点的等待状态来判断节点所处的队列。
*
* node.prev 仅会在节点获取同步状态后,调用 setHead 方法将自己设为头结点时被置为
* null,所以只要节点在同步队列上,node.prev 一定不会为 null
*/
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
/*
* 如果节点后继被为 null,则表明节点在同步队列上。因为条件队列使用的是 nextWaiter 指
* 向后继节点的,条件队列上节点的 next 指针均为 null。但仅以 node.next != null 条
* 件断定节点在同步队列是不充分的。节点在入队过程中,是先设置 node.prev,后设置
* node.next。如果设置完 node.prev 后,线程被切换了,此时 node.next 仍然为
* null,但此时 node 确实已经在同步队列上了,所以这里还需要进行后续的判断。
*/
if (node.next != null)
return true;
// 在同步队列上,从后向前查找 node 节点
return findNodeFromTail(node);
}
/** 由于同步队列上的的节点 prev 引用不会为空,所以这里从后向前查找 node 节点 */
private boolean findNodeFromTail(Node node) {
Node t = tail;
for (;;) {
if (t == node)
return true;
if (t == null)
return false;
t = t.prev;
}
}
/** 检测线程在等待期间是否发生了中断 */
private int checkInterruptWhileWaiting(Node node) {
return Thread.interrupted() ?
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
}
/**
* 判断中断发生的时机,分为两种:
* 1. 中断在节点被转移到同步队列前发生,此时返回 true
* 2. 中断在节点被转移到同步队列期间或之后发生,此时返回 false
*/
final boolean transferAfterCancelledWait(Node node) {
// 中断在节点被转移到同步队列前发生,此时自行将节点转移到同步队列上,并返回 true
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
// 调用 enq 将节点转移到同步队列中
enq(node);
return true;
}
/*
* 如果上面的条件分支失败了,则表明已经有线程在调用 signal/signalAll 方法了,这两个
* 方法会先将节点等待状态由 CONDITION 设置为 0 后,再调用 enq 方法转移节点。下面判断节
* 点是否已经在同步队列上的原因是,signal/signalAll 方法可能仅设置了等待状态,还没
* 来得及转移节点就被切换走了。所以这里用自旋的方式判断 signal/signalAll 是否已经完
* 成了转移操作。这种情况表明了中断发生在节点被转移到同步队列期间。
*/
while (!isOnSyncQueue(node))
Thread.yield();
}
// 中断在节点被转移到同步队列期间或之后发生,返回 false
return false;
}
/**
* 根据中断类型做出相应的处理:
* THROW_IE:抛出 InterruptedException 异常
* REINTERRUPT:重新设置中断标志,向后传递中断
*/
private void reportInterruptAfterWait(int interruptMode)
throws InterruptedException {
if (interruptMode == THROW_IE)
throw new InterruptedException();
else if (interruptMode == REINTERRUPT)
selfInterrupt();
}
/** 中断线程 */
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
notify
/** 将条件队列中的头结点转移到同步队列中 */
public final void signal() {
// 检查线程是否获取了独占锁,未获取独占锁调用 signal 方法是不允许的
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
// 将头结点转移到同步队列中
doSignal(first);
}
private void doSignal(Node first) {
do {
/*
* 将 firstWaiter 指向 first 节点的 nextWaiter 节点,while 循环将会用到更新后的
* firstWaiter 作为判断条件。
*/
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
// 将头结点从条件队列中移除
first.nextWaiter = null;
/*
* 调用 transferForSignal 将节点转移到同步队列中,如果失败,且 firstWaiter
* 不为 null,则再次进行尝试。transferForSignal 成功了,while 循环就结束了。
*/
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
/** 这个方法用于将条件队列中的节点转移到同步队列中 */
final boolean transferForSignal(Node node) {
/*
* 如果将节点的等待状态由 CONDITION 设为 0 失败,则表明节点被取消。
* 因为 transferForSignal 中不存在线程竞争的问题,所以下面的 CAS
* 失败的唯一原因是节点的等待状态为 CANCELLED。
*/
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
// 调用 enq 方法将 node 转移到同步队列中,并返回 node 的前驱节点 p
Node p = enq(node);
int ws = p.waitStatus;
/*
* 如果前驱节点的等待状态 ws > 0,则表明前驱节点处于取消状态,此时应唤醒 node 对应的
* 线程去获取同步状态。如果 ws <= 0,这里通过 CAS 将节点 p 的等待设为 SIGNAL。
* 这样,节点 p 在释放同步状态后,才会唤醒后继节点 node。如果 CAS 设置失败,则应立即
* 唤醒 node 节点对应的线程。以免因 node 没有被唤醒导致同步队列挂掉。关于同步队列的相关的
* 知识,请参考我的另一篇文章“AbstractQueuedSynchronizer 原理分析 - 独占/共享模式”,
* 链接为:http://t.cn/RuERpHl
*/
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
8.3 ReentrentLock
可重入锁,内部使用AQS实现,功能类似于Synchronized,但支持可中断、非公平锁、以及超时获取等功能
特性 | synchronized | ReentrantLock | 相同 |
---|---|---|---|
可重入 | 是 | 是 | ✅ |
响应中断 | 否 | 是 | ❌ |
超时等待 | 否 | 是 | ❌ |
公平锁 | 否 | 是 | ❌ |
非公平锁 | 是 | 是 | ✅ |
是否可尝试加锁 | 否 | 是 | ❌ |
是否是Java内置特性 | 是 | 否 | ❌ |
自动获取/释放锁 | 是 | 否 | ❌ |
对异常的处理 | 自动释放锁 | 需手动释放锁 | ❌ |
8.3.1 原理
内部有一个继承自AQS的Sync内部类,NonfailSync和FairSync则继承自Sync,从而实现公平与非公平锁
8.3.2 公平锁
+--- ReentrantLock.FairSync.java
final void lock() {
// 调用 AQS acquire 获取锁
acquire(1);
}
+--- AbstractQueuedSynchronizer.java
/**
* 该方法主要做了三件事情:
* 1. 调用 tryAcquire 尝试获取锁,该方法需由 AQS 的继承类实现,获取成功直接返回
* 2. 若 tryAcquire 返回 false,则调用 addWaiter 方法,将当前线程封装成节点,
* 并将节点放入同步队列尾部
* 3. 调用 acquireQueued 方法让同步队列中的节点循环尝试获取锁
*/
public final void acquire(int arg) {
// acquireQueued 和 addWaiter 属于 AQS 中的方法,这里不展开分析了
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
+--- ReentrantLock.FairSync.java
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// 获取同步状态
int c = getState();
// 如果同步状态 c 为0,表示锁暂时没被其他线程获取
if (c == 0) {
/*
* 判断是否有其他线程等待的时间更长。如果有,应该先让等待时间更长的节点先获取锁。
* 如果没有,调用 compareAndSetState 尝试设置同步状态。
*/
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
// 将当前线程设置为持有锁的线程
setExclusiveOwnerThread(current);
return true;
}
}
// 如果当前线程为持有锁的线程,则执行重入逻辑
else if (current == getExclusiveOwnerThread()) {
// 计算重入后的同步状态,acquires 一般为1
int nextc = c + acquires;
// 如果重入次数超过限制,这里会抛出异常
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
// 设置重入后的同步状态
setState(nextc);
return true;
}
return false;
}
+--- AbstractQueuedSynchronizer.java
/** 该方法用于判断同步队列中有比当前线程等待时间更长的线程 */
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
/*
* 在同步队列中,头结点是已经获取了锁的节点,头结点的后继节点则是即将获取锁的节点。
* 如果有节点对应的线程等待的时间比当前线程长,则返回 true,否则返回 false
*/
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
- 调用acquire方法,将线程放入同步队列中的等待
- 如果成功获取到锁,则将自己设置为持有锁的线程并返回
- 如同步状态不为0,且当前线程为持有线程,则执行重入机制
8.3.3 非公平锁
+--- ReentrantLock.NonfairSync
final void lock() {
/*
* 这里调用直接 CAS 设置 state 变量,如果设置成功,表明加锁成功。这里并没有像公平锁
* 那样调用 acquire 方法让线程进入同步队列进行排队,而是直接调用 CAS 抢占锁。抢占失败
* 再调用 acquire 方法将线程置于队列尾部排队。
*/
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
+--- AbstractQueuedSynchronizer
/** 参考上一节的分析 */
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
+--- ReentrantLock.NonfairSync
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
+--- ReentrantLock.Sync
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// 获取同步状态
int c = getState();
// 如果同步状态 c = 0,表明锁当前没有线程获得,此时可加锁。
if (c == 0) {
// 调用 CAS 加锁,如果失败,则说明有其他线程在竞争获取锁
if (compareAndSetState(0, acquires)) {
// 设置当前线程为锁的持有线程
setExclusiveOwnerThread(current);
return true;
}
}
// 如果当前线程已经持有锁,此处条件为 true,表明线程需再次获取锁,也就是重入
else if (current == getExclusiveOwnerThread()) {
// 计算重入后的同步状态值,acquires 一般为1
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 设置新的同步状态值
setState(nextc);
return true;
}
return false;
}
- 使用compareAndSetState方法抢占式获取锁,加锁成功则将自己设为持有锁的线程并返回
- 如加锁失败,则调用acquire方法将线程放入同步队列尾部等待
- 线程在同步队列中获取到锁,则将自己设为持有锁的线程并返回
- 若同步状态不为0 且当前线程为持有锁线程,则执行重入逻辑
8.3.4 公平锁 VS 非公平锁
- 从源码上看,非公平锁会首先尝试获取锁,而公平锁会直接调用acquire将线程放入同步队列中等待
- 通常情况下,非公平锁比公平锁效率更高
- CPU在恢复一个被挂起的线程与该线程真正执行之间存在着严重的延迟
- 公平锁线程的切换次数要远多与非公平锁
8.4 Semaphore
信号量,定义了共享资源的数量,类似于一个停车场的车位,每个汽车都是一个线程,每来一辆汽车,车位就减一,直到车位为0,不允许汽车进入。
public class SemaphoreTest {
static class Parking {
//信号量
private Semaphore semaphore;
Parking(int count) {
semaphore = new Semaphore(count);
}
private void park() {
try {
//获取信号量
semaphore.acquire();
long time =(long) (Math.random() * 10);
System.out.println(Thread.currentThread().getName()
+ "进入停车场,停车" + time +"秒...");
Thread.sleep(time);
System.out.println(Thread.currentThread().getName()
+ "开出停车厂...");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
}
}
static class Car extends Thread {
Parking parking;
Car(Parking parking) {
this.parking = parking;
}
@Override
public void run() {
parking.park(); //进入停车厂
}
}
public static void main(String[] args) {
Parking parking = new Parking(3);
for (int i = 0; i < 5; i++) {
new Car(parking).start();
}
}
}
8.4.1 原理
内部也定义了一个继承自AQS的内部类Sync,NonfairSync和FairSync分别继承自Sync。
// 实际上最终将AQS中state置为count
protected final void setState(int newState) {
state = newState;
}
8.5 BlockingQueue
8.5.1 概述
在新增的Concurrent包中,BlockingQueue很好的解决了多线程中,如何高效安全“传输”数据的问题。通过这些高效并且线程安全的队列类,为我们快速搭建高质量的多线程程序带来极大的便利。通俗理解就是线程通信的一个工具,在任意时刻不管并发有多高,在单JVM上同一时刻永远只会有一个线程能够对队列进行入队或出队操作,应用于SpringCloud-Eureka的三级缓存、Nacos、Netty、MQ等场景
队列 | 有界性 | 锁 | 数据结构 |
---|---|---|---|
ArrayBlockingQueue | bounded(有界) | 加锁 | arrayList |
LinkedBlockingQueue | optionally-bounded | 加锁 | linkedList |
PriorityBlockingQueue | unbounded | 加锁 | heap |
DelayQueue | unbounded | 加锁 | heap |
SynchronousQueue | bounded | 加锁 | 无 |
LinkedTransferQueue | unbounded | 加锁 | heap |
LinkedBlockingDeque | unbounded | 无锁 | heap |
-
ArrayBlockingQueue:是一个用数组实现的有界阻塞队列,此队列按照先进先出(FIFO)的原则对元素进行排序。支持公平锁和非公平锁。【注:每一个线程在获取锁的时候可能都会排队等待,如果在等待时间上,先获取锁的线程的请求一定先被满足,那么这个锁就是公平的。反之,这个锁就是不公平的。公平的获取锁,也就是当前等待时间最长的线程先获取锁】
-
LinkedBlockingQueue:一个由链表结构组成的有界队列,此队列的长度为Integer.MAX_VALUE。此队列按照先进先出的顺序进行排序。
-
PriorityBlockingQueue: 一个支持线程优先级排序的无界队列,默认自然序进行排序,也可以自定义实现compareTo()方法来指定元素排序规则,不能保证同优先级元素的顺序。
-
DelayQueue: 一个实现PriorityBlockingQueue实现延迟获取的无界队列,在创建元素时,可以指定多久才能从队列中获取当前元素。只有延时期满后才能从队列中获取元素。
DelayQueue可以运用在以下应用场景:
1.缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。
2.定时任务调度。使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,从比如TimerQueue就是使用DelayQueue实现的。
-
SynchronousQueue: 一个不存储元素的阻塞队列,每一个put操作必须等待take操作,否则不能添加元素。支持公平锁和非公平锁。SynchronousQueue的一个使用场景是在线程池里。Executors.newCachedThreadPool()就使用了SynchronousQueue,这个线程池根据需要(新任务到来时)创建新的线程,如果有空闲线程则会重复使用,线程空闲了60秒后会被回收。
-
LinkedTransferQueue: 一个由链表结构组成的无界阻塞队列,相当于其它队列,LinkedTransferQueue队列多了transfer和tryTransfer方法。
-
LinkedBlockingDeque: 一个由链表结构组成的双向阻塞队列。队列头部和尾部都可以添加和移除元素,多线程并发时,可以将锁的竞争最多降到一半。
下面以ArrayBlockingQueue为例,详解它的使用方法
8.5.1 ArrayBlockingQueue
构造方法
// 存储队列元素的数组
final Object[] items;
// 拿数据的索引,用于take,poll,peek,remove方法
int takeIndex;
// 放数据的索引,用于put,offer,add方法
int putIndex;
// 元素个数
int count;
// 可重入锁
final ReentrantLock lock;
// notEmpty条件对象,由lock创建
private final Condition notEmpty;
// notFull条件对象,由lock创建
private final Condition notFull;
public ArrayBlockingQueue(int capacity) {
this(capacity, false);//默认构造非公平锁的阻塞队列
}
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
//初始化ReentrantLock重入锁,出队入队拥有这同一个锁
lock = new ReentrantLock(fair);
//初始化非空等待队列
notEmpty = lock.newCondition();
//初始化非满等待队列
notFull = lock.newCondition();
}
public ArrayBlockingQueue(int capacity, boolean fair,
Collection<? extends E> c) {
this(capacity, fair);
final ReentrantLock lock = this.lock;
lock.lock(); // Lock only for visibility, not mutual exclusion
try {
int i = 0;
//将集合添加进数组构成的队列中
try {
for (E e : c) {
checkNotNull(e);
items[i++] = e;
}
} catch (ArrayIndexOutOfBoundsException ex) {
throw new IllegalArgumentException();
}
count = i;
putIndex = (i == capacity) ? 0 : i;
} finally {
lock.unlock();
}
}
入队
//入队操作
private void enqueue(E x) {
final Object[] items = this.items;
//通过putIndex索引对数组进行赋值
items[putIndex] = x;
//索引自增,如果已是最后一个位置,重新设置 putIndex = 0;
if (++putIndex == items.length)
putIndex = 0;
count++;
notEmpty.signal();
}
这里的add方法和offer方法最终调用的是enqueue(E x)方法,其方法内部通过putIndex索引直接将元素添加到数组items中,这里可能会疑惑的是当putIndex索引大小等于数组长度时,需要将putIndex重新设置为0,这是因为当前队列执行元素获取时总是从队列头部获取,而添加元素从中从队列尾部获取所以当队列索引(从0开始)与数组长度相等时,下次我们就需要从数组头部开始添加了,如上图演示
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
//当队列元素个数与数组长度相等时,无法添加元素
while (count == items.length)
//将当前调用线程挂起,添加到notFull条件队列中等待唤醒
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}
put方法是一个阻塞的方法,如果队列元素已满,那么当前线程将会被notFull条件对象挂起加到等待队列中,直到队列有空档才会唤醒执行添加操作。但如果队列没有满,那么就直接调用enqueue(e)方法将元素加入到数组队列中。到此我们对三个添加方法即put,offer,add都分析完毕,其中offer,add在正常情况下都是无阻塞的添加,而put方法是阻塞添加。这就是阻塞队列的添加过程。说白了就是当队列满时通过条件对象Condtion来阻塞当前调用put方法的线程,直到线程又再次被唤醒执行。总得来说添加线程的执行存在以下两种情况,
1 队列已满,那么新到来的put线程将添加到notFull的条件队列中等待,
2 有移除线程执行移除操作,移除成功同时唤醒put线程,如上图所示
出队
poll方法,该方法获取并移除此队列的头元素,若队列为空,则返回 null
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
//判断队列是否为null,不为null执行dequeue()方法,否则返回null
return (count == 0) ? null : dequeue();
} finally {
lock.unlock();
}
}
//删除队列头元素并返回
private E dequeue() {
//拿到当前数组的数据
final Object[] items = this.items;
@SuppressWarnings("unchecked")
//获取要删除的对象
E x = (E) items[takeIndex];
// 将数组中takeIndex索引位置设置为null
items[takeIndex] = null;
//takeIndex索引加1并判断是否与数组长度相等,
//如果相等说明已到尽头,恢复为0
if (++takeIndex == items.length)
takeIndex = 0;
count--;//队列个数减1
if (itrs != null)
itrs.elementDequeued();//同时更新迭代器中的元素数据
//删除了元素说明队列有空位,唤醒notFull条件对象添加线程,执行添加操作
notFull.signal();
return x;
}
remove方法
public boolean remove(Object o) {
if (o == null) return false;
//获取数组数据
final Object[] items = this.items;
final ReentrantLock lock = this.lock;
lock.lock();//加锁
try {
//如果此时队列不为null,这里是为了防止并发情况
if (count > 0) {
//获取下一个要添加元素时的索引
final int putIndex = this.putIndex;
//获取当前要被删除元素的索引
int i = takeIndex;
//执行循环查找要删除的元素
do {
//找到要删除的元素
if (o.equals(items[i])) {
removeAt(i);//执行删除
return true;//删除成功返回true
}
//当前删除索引执行加1后判断是否与数组长度相等
//若为true,说明索引已到数组尽头,将i设置为0
if (++i == items.length)
i = 0;
} while (i != putIndex);//继承查找
}
return false;
} finally {
lock.unlock();
}
}
//根据索引删除元素,实际上是把删除索引之后的元素往前移动一个位置
void removeAt(final int removeIndex) {
final Object[] items = this.items;
//先判断要删除的元素是否为当前队列头元素
if (removeIndex == takeIndex) {
//如果是直接删除
items[takeIndex] = null;
//当前队列头元素加1并判断是否与数组长度相等,若为true设置为0
if (++takeIndex == items.length)
takeIndex = 0;
count--;//队列元素减1
if (itrs != null)
itrs.elementDequeued();//更新迭代器中的数据
} else {
//如果要删除的元素不在队列头部,
//那么只需循环迭代把删除元素后面的所有元素往前移动一个位置
//获取下一个要被添加的元素的索引,作为循环判断结束条件
final int putIndex = this.putIndex;
//执行循环
for (int i = removeIndex;;) {
//获取要删除节点索引的下一个索引
int next = i + 1;
//判断是否已为数组长度,如果是从数组头部(索引为0)开始找
if (next == items.length)
next = 0;
//如果查找的索引不等于要添加元素的索引,说明元素可以再移动
if (next != putIndex) {
items[i] = items[next];//把后一个元素前移覆盖要删除的元
i = next;
} else {
//在removeIndex索引之后的元素都往前移动完毕后清空最后一个元素
items[i] = null;
this.putIndex = i;
break;//结束循环
}
}
count--;//队列元素减1
if (itrs != null)
itrs.removedAt(removeIndex);//更新迭代器数据
}
notFull.signal();//唤醒添加线程
}
remove(Object o)方法的删除过程相对复杂些,因为该方法并不是直接从队列头部删除元素。首先线程先获取锁,再一步判断队列count>0,这点是保证并发情况下删除操作安全执行。接着获取下一个要添加源的索引putIndex以及takeIndex索引 ,作为后续循环的结束判断,因为只要putIndex与takeIndex不相等就说明队列没有结束。然后通过while循环找到要删除的元素索引,执行removeAt(i)方法删除,在removeAt(i)方法中实际上做了两件事,一是首先判断队列头部元素是否为删除元素,如果是直接删除,并唤醒添加线程,二是如果要删除的元素并不是队列头元素,那么执行循环操作,从要删除元素的索引removeIndex之后的元素都往前移动一个位置,那么要删除的元素就被removeIndex之后的元素替换,从而也就完成了删除操作。
//从队列头部删除,队列没有元素就阻塞,可中断
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();//中断
try {
//如果队列没有元素
while (count == 0)
//执行阻塞操作
notEmpty.await();
return dequeue();//如果队列有元素执行删除操作
} finally {
lock.unlock();
}
}
take方法其实很简单,有就删除没有就阻塞,注意这个阻塞是可以中断的,如果队列没有数据那么就加入notEmpty条件队列等待(有数据就直接取走,方法结束),如果有新的put线程添加了数据,那么put操作将会唤醒take线程,执行take操作。
public E peek() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
//直接返回当前队列的头元素,但不删除
return itemAt(takeIndex); // null when queue is empty
} finally {
lock.unlock();
}
}
final E itemAt(int i) {
return (E) items[i];
}
peek()方法,比较简单,直接返回当前队列的头元素但不删除任何元素。
九 常用方法
9.1 HashMap
HashMap是常用的键值对集合,jdk1.7中使用Segment数组+链表实现,jdk1.8中使用Node数组+链表+红黑树实现。HashMap线程不安全,允许存储null值(hash=0)。
9.1.1 construction
/** 构造方法 1 */
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
/** 构造方法 2 */
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/** 构造方法 3 */
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
/** 构造方法 4 */
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
名称 | 用途 |
---|---|
initialCapacity | HashMap 初始容量, 默认为16 |
loadFactor | 负载因子,默认为0.75(空间与时间的权衡) |
threshold | 当前 HashMap 所能容纳键值对数量的最大值,超过这个值,则需扩容, initialCapacity*loadFactor |
9.1.2 hashCode
根据key生成的哈希码(hash值)分布更加均匀、更具备随机性,避免出现hash值冲突
- JDK 1.7实现: 将 键key 转换成 哈希码(hash值)操作 = 使用hashCode() + 4次位运算 + 5次异或运算(9次扰动)
- JDK 1.8实现: 将 键key 转换成 哈希码(hash值)操作 = 使用hashCode() + 1次位运算 + 1次异或运算(2次扰动)
/**
* 函数使用原型
* 主要分为2步:计算hash值、根据hash值再计算得出最后数组位置
*/
// a. 根据键值key计算hash值 ->> 分析1
int hash = hash(key);
// b. 根据hash值 最终获得 key对应存放的数组Table中位置 ->> 分析2
int i = indexFor(hash, table.length);
/**
* 源码分析1:hash(key)
* 该函数在JDK 1.7 和 1.8 中的实现不同,但原理一样 = 扰动函数 = 使得根据key生成的哈希码(hash值)分布更加均匀、更具备随机性,
* 避免出现hash值冲突(即指不同key但生成同1个hash值)
* JDK 1.7 做了9次扰动处理 = 4次位运算 + 5次异或运算
* JDK 1.8 简化了扰动函数 = 只做了2次扰动 = 1次位运算 + 1次异或运算
*/
// JDK 1.7实现:将 键key 转换成 哈希码(hash值)操作 = 使用hashCode() + 4次位运算 + 5次异或运算(9次扰动)
static final int hash(int h) {
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
// JDK 1.8实现:将 键key 转换成 哈希码(hash值)操作 = 使用hashCode() + 1次位运算 + 1次异或运算(2次扰动)
// 1. 取hashCode值: h = key.hashCode()
// 2. 高位参与低位的运算:h ^ (h >>> 16)
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
// a. 当key = null时,hash值 = 0,所以HashMap的key 可为null
// 注:对比HashTable,HashTable对key直接hashCode(),若key为null时,会抛出异常,所以HashTable的key不可为null
// b. 当key ≠ null时,则通过先计算出 key的 hashCode()(记为h),
// 然后 对哈希码进行 扰动处理: 按位 异或(^) 哈希码自身右移16位后的二进制
}
/**
* 函数源码分析2:indexFor(hash, table.length)
* JDK 1.8中实际上无该函数,但原理相同,即具备类似作用的函数
*/
static int indexFor(int h, int length) {
return h & (length-1);
// 将对哈希码扰动处理后的结果 与运算(&) (数组长度-1),最终得到存储在数组table的位置(即数组下标、索引)
}
9.1.3 get
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 1. 定位键值对所在桶的位置
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
// 2. 如果 first 是 TreeNode 类型,则调用黑红树查找方法
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 2. 对链表进行查找
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
- 根据key的hash值定位到指定的桶
- (n - 1) & hash直接定位到桶位置,同取余操作,效率上更高
- 如果是链表类型,查询链表方法
- 如果是红黑树类型,查询红黑树方法
9.1.4 put
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 初始化桶数组 table,table 被延迟到插入新数据时再进行初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 如果桶中不包含键值对节点引用,则将新键值对节点的引用存入桶中即可
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 如果键的值以及节点 hash 等于链表中的第一个键值对节点时,则将 e 指向该键值对
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 如果桶中的引用类型为 TreeNode,则调用红黑树的插入方法
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 对链表进行遍历,并统计链表长度
for (int binCount = 0; ; ++binCount) {
// 链表中不包含要插入的键值对节点时,则将该节点接在链表的最后
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 如果链表长度大于或等于树化阈值,则进行树化操作
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 条件为 true,表示当前链表包含要插入的键值对,终止遍历
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 判断要插入的键值对是否存在 HashMap 中
if (e != null) { // existing mapping for key
V oldValue = e.value;
// onlyIfAbsent 表示是否仅在 oldValue 为 null 的情况下更新键值对的值
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 键值对数量超过阈值时,则进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
-
根据key定位到桶
-
如果为空,则先进行初始化
-
查询键值对是否已经存在,如果存在直接覆盖
-
如果不存在,将键值对插入到链表最后,并根据链表长度判断是否需要转为红黑树
-
判断键值对是否大于阈值,然后扩容
9.1.5 resize
HashMap在扩容时,扩容到当前数组长度的2倍,阈值也变为之前的2倍,扩容结束后需要重新计算键值对,并移到对应的位置
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
// 如果 table 不为空,表明已经初始化过了
if (oldCap > 0) {
// 当 table 容量超过容量最大值,则不再扩容
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 按旧容量和阈值的2倍计算新容量和阈值的大小
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
} else if (oldThr > 0) // initial capacity was placed in threshold
/*
* 初始化时,将 threshold 的值赋值给 newCap,
* HashMap 使用 threshold 变量暂时保存 initialCapacity 参数的值
*/
newCap = oldThr;
else { // zero initial threshold signifies using defaults
/*
* 调用无参构造方法时,桶数组容量为默认容量,
* 阈值为默认容量与默认负载因子乘积
*/
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// newThr 为 0 时,按阈值计算公式进行计算
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
// 创建新的桶数组,桶数组的初始化也是在这里完成的
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
// 如果旧的桶数组不为空,则遍历桶数组,并将键值对映射到新的桶数组中
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
// 重新映射时,需要对红黑树进行拆分
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
// 遍历链表,并将链表节点按原顺序进行分组
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 将分组后的链表映射到新桶中
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
- 计算新桶数组的容量newCap和新阈值newThr
- 创建新桶数组并初始化
- 将键值对重新映射到新桶数组中。
- 如果TreeNode类型,拆分红黑树
- 普通节点,分组映射
扩容结束后,仍然保持之前链表中的顺序。将普通节点分为两组链表,提高扩容速度
- e.hash & oldCap = 0, 无需移动
- e.hash & oldCap = 1,需将元素移到新桶中,新桶newIndex = oldCap + oldIndex
9.1.6 delete
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
// 1. 定位桶位置
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
// 如果键的值与链表第一个节点相等,则将 node 指向该节点
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
// 如果是 TreeNode 类型,调用红黑树的查找逻辑定位待删除节点
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
// 2. 遍历链表,找到待删除节点
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
// 3. 删除节点,并修复链表或红黑树
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)
tab[index] = node.next;
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
- 根据key定位到桶位置
- 遍历链表或红黑树,找到对应的键值对
- 删除节点
9.1.7 HashMap死锁
原因:jdk1.7 中采用的是头插入法,在扩容时会将节点顺序倒叙插入新数组中,多线程时可能会产生死锁
/**
* 源码分析:resize(2 * table.length)
* 作用:当容量不足时(容量 > 阈值),则扩容(扩到2倍)
*/
void resize(int newCapacity) {
// 1. 保存旧数组(old table)
Entry[] oldTable = table;
// 2. 保存旧容量(old capacity ),即数组长度
int oldCapacity = oldTable.length;
// 3. 若旧容量已经是系统默认最大容量了,那么将阈值设置成整型的最大值,退出
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
// 4. 根据新容量(2倍容量)新建1个数组,即新table
Entry[] newTable = new Entry[newCapacity];
// 5. (重点分析)将旧数组上的数据(键值对)转移到新table中,从而完成扩容 ->>分析1.1
transfer(newTable);
// 6. 新数组table引用到HashMap的table属性上
table = newTable;
// 7. 重新设置阈值
threshold = (int)(newCapacity * loadFactor);
}
/**
* 分析1.1:transfer(newTable);
* 作用:将旧数组上的数据(键值对)转移到新table中,从而完成扩容
* 过程:按旧链表的正序遍历链表、在新链表的头部依次插入
*/
void transfer(Entry[] newTable) {
// 1. src引用了旧数组
Entry[] src = table;
// 2. 获取新数组的大小 = 获取新容量大小
int newCapacity = newTable.length;
// 3. 通过遍历 旧数组,将旧数组上的数据(键值对)转移到新数组中
for (int j = 0; j < src.length; j++) {
// 3.1 取得旧数组的每个元素
Entry<K,V> e = src[j];
if (e != null) {
// 3.2 释放旧数组的对象引用(for循环后,旧数组不再引用任何对象)
src[j] = null;
do {
// 3.3 遍历 以该数组元素为首 的链表
// 注:转移链表时,因是单链表,故要保存下1个结点,否则转移后链表会断开
Entry<K,V> next = e.next;
// 3.3 重新计算每个元素的存储位置
int i = indexFor(e.hash, newCapacity);
// 3.4 将元素放在数组上:采用单链表的头插入方式 = 在链表头上存放数据 = 将数组位置的原有数据放在后1个指针、将需放入的数据放到数组位置中
// 即 扩容后,可能出现逆序:按旧链表的正序遍历链表、在新链表的头部依次插入
e.next = newTable[i];
newTable[i] = e;
// 访问下1个Entry链上的元素,如此不断循环,直到遍历完该链表上的所有节点
e = next;
} while (e != null);
// 如此不断循环,直到遍历完数组上的所有数据元素
}
}
}
9.1.8 HashMap 1.7 VS 1.8
- java1.7中增加的元素会放入链表头部(多线程下可能造成死锁),1.8中新增的元素放到链表尾部
- java1.8引入了红黑树,当桶数量>64且链表长度>8时,转为红黑树(泊松分布)。当扩容后红黑树节点小于6,则转为链表
- java1.8在多线程下会出现扩容丢数据
9.2 ConcurrentHashMap
线程安全的hashMap,key不能为null,value也不能为null
// 默认为 0
// 当初始化时, 为 -1
// 当扩容时, 为 -(1 + 扩容线程数)
// 当初始化或扩容完成后,为 下一次的扩容的阈值大小
private transient volatile int sizeCtl;
// 整个 ConcurrentHashMap 就是一个 Node[]
static class Node<K,V> implements Map.Entry<K,V> {}
// hash 表
transient volatile Node<K,V>[] table;
// 扩容时的 新 hash 表
private transient volatile Node<K,V>[] nextTable;
// 扩容时如果某个 bin 迁移完毕, 用 ForwardingNode 作为旧 table bin 的头结点
static final class ForwardingNode<K,V> extends Node<K,V> {}
// 用在 compute 以及 computeIfAbsent 时, 用来占位, 计算完成后替换为普通 Node
static final class ReservationNode<K,V> extends Node<K,V> {}
// 作为 treebin 的头节点, 存储 root 和 first
static final class TreeBin<K,V> extends Node<K,V> {}
// 作为 treebin 的节点, 存储 parent, left, right
static final class TreeNode<K,V> extends Node<K,V> {}
// 获取 Node[] 中第 i 个 Node
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i)
// cas 修改 Node[] 中第 i 个 Node 的值, c 为旧值, v 为新值
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v)
// 直接修改 Node[] 中第 i 个 Node 的值, v 为新值
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v)
9.2.1 construction
构造方法中懒惰初始化,仅仅计算了table的大小,并没有真正创建数组
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (initialCapacity < concurrencyLevel) // Use at least as many bins
initialCapacity = concurrencyLevel; // as estimated threads
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
// tableSizeFor计算容量最接近的2次幂
int cap = (size >= (long)MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY : tableSizeFor((int)size);
this.sizeCtl = cap;
}
9.2.2 get
get操作中没有加锁,效率非常高
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
// spread 方法能确保返回结果是正数
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
// 如果头结点已经是要查找的 key
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
// hash 为负数表示该 bin 在扩容中(-1)或是 treebin(-2), 这时调用 find 方法来查找
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
// 正常遍历链表, 用 equals 比较
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
- 计算key的hash
- 定位到桶,如果头节点等于key直接返回
- 如果hash < 0
- ForwardingNode节点,调用对应的find方法
- treebin节点,调用红黑树的find方法
- 遍历链表,寻找key
9.2.3 put
源码中的table代表桶数组,bin代表链表
public V put(K key, V value) {
// false 表示当key冲突时,用新值代替旧值
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
// 校验,key和value均不能为null
if (key == null || value == null) throw new NullPointerException();
// spred方法会综合高位和低位,计算出hashCode,保证会大于0
int hash = spread(key.hashCode());
// 链表长度,会根据此值树化和并行计算容量
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
// f 表示链表头节点
// n 表示数组长度
// i 表示table中的下标
// fh 表示链表头节点的hash值
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
// 延迟初始化,真正使用的时候才会创建数组,内部使用cas实现
tab = initTable();
// 进入下面分支,说明当前桶的头节点为null,需要创建头节点
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
// fh=-1.帮助其他线程扩容该桶中的链表,提高扩容效率
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
// 进入此分支,表明有冲突,需要加锁
synchronized (f) {
// 双保险,确保当前链表头节点没有被其他线程移动过
if (tabAt(tab, i) == f) {
// fh > 0,说明是普通的链表结构
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
// 遍历链表,如果找到相同的key则直接更新
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
// 链表中没有找到对应的key,则直接追加到链表尾部
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
// fh = -2,进入红黑树的处理逻辑
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
// 判断链表是否需要树化
if (binCount != 0) {
// 如果链表长度 >= 树化阈值(8), 进行链表转为红黑树
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
//增加size计数
addCount(1L, binCount);
return null;
}
// 数组初始化
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
// 尝试将sizeCtl设置为-1(表明正在初始化table)
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
- 检验key和value,两者均不能为null
- 计算key的hash值
- 如果table为空,初始化数组(体现懒惰初始化思想)
- 如果链表为空,直接创建头节点返回
- 如果fh < 0, 帮助其它线程扩容
- 加锁遍历链表,如果找到对应的key直接覆盖,否则追加的链表尾部
- 判断数组是否需要树化
- 计算容量(内部采用多cell提高速度,只能统计大概值,不是最终的精确值)
9.2.4 差异比较1.7 VS 1.8
结构不同
-
1.7 中使用Segment+HashEntry,维护了长度为16的Segment数组,每个Segment对应一把锁,相当于最多支持16个并发访问
-
1.8中使用Node+CAS+ Synchronized
计算总数方式不同
-
1.7中先采用不加锁的方式,连续计算元素的个数,最多计算3次
如果前后两次计算结果相同,说明计算准确
如果前后两次计算结果不同,则给每个segment加锁,重新计算个数
-
1.8中内部维护 baseCount+ CounerCell,只是一个近似值
支持的key、value不同
- 1.7中key可以为null,value不能为null。1.8中两者均不能为null
冲突解决方式不同
- 1.7使用拉链法
- 1.8在拉链法的基础上增加红黑树,当数组长度大于64且链表长度大于8时,将链表结构转为红黑树结构
扩容时计算方式不同功能
- 1.7 重新计算存储位置(hashCode-> 扰动处理->h & (lenght -1))
- 1.8 分组计算,扩容后位置=原位置 or 原位置+旧容量
9.3 ThreadLocal
线程局部变量,每个线程都有一份共享变量的拷贝,解决多线程访问资源的共享问题
与HashMap有所不同,ThreadLocal中的Map初始值为16,负载因子为2/3,解决冲突的方法是线性探测法(即在当前Hash的基础上再加上一个常量,重新尝试添加)
类型 | 回收时机 | 应用场景 |
---|---|---|
强引用 | 一直有效,除非GC Roots不可达 | 所有程序的场景,基本对象等 |
软引用 | 内存不足时会被回收 | 一般用于内存非常敏感资源及缓存,如网页缓存、图片缓存等 |
弱引用 | 每次GC都会回收 | 声明周期很短的对象,如Threadlocal中的key |
虚引用 | 随时都可能被回收 | JVM团队跟踪内部的JVM垃圾回收过程,一般不会在应用中使用 |
9.3.1 原理
// 每个线程都有一个属性指向ThreadLocal.ThreadLocalMap
ThreadLocal.ThreadLocalMap threadLocals = null;
/**
ThreadLocalMap是ThreadLocal中的内部类,维护了ThreadLocal变量与具体实例的映射
key为ThreadLocal变量的弱引用
value为每个线程的共享变量副本
*/
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
9.3.2 get
public T get() {
Thread t = Thread.currentThread();
// 从当前线程中获取ThreadLocal对象
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
读取实例时,首先通过getMap方法获取自身的ThreadLocalMap。该ThreadLocalMap实例是线程的一个属性
获取 ThreadLocalMap后,通过getEntry方法获取该ThreadLocal在当前线程的ThreadLocalMap中对应的Entry,该方法中的this即当前访问的ThreadLocal对象
如果获取的Entry不为null,从Entry中取值相应的值,否则,通过setInitialValue设置该ThreadLocal变量在该线程转给对应的具体实例的初始值
9.3.3 set
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
9.3.4 remove
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
9.3.5 内存泄漏
对于已经不再使用的ThreadLocal对象,它在每个线程中内对应的实例由于被线程的ThreadLocalMap的Entry强引用,无法被回收可能会造成内存泄漏
如上图所示,ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用使用,那么系统GC时,会回收它。此时,ThreadLocalMap中就会出现key为null的Entry,如果当前线程迟迟不结束,这些值为null的Entry的value就会一直存在一个强引用链:
Thread Ref => Thread => ThradLocalMap =>Entry =>value
最终导致内存泄漏
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
解决方案
- 每次调用ThreadLocalMap的set方法时,会将所有key=null的Entry的值也设置为null
- 每次调用ThreadLocalMap的getEntry方法时,会将所有key=null的Entry的值也设置为null
- 将ThreadLocal变量设置为private static,延长ThreadLocal的生命周期
- 建议在代码中及时调用remove方法,清除不再使用的Entry
9.3.6 适用场景
- 每个线程需要有自己独立的实例
- 实例需要在多个方法中共享,但不希望被多线程共享
- Spring JDBC连接池
在Java web中,Session保存了很多用户信息,业务中很多地方需要使用用户信息。需要保证每个线程独有一份Session,每个线程中的方法可以共享同一份Session。如果不使用ThreadLocal,则需要在每个线程中维护一个Session实例,并将该实例在多个方法中传递。
public class SessionHandler {
@Data
public static class Session {
private String id;
private String user;
private String status;
}
public Session createSession() {
return new Session();
}
public String getUser(Session session) {
return session.getUser();
}
public String getStatus(Session session) {
return session.getStatus();
}
public void setStatus(Session session, String status) {
session.setStatus(status);
}
public static void main(String[] args) {
new Thread(() -> {
SessionHandler handler = new SessionHandler();
Session session = handler.createSession();
handler.getStatus(session);
handler.getUser(session);
handler.setStatus(session, "close");
handler.getStatus(session);
}).start();
}
}
如果使用ThreadLocal保存Session,则实现起来更加优雅和高效
public class SessionHandler {
public static ThreadLocal<Session> session = ThreadLocal.withInitial(() -> new Session());
@Data
public static class Session {
private String id;
private String user;
private String status;
}
public String getUser() {
return session.get().getUser();
}
public String getStatus() {
return session.get().getStatus();
}
public void setStatus(String status) {
session.get().setStatus(status);
}
public static void main(String[] args) {
new Thread(() -> {
SessionHandler handler = new SessionHandler();
handler.getStatus();
handler.getUser();
handler.setStatus("close");
handler.getStatus();
}).start();
}
}
9.3.7 扩展
业务中通常开启多线程来实现并行处理,此时需要将主线程中的数据传递到子线程中,thredLocal与线程绑定,因而子线程无法获取到主线程中的数据
-
InhertableThreadLocal
子线程创建时,会将主线程的inheritableThreadLocals复制到自己的线程中。此种方式仅能解决主线程主动创建子线程场景,不适用线程池模式
/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null; /* * InheritableThreadLocal values pertaining to this thread. This map is * maintained by the InheritableThreadLocal class. */ ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; -- 子线程初始化时调用 /** * Construct a new map including all Inheritable ThreadLocals * * @param parentMap the map associated with parent thread. */ private ThreadLocalMap(ThreadLocalMap parentMap) { Entry[] parentTable = parentMap.table; int len = parentTable.length; setThreshold(len); table = new Entry[len]; for (int j = 0; j < len; j++) { Entry e = parentTable[j]; if (e != null) { @SuppressWarnings("unchecked") ThreadLocal<Object> key = (ThreadLocal<Object>) e.get(); if (key != null) { Object value = key.childValue(e.value); Entry c = new Entry(key, value); int h = key.threadLocalHashCode & (len - 1); while (table[h] != null) h = nextIndex(h, len); table[h] = c; size++; } } } }
示例
/** * InheritableThreadLocal * 子线程复用母线程的ThreadLocal * 不适用线程池 * */ @Slf4j public class InheritableThreadLocalTest { public static ThreadLocal inheritableThreadLocal = new InheritableThreadLocal(); public static void main(String[] args) throws InterruptedException { log.info("主线程启动"); inheritableThreadLocal.set("MAIN KEY"); new SubThread().start(); log.info("主线程休眠3s"); TimeUnit.SECONDS.sleep(3); log.info("主线程结束"); } static class SubThread extends Thread { @Override public void run() { log.info("子线程启动。。。。。"); Object o = inheritableThreadLocal.get(); log.info("子线程获取inheritableThreadLocal= {}", o); log.info("子线程结束"); } } } 13:48:11.865 [main] INFO jincong.practice.thread.threadLocal.InheritableThreadLocalTest - 主线程启动 13:48:11.868 [main] INFO jincong.practice.thread.threadLocal.InheritableThreadLocalTest - 主线程休眠3s 13:48:11.868 [Thread-0] INFO jincong.practice.thread.threadLocal.InheritableThreadLocalTest - 子线程启动。。。。。 13:48:11.868 [Thread-0] INFO jincong.practice.thread.threadLocal.InheritableThreadLocalTest - 子线程获取inheritableThreadLocal= MAIN KEY 13:48:11.870 [Thread-0] INFO jincong.practice.thread.threadLocal.InheritableThreadLocalTest - 子线程结束
-
TransmittableThreadLocal
阿里开源的用于解决线程池共享threadLocal变量的问题
// 使用TtlExecutors包装线程池 public static ExecutorService executorService = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(THREAD_SIZE)); // 使用TransmittableThreadLocal public static TransmittableThreadLocal transmittableThreadLocal = new TransmittableThreadLocal(); // set、get、remove方法同threadLocal