【要图】

image-20251110150021006

线程

并发编程存在的三大问题?

  1. 原子性

同一时刻只能有一个线程对数据进行操作。在java中使用了atomic包synchronized关键字来确保原子性

  1. 可见性

一个线程对主内存进行修改其他线程可以看到。在java中使用synchronizedvolatile两个关键字实现

  1. 有序性

一个线程观察其他线程的执行顺序,一般无序。在java中用happens-before原则来确保有序性

happens-before:如果 A happens-before B,那么 A 的结果(内存写入)对 B 可见,并且 A 的执行顺序在 B 之前(逻辑上有序)。


线程创建的方式有哪些?

继承Thread类

继承Thread类,重写run()方法;
创建该类实例后,调用start()方法启动线程

image-20251110150355195

优点:编写简单,若需要访问当前线程,无需使用Thread.currentThread()方法,使用this关键字即可获取当前线程;缺点:不能继承其他父类


实现Runnable接口

实现Runnable接口需要重写run()方法,将Runnable对象作为参数传递给Thread类,再调用start()方法启动线程

image-20251110150658171

优点:线程只实现了Runnable接口,还可以继承其他类,适合多个相同线程处理同一份资源;缺点:编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法


实现Callable接口与FutureTask

Callable接口类似于Runnable,但是它的call()方法有返回值,并且可以抛出异常

要执行Callable任务,需要包装进一个FutureTask

  • Thread类构造器只接受Runnable参数
  • FutureTask实现了Runnable接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MyCallable implements Callable<Integer> {

@Override
public Integer call() throws Exception {
// 线程执行的代码,这里返回一个整数结果
System.out.println("线程运行中:" + Thread.currentThread().getName());
return 1;
}

public static void main(String[] args) {
MyCallable task = new MyCallable();
FutureTask<Integer> futureTask = new FutureTask<>(task);
Thread t = new Thread(futureTask);
t.start();

try {
// 获取线程执行结果
Integer result = futureTask.get();
System.out.println("Result: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}

优点:适合多个相同线程处理同一份资源;缺点:编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法


怎么启动线程?

通过Thread类的start()

1
2
3
4
5
6
7
// 创建两个线程,使用start()开启线程

MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();

myThread1.start();
myThread2.start();

怎么停止自己的线程运行?

  1. 异常法停止:线程调用interrupt()方法后,在线程的run方法中判断当前对象的interrupted()状态,如果是中断状态则抛出异常,达到中断线程的效果
  2. 在沉睡中停止:先将线程sleep,再调用interrupt标记中断状态,interrupt会将阻塞状态的线程中断。抛出中断异常,达到停止线程的效果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 在沉睡中停止
Thread t = new Thread(() -> {
try {
System.out.println("准备 sleep 3 秒");
Thread.sleep(3000); // 第二/三步
System.out.println("正常醒来(一般到不了这句)");
} catch (InterruptedException e) {
System.out.println("sleep 被打断"); // 第五步
}
}, "worker");

t.start(); // 第一步
Thread.sleep(300); // 第三/二步
t.interrupt(); // 第四步
t.join();
  1. stop() 暴力停止:线程调用stop()方法会被暴力停止,方法已弃用,强制让线程停止可能会使一些请理性工作得不到完成
  2. 使用return停止线程:调用interrupt标记为中断状态后,在run方法中判断当前线程状态,如果为中断状态则return,达到停止线程的效果
1
2
3
4
5
6
7
8
9
10
11
12
13
// 使用return停止线程
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
// xxx
}
// 检测到中断后,正常 return。和抛异常相比,这里是“静默”退出。
System.out.println("检测到中断标记,直接 return 结束");
}, "worker");

t.start();
Thread.sleep(300);
t.interrupt();
t.join();

调用interrupt是如何让线程抛出异常的?

interrupt是什么?每个线程都有一个中断状态(boolean值),默认是false;调用thread.interrupt()会把这个状态设为true,表示希望你停下来;线程本身需要配合检查这个状态来决定是否停止

interrupt() 不是杀死线程,而是发出一个“中断请求”。

Interrupt() 适用的两种情况:1. 线程在阻塞(sleep/join/wait),会抛出InterruptedException并结束阻塞,2. 线程在运行,Interrupt() 只会设置一个标记,需要线程自己检查并退出


怎么停止其他线程运行?

  1. 通过共享标志位主动终止
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class SafeStopWithFlag implements Runnable {
// 使用 volatile 保证可见性
private volatile boolean running = true;

@Override
public void run() {
while (running) {
try {
// 处理任务逻辑
System.out.println("Thread is running...");
Thread.sleep(1000);
} catch (InterruptedException e) {
// 捕获中断异常后设置 running = false
running = false;
Thread.currentThread().interrupt(); // 重新设置中断标志
}
}
System.out.println("Thread terminated safely.");
}

// 停止线程的方法(由外部调用)
public void stop() {
running = false;
}
}
  1. 通过线程中断机制

通过 Thread.interrupt 触发线程中断状态,结合中断检测逻辑实现安全停止

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class InterruptExample implements Runnable {

@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
System.out.println("Working...");
Thread.sleep(1000);
} catch (InterruptedException e) {
// 当阻塞时被中断,抛出异常并清除中断状态
System.out.println("Interrupted during sleep!");
Thread.currentThread().interrupt(); // 把“被打断”的信号继续往外层传递
}
}
System.out.println("Thread terminated by interrupt.");
}

public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(new InterruptExample());
t.start();

// 运行 3 秒后中断线程
Thread.sleep(3000);
System.out.println("Main thread sending interrupt signal...");

// Interrupt不会立即中断,只设置中断标志位
t.interrupt();
}
}
  1. 通过 Future 取消任务

核心思想:用线程池执行任务,获取Future对象来控制任务,调 future.cancel(true) —> 调用任务线程的 interrupt(),依赖中断机制让任务结束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 一、创建线程池
ExecutorService executor = Executors.newSingleThreadExecutor();

// 二、提交任务并获取Future
Future<?> future = executor.submit(() -> {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("Task running...");
try {
Thread.sleep(1000); // 模拟任务耗时
} catch (InterruptedException e) {
System.out.println("Task interrupted.");
Thread.currentThread().interrupt(); // 重新设置中断标志
}
}
});

try {
// 三、主线程等待一段时间
Thread.sleep(3000);

// 四、调用Future取消任务
future.cancel(true);
} catch(InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 五、关闭线程池
executor.shutdown();
}

/**
* 运行结果:
*
* Task running...
* Task running...
* Task running...
* Task interrupted.
*/

image-20251110151930322


juc包下常用的类

线程池相关

ThreadPoolExecutor:最核心的线程池,用于创建和管理线程池。可以灵活配置线程池的参数(核心线程数、最大线程数、任务队列)

Executors:线程池工厂类,提供了一系列静态方法来创建不同的线程池。newFixedThreadPool(创建固定线程数的线程池)、newCachedThreadPool(创建可缓存线程池)、newSingleThreadExecutor(创建单线程线程池)


并发集合类

ConcurrentHashMap:线程安全的哈希映射表,用于在多线程环境下高效存储和访问键值对

CopyOnWriteArrayList:线程安全的列表,在操作列表时,会创建一个新的底层数组,将修改操作应用到新数组上,读操作仍可以在旧数据上读,实现了读写分离


同步工具类

CountDownLatch:让一个线程等待其他线程执行完毕后再执行;用于主线程等待多个子任务完成后再汇总结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ExecutorService pool = Executors.newFixedThreadPool(3);
CountDownLatch latch = new CountDownLatch(3);

pool.submit(() -> {
// ...
latch.countDown();
});

pool.submit(() -> {
// ...
latch.countDown();
});

pool.submit(() -> {
// ...
latch.countDown();
});

latch.await();
// 等待latch都执行完了后,再执行对应的任务

CyclicBarrier:让一组线程相互等待,直到全部达到屏障点;用于多线程分阶段计算。

屏障点,一组线程必须全部到达的一个同步位置,比如await()方法就是到达屏障点的动作

Semaphore:控制同时访问某个资源的线程数量,用于限制资源访问数量(数据库连接、线程池)


原子类

AtomicInteger:原子整数类,提供了对整数类型的原子操作,如自增、自删、比较并交换

AtomicReference:原子引用类,用于对对象引用进行的原子操作


如何保证多线程安全?

可以通过synchronized关键字、volatile关键字、Lock接口和ReentrantLock类、原子类、线程局部变量(ThreadLocal类可以为每个线程提供自己的变量)、并发集合、JUC工具类(Semaphore、CyclicBarrier)


如何保证数据一致性的方案?

  1. 事务管理

使用数据库事务来确保一组数据操作要么全部成功,要么全部失败回滚。通过ACID(原子性、一致性、隔离性、持久性)来确保

  1. 锁机制

使用锁来实现对共享资源的互斥访问

  1. 版本控制

通过乐观锁,再更新数据时,记录数据的版本,避免对同一数据进行修改

乐观锁、悲观锁

乐观锁:假设冲突很少发生,不加锁直接去更新,失败了再处理

核心思想:在数据库中,加一个版本号字段;每次更新时,先检查版本号是否一致,一致才更新,并将版本号 + 1;如果版本号不一致,说明在你更新前,别人已经修改了这条数据,更新会失败

悲观锁:假设冲突会发生,更新前先锁住记录,别人不能修改

核心思想:假设并发冲突一定会发生,所以在操作数据前锁住数据;其他事务在此期间不能修改,直到锁释放;通过数据库锁机制,来保证同一时刻只能有一个事务能操作这条数据


Java的线程状态有哪些?

image-20251118091456550

通过Thread中的getState()方法可以获取当前线程的状态

  • new:线程正在创建,还未调用start方法
  • runnable:就绪状态(调用start,等待调度)+ 正在运行
  • blocked:等待锁时,陷入阻塞状态
  • waiting:无限期的等待
  • timed_waiting:有时限的等待
  • terminated:线程完成执行,终止状态

sleep和wait的区别是什么?

所属分类不同:sleep是Thread类的静态方法,可以在任何地方直接通过Thread.sleep()调用,无需依赖实例对象;wait是Object类的实例方法,必须通过实例对象来调用

锁释放的情况:在使用Thread.sleep()期间,其他线程无法获得该线程持有的锁;调用Object.wait()时,线程会释放持有的锁,进入等待状态,直到其他线程调用相同对象的notify() 或 notifyAll() 唤醒它

使用条件:sleep可以在任意位置调用,无需事先获取锁;wait必须在同步块或同步方法内调用(该线程必须持有该对象的锁),否则会抛出 IllegalMonitorStateException 异常

唤醒机制:sleep休眠时间结束后,线程自动恢复到就绪状态;wait需要其他线程调用相同对象的nofity() 或 notifyAll()方法才能被唤醒

设计用途:sleep() 暂停线程执行,不涉及锁协作;wait()进程之间的协作


blocked和waiting区别是什么?

触发机制:blocked是锁竞争失败后被动触发的状态;waiting是人为主动触发的状态

唤醒方式:blocked唤醒时自动触发;waiting是必须通过notify,nofityAll主动唤醒


notify和notifyAll的区别?

notify:唤醒一个线程,其余线程依然处于wait的等待唤醒状态

notifyAll() :唤醒等待集中的所有线程,它们在当前持锁线程释放锁后进入锁竞争;竞争顺序未指定,且任意时刻只有一个线程能获得该监视器锁


sleep会释放cpu吗?

会的,调用Thread.sleep()时,线程会释放cpu,但不会释放持有的锁;当线程调用sleep后,会主动让出CPU时间片,进入timed_waiting状态,此时操作系统将CPU分配给其他就绪的线程


wait状态下的进程如何恢复到running状态?

由等待(wait)状态到运行(running)状态的核心机制是通过外部事件触发或资源可用性变化,比如等待线程被其他线程通过nofity和nofityAll唤醒


notify选择哪个线程?

notify在源码的注释中说到notify选择唤醒的线程是任意的,但是依赖于具体实现的jvm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static final Object lock = new Object();

public static void main(String[] args) {
// 1) 若干等待线程:全部 wait 到 WaitSet 里
for (int i = 1; i <= 5; i++) {
startThread(() -> {
synchronized (lock) {
lock.wait(); // ← 被 notify 选中的“某一个”才会从这里返回
// ...继续执行(此处无业务,演示用)
}
});
}

sleep(300); // 给他们时间都进入 wait

// 2) 唤醒一个,但不保证是哪个
synchronized (lock) {
lock.notify(); // ← 唤醒 WaitSet 中“某一个”,选择是任意的
} // 释放锁后,被选中的那个才有机会真正继续

// 想多叫几个?那就多次 notify,每次只唤醒一个:
synchronized (lock) { lock.notify(); } // 第2个
synchronized (lock) { lock.notify(); } // 第3个
}

不同的线程之间如何通信?

  1. Volatile关键字
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private static volatile boolean flag = false;

Thread producer = new Thread(() -> {
Thread.sleep(200);
flag = true;
});


Thread consumer = new Thread(() -> {
while(!flag) {
// ...
}
});

producer.start();
consumer.start();

volatile关键字确保了一个变量在多个线程之间的可见性和禁止指令重排序,一个线程修改了一个变量,另一个线程能立即看见

生产者线程睡眠0.2s后,将flag设置为true;消费者线程在flag为false时,一直等待,直到flag变为true才继续执行

  1. notify()方法

Object类中的wait()、nofity()和notifyAll()方法都可以用于线程间的协作。

  • wait()方法:使当前线程进入等待状态
  • nofity()方法:唤醒此对象监视器上等待的单个随机线程
  • nofityAll方法:唤醒此对象监视器上等待的所有线程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class WaitNotifyExample {

private static final Object lock = new Object();

public static void main(String[] args) {
// 生产者线程
Thread producer = new Thread(() -> {
synchronized (lock) {
try {
System.out.println("Producer: 第二个执行");
Thread.sleep(2000); // 模拟生产耗时
System.out.println("Producer: 第三个执行");
// 唤醒等待的线程
lock.notify();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "producer");

// 消费者线程
Thread consumer = new Thread(() -> {
synchronized (lock) {
try {
System.out.println("Consumer: 第一个执行");
// 进入等待状态(释放 lock,等待被唤醒)
lock.wait();
System.out.println("Consumer: 第四个执行");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "consumer");

// 先启动消费者,再启动生产者
consumer.start();
producer.start();
}
}

lock 是一个用于同步的对象,生产者和消费者线程都需要获取该对象的锁才能执行相关操作,wait、nofity、notifyAll

  1. Lock、Condition接口

Lock和Condition接口提供了比synchronized更灵活的线程通讯方式;

Condition接口的await()方法类似于wait()方法,signal()方法类似于nofity()方法,signalAll()方法类似于nofityAll()方法;

一个Lock可以对应多个Condition

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public class MultiConditionExample {
private static final Lock lock = new ReentrantLock();

// 两个条件队列
private static final Condition productReady = lock.newCondition(); // 生产完成信号
private static final Condition checkDone = lock.newCondition(); // 检查完成信号

private static boolean hasProduct = false;
private static boolean checked = false;

public static void main(String[] args) {
// 生产者线程
Thread producer = new Thread(() -> {
lock.lock();
try {
System.out.println("Producer: 正在生产...");
Thread.sleep(1000);
hasProduct = true;
System.out.println("Producer: 生产完成,通知质检员。");
productReady.signal(); // 通知质检员可以检查
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
});

// 质检员线程
Thread checker = new Thread(() -> {
lock.lock();
try {
while (!hasProduct) {
productReady.await(); // 等待生产完成
}
System.out.println("Checker: 正在质检...");
Thread.sleep(1000);
checked = true;
System.out.println("Checker: 质检完成,通知消费者。");
checkDone.signal(); // 通知消费者可以消费
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
});

// 消费者线程
Thread consumer = new Thread(() -> {
lock.lock();
try {
while (!checked) {
checkDone.await(); // 等待质检完成
}
System.out.println("Consumer: 开始消费产品!");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
});

consumer.start();
checker.start();
producer.start();
}
}

和wait、nofity的区别?

wait/nofity 必须用 synchronized 搭配

await/signal 必须用 Lock 搭配,而且一个锁可以有多个 Condition (更灵活,可实现多条件唤醒)

  1. ReentrantLock

是Lock接口的一个实现类

Reentrant 表示可重入:同一个线程可以多次获取一把锁而不会死锁(通过维护一个持有计数)

主要特点(对比synchronized)
image-20251118094134446

构造方式

1
2
3
4
5
// 默认非公平锁(性能好,但可能“插队”)
ReentrantLock lock1 = new ReentrantLock();

// 公平锁(先到先得,避免插队)
ReentrantLock lock2 = new ReentrantLock(true);

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();

Thread t = new Thread(() -> {
lock.lock();
try {
System.out.println("Thread waiting...");
condition.await(); // 2. 释放锁并等待
System.out.println("Thread resumed!"); // 4. 打印
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
});

t.start(); // 1. 开启线程

Thread.sleep(1000);

lock.lock();
try {
System.out.println("Main thread signaling...");
condition.signal(); // 3. 再次开启t线程
} finally {
lock.unlock();
}

/*
打印顺序为:

Thread waiting...
Main thread signaling...
Thread resumed!
*/

守护线程了解吗?

了解,守护线程可以理解为后台服务线程,比如垃圾回收线程

它的特点就是:当所有用户线程(非守护线程)都结束时,即使守护线程的代码还没执行完,它也会自动结束。通过 setDaemon(true) 来设置

Java的并发工具有哪些?

  1. CountDownLatch(倒计数器)

调用 countDown() 方法会使计数器减一,当计数器的值减为0时,等待的线程会被唤醒

CountDownLatch是什么?

它是java并发包中的一个同步工具类,用于让一个或多个线程等待其他线程操作完后再执行;核心通过一个计数器(Counter)实现线程间的协调

核心原理

  • 初始化计数器:创建CountDownLatch时指定一个初始计数值(如7)
  • 等待线程阻塞:调用 await() 的线程会被阻塞,直到计数器变为0
  • 任务完成通知:其他线程完成任务后调用 countDown(),使计数器减1
  • 唤醒等待线程:当计数器减到0时,所有等待的线程会被唤醒
  1. CyclicBarrier(同步屏障)

让一组线程相互等待,直到全部达到屏障点;比如new CyclicBarrier(5),表示需要5个线程都调用await()方法等待。当第5个线程到达时,屏障打开,所有被阻塞的线程同时被唤醒继续执行。

  1. Semaphore(信号量)

用于设置同时访问某个共享资源的线程数

  1. Future和Callable

配合线程池,将Callable创建的线程交给线程池,由Future创建的对象接收结果

  1. ConcurrentHashMap

线程安全的哈希表,允许多个线程同时进行读写操作


CountDownLatch和CyclicBarrier有什么区别?

核心思想:CountDownLatch是一个线程等多个线程;CyclicBarrier是多个线程互相等

重置性:CountDownLatch的计数器只能用一次;CyclicBarrier的计数器可以重置,循环使用

动作:CountDownLatch的减计数操作countDown()可以在多个线程中分散调用;CyclicBarrier的等待操作await()必须在每个需要同步的线程中自己调用

Java中有哪些常用的锁

  • 内置锁(synchronized)
  • ReentrantLock
  • 读写锁(ReadWriteLock)
  • 乐观锁、悲观锁:synchronized和ReentrantLock默认都是悲观锁
  • 自旋锁(CAS):自旋锁是一种锁机制,线程在等待锁时会持续循环检查锁是否可用,而不是放弃CPU并阻塞

非公平锁吞吐量为什么比公平锁大?

  1. 公平锁执行流程:获取锁时,先将线程自己添加到等待队列队尾,当某线程用完锁之后,会唤醒等待队列队首的线程尝试去获取锁。整个过程中,线程会从 运行状态 ——> 休眠状态,再从 休眠状态 ——> 运行状态。每次休眠和恢复都要从用户态切换到内核态,切换状态比较慢
  2. 非公平锁的执行流程:当线程获取锁时,先通过CAS尝试获取锁,不用唤醒其他线程,成功直接拥有锁,失败进入等待队列中阻塞,等待下次尝试获取锁

什么情况下会产生死锁、如何解决?

image-20251118100036449

如何解决?

使用资源有序分配法

给所有资源类型规定一个全局总顺序(全序),例如:A > B > C > …

线程1 先获取A资源,再获取B资源

线程2同样,先获取A资源,再获取B资源

线程n先获取A资源,再获取B资源

就是说线程1 ~ n总以相同的全局总顺序获取资源

Synchronized和Reentrantlock

Synchronized

工作原理

  1. synchronized是java提供的原子性内置锁,这种使用者看不到的锁也被称为监视器锁
  2. 使用synchronized后,会在编译之后,同步的代码块前后加上monitorenter和monitorexit字节码指令
  3. 执行monitorenter指令时会尝试获取锁对象,如果代码块没有被锁定或已经获得了锁,锁计数器 + 1,此时其他竞争锁的线程则会进入等待队列中
  4. 执行monitorexit指令时会将计数器 - 1,当计数器为0时,锁释放,处于等待队列中的线程再继续竞争锁
  5. synchronized是排它锁,当一个线程获得锁之后,其他线程必须等待该线程释放锁后,才能竞争锁

应用场景

  1. 简单同步需求
  2. 代码块同步
  3. 内置锁的使用

除了用synchronized,还有什么方法可以实现线程同步?

  • 使用volatile关键字
  • 使用ReentrantLock类
  • 使用Atomic类

synchronized锁静态方法和普通方法区别?

image-20251118100956490

  • 普通方法
1
2
3
4
5
6
7
public synchronized void normalMethod() { ... }
// 等价于:
public void normalMethod() {
synchronized (this) {
...
}
}

锁的是 this 对象本身
也就是说:

  • 同一个对象 A:
    • 线程1 调用 A.normalMethod()
    • 线程2 调用 A.otherSyncMethod()
    • 两个方法不能并发,因为锁都是 A 这一个对象。
  • 不同对象 A、B:
    • 线程1 调用 A.normalMethod()
    • 线程2 调用 B.normalMethod()
    • 可以并发执行,因为锁对象是 AB,两把完全不同的锁。

  • 静态方法
1
2
3
4
5
6
7
public static synchronized void staticMethod() { ... }
// 等价于:
public static void staticMethod() {
synchronized (SomeClass.class) {
...
}
}

static synchronized锁的是,也就是说,同一个类里的所有 static synchronized 方法,以及任何 synchronized(SomeClass.class) 的代码块,同一时刻只能被一个线程执行。


JVM对Synchronized的优化

  1. 锁膨胀:Synchronized从无锁升级到偏向锁、再到轻量级锁,最后到重量级锁的过程,叫做锁膨胀或者锁升级;大部分场景不需要用户态到内核态(内存中阻塞)的转换了
  2. 锁消除:JVM虚拟机如果检测不到某段代码被共享和竞争的可能性,就会将这段代码所属的同步锁消除掉,从而提高程序性能的目的
  3. 锁粗化:将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁
  4. 自适应自旋锁:通过自身循环尝试获取锁,避免了从用户态转化为内核态

Synchronized锁升级过程

前置信息

  • Lock Record 是存在线程栈里的锁记录结构,用来保存加锁前对象头的 Mark Word

  • Java对象头

Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息

image-20251118103748958

Klass Point:虚拟机通过这个指针来确定这个对象是哪个类的实例

  • Monitor:同步工具、同步机制,每一个Java对象就有一把看不见的锁,称为Monitor锁

在JDK15之前过程是:无锁 —> 偏向锁 —> 轻量级锁 —> 重量级锁

JDK15:偏向锁默认禁止

JDK18:偏向锁废除——为什么不使用了?

  • 早年代码同步过度:早年的集合类如HashTable、Vector,内部存在大量的synchronized,如果单线程使用它不加偏向锁,每次都要验证;而现在普通场景HashMap、ArrayList,多线程ConcurrentHashMap之类的,根本不用每次访问都同步
  • CAS成本下降:现在硬件好了,JVM优化好了,CAS成本下降很多
  • 线程池模型不友好:同一把锁容易被不同线程先后用到,那偏向的就经常要被撤销,撤销就很贵

无锁:标志位 01

无锁没有对资源进行锁定,所有线程都能访问并修改同一个资源,但是同一时间只有一个线程可以修改成功;CAS的原理就是无锁的实现

偏向锁:标志位 01

概括:适用于只有一个线程访问同步块。 Mark Word里记录线程ID,下次同一个线程来,无需加锁。一段同步代码块一直被一个线程所访问,那么该线程就会自动获取锁,降低获取锁的代价

目的:在只有一个线程执行同步代码块时能提高性能,比如早期的HashTable、Vector;在无多线程竞争下尽量减少不必要的轻量级锁

实现:当一个线程访问同步代码块并获取锁时,会在Mark Word中存储偏向锁的线程ID,在线程进行和退出同步块时不通过CAS操作来加锁和解锁,而是检测Mark Word里是否存储着当前线程的偏向锁

偏向撤销:偏向锁在被其他线程竞争时,会先进行偏向撤销,把对象头从偏向模式恢复为普通可锁状态,再升级为轻量级锁/重量级锁

轻量级锁:标志位 00

概括:当有少量竞争时,通过CAS自旋来尝试获取锁,避免直接阻塞线程

当前的并发不是很严重

当指向锁时偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁;其他线程会通过自旋尝试获取锁,不会阻塞,从而提高性能

实现:假设T1线程拿到锁,T2,T3线程没有拿到,此时T2,T3线程不再进行阻塞,而是进行一个自旋操作尝试获取锁

升级为轻量级锁的过程:轻量级锁通过在线程栈中创建 Lock Record,把对象头obj的 Mark Word 复制到栈上,然后用 CAS 把对象头替换为指向 Lock Record 的指针并标记为轻量级锁
无竞争时加解锁只是在对象头和栈上来回 CAS,遇到竞争就先自旋,严重时再膨胀成重量级锁。

重量级锁:标志位 10

概括:竞争激烈时,轻量级锁自旋超过一定次数,就会升级为重量级锁。未获取到锁的线程会直接进入阻塞队列,等待操作系统调度,开销最大

当前的并发严重

图示:

Thread-2访问obj对象

image-20251118104643362

Thread-2释放锁,由EntryList(Thread-1、Thread-2)中的Thread-1抢到锁

image-20251118104709773

说明:EntryList 抢锁的队列,WaitSet 等条件;Owner 正在执行的线程,MarkWord 指针告诉你锁已经膨胀

  1. 当线程Thread-2访问obj对象:此时它会尝试将obj对象和操作系统提供的monitor对象相关联,靠一个指针地址关联,通过这个指针可以找到monitor对象;此时没有其他线程,Thread-2就绑定在Monitor的Owner上
  2. Thread-1来了,访问obj对象:首先它会根据obj对象中的MarkWord指向的Monitor对象,看里面有没有Owner,如果有,就将Thread-1存储在EntryList中,将线程置为Block阻塞状态
  3. Thread-3来了,访问obj对象:此时同步代码块没有执行完毕,将Thread-3同理存储在EntryList中
  4. 当Thread-2线程结束后,会变遍历EntryList中的线程进行抢锁,抢到锁的线程绑定再Owner上(假设Thread-1抢到了锁)

image-20251118105034462

Reentrantlock

工作原理

  1. 底层依赖于AbstractQueuedSynchronizer(AQS)这个抽象类,在它的基础上通过内部类Sync实现具体锁操作
  2. 可中断性:线程在等待锁的过程中,可以被其他线程中断而提前结束等待
  3. 设置超时时间:等待一段时间后如果没获得锁,则放弃锁的获取
  4. 公平锁和非公平锁:默认是非公平锁,在创建的时加 true 就可以创建公平锁

公平锁和非公平锁的Lock()方法唯一区别就在于是否判断 hasQueuedPredecessors() 这个方法

hasQueuedPredecessors():表示在等待队列中是否已经有线程在排队了

公平锁会判断hasQueuedPredecessors(),如果有线程在排队,当前线程就不再尝试获取锁

注意】tryLock():tryLock可以插队,当线程执行tryLock() 方法时,一旦有线程释放了锁,那么正在tryLock的线程就能获取锁,即是公平锁模式

1
2
3
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
  1. 多个条件变量:支持多个条件变量,每一个变量可以与ReentrantLock关联——一把 ReentrantLock 可以生成很多个 Condition

  2. 可重入性

同一个线程可以多次获取同一把锁,不产生死锁。通过holdCount实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class ReentrantLockDemo {
private final ReentrantLock lock = new ReentrantLock();

public void methodA() {
lock.lock(); // 第一次获取锁
try {
System.out.println(Thread.currentThread().getName() + " 进入 methodA");
methodB(); // 调用另一个上锁方法
} finally {
lock.unlock(); // 对应释放一次
}
}

public void methodB() {
lock.lock(); // 第二次获取同一把锁
try {
System.out.println(Thread.currentThread().getName() + " 进入 methodB");
} finally {
lock.unlock(); // 对应释放一次
}
}

public static void main(String[] args) {
ReentrantLockDemo demo = new ReentrantLockDemo();
demo.methodA();
}
}
  • 一个线程第一次获取锁时,计数器 + 1
  • 如果同一个线程再次获取锁,计数器 + 1
  • 线程释放锁时,计数器 - 1
  • 当计数器为0,锁才完全释放

应用场景

  • 高级锁功能需求
  • 性能优化

Synchronized和Reentrantlock的区别

image-20251118112144598


CAS、AQS

CAS

CAS是一个原子指令本身不是锁,包含三个操作数:需要读写的内存值V,需要比较的值(预期值)A,新值B

当且仅当 V 的值等于 A 时,CAS通过原子方式用新值B来更新V的值(“比较+更新”整体是一个原子操作),否则不会执行任何操作。

Java中的CAS是( 由 AtomicInteger 类中 compareAndSet 方法 )调用 unsafe对象方法

compareAndSet(int expect, int update)——如果类内部的value值 = except,就将value值更新为update

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// compareAndSet源码
public final class AtomicInteger extends Number implements java.io.Serializable {
private volatile int value;
private static final long valueOffset;

static {
try {
valueOffset = Unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
}
catch (Exception ex) { throw new Error(ex); }
}

public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
// Other methods omitted for brevity
}

unsafe.compareAndSwapInt()中的字段

  • Object obj:表示要更新的对象
  • long offset:对象内字段的偏移量,表示AtomicInteger对象中的值内存位置
  • int expected:字段预期值
  • int x:要设置的新值

应用场景:抢票

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class CASTicketingSystem {
// 票的数量为100
private static AtomicInteger availableTickets = new AtomicInteger(100);

// 传入购买的张数
public boolean bookTicket(int requestedTickets) {

// 【重点】通过while自旋
while (true) {

// 获取剩余票数
int currentAvailable = availableTickets.get();

// 如果①剩余票数 < 购买张数 ②获取剩余票数与类中value值不匹配(数据被其他线程修改了)
if (currentAvailable < requestedTickets ||
!availableTickets.compareAndSet(currentAvailable, currentAvailable - requestedTickets)) {
// 继续处理
}
return true; // 购票成功
}
}
}

通过while自旋:一旦CAS失败,就重新读取最新的值,再计算新的结果,直到成功为止

compareAndSet逻辑

  • CAS 会检查当前余票是不是刚才读到的 currentAvailable。
  • 如果没被别人改过,就更新成功。
  • 如果这期间有人也在抢票,把余票改掉了,那 CAS 就失败,返回 false。

该应用相比较于传统方法,用synchronized将“读余票 —> 减扣 —> 返回结果”整个过程抱起来,线程是安全,但是效率低;而用CAS保证了在并发环境下修改共享变量的正确性


CAS有什么缺点?

  1. ABA问题

线程一:先读到内存值为A,准备改成C;
线程二:将它修改“A —> B —> A”修改一圈回到了A;
线程一:再做CAS以为数据没动过,CAS成功了;

问题是:期间的状态线程一是不知道的

解决方法:将“值“变为“版本号 + 值”,只要发现版本号不一致就说明其他线程修改了的;变化过程就从“A-B-A”变成了“1A-2B-3A”

  1. 循环时间长开销大

  2. 只能保证一个共享变量的原子操作

CAS对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的

比如:同一个对象的不同字段:比如 order.status 和 order.version。

order对象中有几十种字段,但我就想让status、version完成一次CAS

  • 原始值:
    status = '1', version = 1
  • 期望更新为:
    status = '2', version = 2

想要的效果是

对其他线程来说,要么看到 ('1', 1)
要么看到 ('2', 2)
不要看到中间奇怪的组合:('1', 2)('2', 1)

另一个线程 B 在两步之间读:

  • 可能看到:status = "2", version = 1
  • 或:status = "1", version = 2

这就不是原子操作

不满足原子性的操作,要解决很简单,就是将这两个字段单独抽出来做成一个变量OrderState,做一次CAS

1
2
3
4
5
6
7
8
9
class OrderState {
final String status;
final int version;

OrderState(String status, int version) {
this.status = status;
this.version = version;
}
}

对于order中几十种字段,如果都需要这样,代码非常复杂


AQS

全称为AbstractQueuedSynchronizer,是Java中的一个抽象类;AQS是一个用于构建锁、同步器、协作工具类的工具类

核心

  • 如果共享资源空闲:就将当前请求资源的线程设为有效的工作线程,将共享资源设置为被占用状态
  • 如果共享资源被占用:需要一定的阻塞等待唤醒机制来保证锁分配,这个机制主要是用CLH队列的变体实现的,将暂时获取不到的锁的线程加入队列中

CLH队列

CLH队列是单向链表

但是AQS中是CLH变体虚拟双向链表,AQS通过将每条请求共享资源的线程封装成一个节点来实现锁的分配

image-20251118124819947

AQS使用一个Volatile的int类型的成员变量State来表示同步状态,通过内置的FIFO队列来完成资源的获取的排队工作,通过CAS完成对State值的修改


AQS主要完成的任务

  • 同步状态(比如计数器)的原子性管理
  • 线程的阻塞和解除阻塞
  • 队列的管理

AQS最核心的三大部分

  1. 状态:state

它会根据不同的实现类而改变

  • Semapore:剩余许可证的数量
  • CountDownLatch:还需要倒数的数量
  • Reentrantlock:锁的占用状态,包括可重入计数,当state的值为0的时候,表示该Lock不被任何线程占用
  1. FIFO队列(双向链表)

AQS会维护一个等待的队列,把线程都放在这个队列中,这个队列是双向链表的形式

这个队列用来存放等待的线程,当锁释放时,锁管理器就会挑选一个合适的线程来占有这个刚刚释放的锁

  1. 实现的获取/释放等重要方法(重写

由协作类自己去实现,每个实现类都要重写tryAcquire和tryRelease等方法


如何用AQS实现一个可重入的公平锁

  1. 继承AbstractQueuedSynchronizer

创建一个内部类继承于它,重写tryAcquire(获取锁)、tryRelease(释放锁)、isHeldExclusively(锁是否被当前线程持有)

  1. 实现可重入逻辑

在tryAcquire方法中,检查当前线程是否持有锁

  • 持有锁:通过state增加锁的持有次数
  • 没有持有锁:尝试用CAS操作来获取锁
  1. 实现公平性

在tryAcquire方法中,按照队列顺序来获取锁,先检查等待队列中是否有线程在等待,如果有,当前线程必须进入队列等待,而不是直接竞争锁

  1. 创建锁的外部类

创建一个外部类,内部持有AbstractQueuedSynchronizer的子类对象,并提供lock、unlock方法,这些方法将调用AbstractQueuedSynchronizer子类中的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;

public class FairReentrantLock {

private static class Sync extends AbstractQueuedSynchronizer {
// 判断锁是否被当前线程持有
@Override
protected boolean isHeldExclusively() {
return getExclusiveOwnerThread() == Thread.currentThread();
}

// 尝试获取锁
@Override
protected boolean tryAcquire(int acquires) {
// ...
}

// 尝试释放锁
@Override
protected boolean tryRelease(int releases) {
// ...
}

// 提供一个条件变量,用于实现更复杂的同步需求,这里只是简单实现
Condition newCondition() {
return new ConditionObject();
}
}

private final Sync sync = new Sync();

// 加锁方法
public void lock() {
sync.acquire(1);
}

// 解锁方法
public void unlock() {
sync.release(1);
}

// 判断当前线程是否持有锁
public boolean isLocked() {
return sync.isHeldExclusively();
}

// 提供一个条件变量,用于实现更复杂的同步需求,这里只是简单实现
public Condition newCondition() {
return sync.newCondition();
}
}

CAS和AQS有什么关系?

CAS为AQS提供原子操作的支持:AQS内部使用CAS操作来更新state变量,实现线程安全的状态修改

为什么AQS中CAS不需要加版本号避免ABA问题?
AQS 的 CAS 根本不依赖“期间有没有变过”,只依赖“当前时刻的值是否满足条件”


ThreadLocal

什么是ThreadLocal?

ThreadLocal就是线程局部变量


ThreadLocal的作用

实现线程变量的存取,在线程生命周期内任何位置都可以直接获取到这个变量不同线程拿到的是各自线程维护的那份值,彼此隔离。

【例子】:当一个用户的请求发送到服务器上的时候,创建处理这个用户的线程,将用户的信息提前加载到ThreadLocal中,这样就可以避免多次重复的查询数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import java.util.*;

public class Main {
private static ThreadLocal<String> userInfoThreadLocal = new ThreadLocal<>();

public static void main(String[] args) {
// 模拟多线程场景
new Thread(() -> {
userInfoThreadLocal.set("用户A");
processOrder();
}).start();

new Thread(() -> {
userInfoThreadLocal.set("用户B");
processOrder();
}).start();
}

// 独属于用户X的业务
private static void processOrder() {
String user = userInfoThreadLocal.get();
System.out.println("正在处理 " + user + " 的订单");
// 模拟订单业务
}
}

/*
输出
正在处理 用户A
正在处理 用户B
*/

包含关系

image-20251118141151909

  • 在Thread类中定义了ThreadLocalMap
  • 在ThreadLocalMap中有内部成员 entry[]
  • entry里面存的值是键值对
    • key: ThreadLocal
    • value: value

image-20251118141245739


为什么key(ThreadLocal)是弱引用?

避免内存泄漏,若使用强引用,当栈帧结束后,由于entry对象有引用,因此不会被垃圾回收,造成内存泄漏;若使用了弱引用,下一次GC就会被回收


为什么value是强引用?

当使用ThreadLocal对象中的set、get、delete方法会检查是否存在key,如果不存在就将value删除;会自动检测值;如果使用了弱引用,一次GC就会将值给清零,数据不一致


ThreadLocal 怎样实现线程隔离?/ 父子线程怎么共享数据给子线程?

每个线程都会有自己的 ThreadLocalMap 变量副本,存储于线程私有的虚拟机栈中,而不是堆中,不会被其他线程访问到,因此实现了线程隔离。


ThreadLocal 有什么问题?

ThreadLocal 的键是弱引用,键被回收后,值仍由线程持有,因此在长寿命线程(尤其线程池、SpringBoot)且没有后续清理操作时,value 可能长期无法释放。为避免泄漏,应在使用完马上 remove()


ThreadLocalMap怎么解决Hash冲突?

通过开放寻址法


ThreadLocalMap扩容机制?

它的扩容阈值是数组长度的2/3。当数组中的Entry数量超过这个阈值时,就会先尝试探测式清理(expungeStaleEntries)掉那些Key为null的陈旧Entry。如果清理后数量还超过阈值的 3/4,也就是数组长度的1/2,才会进行真正的扩容(resize),创建一个2倍大的新数组,然后重新哈希(rehash)所有有效的Entry。


volatile关键字

volatile关键字有什么作用?

  1. 保证对这个变量的写操作会立即刷新到主存中,对这个变量的读操作会直接从主存中读取,保证了多线程下对变量访问的可见性
  2. 禁止指令重排序优化

volatile关键字在java中对内存屏障来禁止特点的指令重排序

image-20251118141655981

指令重排序是什么?

执行程序时,为了提高性能,处理器和编译器会对指令进行重排序,需要满足

  • 在单线程环境下不能改变程序的运行结果
  • 存在数据依赖关系的不允许重排序

【例子】

image-20251118141752521

C依赖于A、B;
A、B相互没关系;

于是A、B可以互换位置,但是C不行,必须在A、B下面


volatile可以保证线程安全吗?

volatile关键字可以保证可见性,但不能保证原子性,因此不能完全保证线程安全。


线程池

介绍一下线程池工作原理

po_diagram

wc = workerCount(当前工作线程数)

1
2
3
4
5
6
7
8
9
10
11
submit 任务

├─ if 当前线程数 < 核心线程数 → 创建一个线程立即执行任务,不进入队列

├─ else if 尝试入队是否成功 → 丢进去
│ └─ if 入队成功 → 保证至少有一个工作线程从队列里把任务取出来执行
│ → 若此时不在 RUNNING,把任务移除并拒绝

├─ else if 入队失败 & 当前线程 < 最大线程数 → 创建一个线程立即执行任务,不进入队列

└─ else → 采取拒绝策略

线程池的参数有哪些?

image-20251118142326145

  1. corePoolSize:线程池核心线程数量,默认情况下,线程池中线程的数量如果 ≤ corePoolSize,那么即使这些线程处于空闲状态,也不会被销毁
  2. maximumPoolSize

限制了线程池能创建的最大线程总数

当corePoolSize已满 并且 尝试将新任务加入阻塞队列失败(队列已满),此时线程数 < maximumPoolSize,就会创建新线程执行此任务

当corePoolSize满,队列满,线程数已达maximumPoolSize,此时有新任务提交,就会出发拒绝策略

  1. keepAliveTime:当线程池中线程数量 > corePoolSize,并且某个线程空闲时间超过keepAliveTime,这个线程就会被销毁
  2. unit:keepAliveTime的时间单位
  3. workQueue:工作队列,当没有空闲的线程执行新任务时,该任务就会被放入工作队列中,等待执行
  4. threadFactory:线程工厂,给线程取名字等等
  5. handler:拒绝策略

线程池工作队列满了有哪些拒绝策略?

  1. CallerRunsPolicy:使用线程池的调用者所在的线程去执行被拒绝的任务,除非线程池被停止或者线程池的任务队列已有空缺。
  2. AbortPolicy:直接抛出一个任务被线程池拒绝的异常
  3. DiscardPolicy:不做任何处理,静默拒绝提交的任务
  4. DiscardOldestPolicy:抛弃最老的任务,然后执行该任务
  5. 自定义拒绝策略,通过实现接口可以自定义任务拒绝策略

线程池提交execute和submit有什么区别?

返回值:execute()没有返回值;submit()会返回一个Future对象,可以通过它获取任务执行结果或取消任务

异常处理:execute()提交的任务如果抛出异常,会直接抛出,可能会终止线程;submit()提交的任务,异常会被封装在Future.get()里,需要调用该方法才能捕获到

参数:execute()只能提交Runnable任务;submit()可以提交Runnable和Callable任务


线程池参数设置的经验?

核心线程数(corePoolSize)

  • CPU密集型:corePoolSize = CPU核数 + 1(避免过多线程竞争CPU)
  • IO密集型:corePoolSize = CPU核数 x 2(或更高,具体看IO等待时间)
  1. 电商场景:特点瞬时高并发,任务处理时间短,线程池配置可设置如下

image-20251118142908735

  1. 后台数据处理服务:稳定流量,任务处理时间长,允许一定延迟,线程池配置可设置如下

image-20251118142929614


核心线程数可不可以设置为0?

可以,当核心线程数为0时候,这表示线程池不会常驻任何线程,当有任务提交的时候,会创建非核心线程来执行任务


线程池种类有哪些?

  1. ScheduledThreadPool:可以设置定期的执行任务,支持定时或周期性执行任务
  2. FixedThreadPool:核心线程数和最大线程数一样,可以把它看作是固定线程数的线程池
  3. CachedThreadPool:可缓存线程池,特点在于线程数几乎可以无限增加的,当线程闲置时还可以对线程进行回收,也就是说线程池的线程数量不是固定不变的
  4. SingleThreadExecutor:使用唯一的线程去执行任务,原理和FixedThreadPool是一样的,由于只有一个线程,非常适合任务按照顺序提交来执行

线程池一般怎么用?

虽然Java中Executors类定义了一些快捷工具方法,能快速创建线程池。

但是《阿里巴巴Java开发手册》中提到,禁止使用这些方法,应该手动new ThreadPoolExecutor来创建线程

要根据实际的业务来评估核心参数(可以举**线程池参数设置的经验?**例子)


线程池中shutdown (),shutdownNow()这两个关闭方法有什么作用?

  • shutdown()

使用了会让状态为SHUTDOWN,温和关闭正在执行的任务会继续执行下去,没有被执行的则会中断,此时不能往线程池中添加任何任务,否则会抛出异常

  • shutdownNow()

使用了会让状态为STOP,暴力关闭,通过调用Thread.interrupt()方法尝试停止所有正在执行的线程,不再处理还在线程池队列中等待的任务,也会返回那些未执行的任务


提交给线程池中的任务可以被撤回吗?

可以,当向线程池提交任务时,会得到一个Futrue对象,这个对象提供了几种方法来管理任务的执行,包括取消任务

取消任务主要的方法是 Future 接口中的 cancel(bool mayInterruptIfRunning) 方法

  • true:如果任务开始执行,中断任务
  • false:如果任务开始执行,不被中断

线程池中线程是如何保活和定时回收的?

在getTask() 方法中

核心线程常驻、非核心该走就走

image-20251118143806965

allowCoreThreadTimeOut
要不要对“核心线程”也启用 keepAliveTime 的闲置超时回收机制默认是 false,也就是核心线程闲着也不走

通过allowCoreThreadTimeOut设置timed的取值,根据timed的值调用相应的函数

image-20251118144139837

  • false(默认):取任务时用 workQueue.take() 取不到就无限等待,不存在闲置超时和回收
  • true:取任务时用 workQueue.poll(),超时拿不到任务可能被干掉

总结:

想要开启闲置超时回收机制

通过ThreadPoolExecutor.allowCoreThreadTimeOut(true);

并且设置一个生存期ThreadPoolExecutor.setKeepAliveTime(60, TimeUnit.SECONDS);