Java 线程池源码解读与实践
肖文英
Java 资深研发工程师
27 人已学习
立即订阅
课程目录
已更新 28 讲/共 32 讲
并发编程基础知识 (8讲)
时长 09:08
时长 40:33
时长 21:26
时长 44:37
时长 09:11
线程池基础知识 (6讲)
时长 12:38
时长 09:28
时长 30:59
时长 44:51
时长 45:02
时长 15:15
线程池实现详解 (8讲)
时长 14:10
时长 23:21
时长 25:00
时长 13:57
时长 36:02
时长 17:28
时长 42:08
常见开源线程池 (3讲)
时长 22:14
时长 27:06
时长 25:32
Java 线程池源码解读与实践
15
15
1.0x
00:00/00:00
登录|注册

什么时候使用线程就可以了

使用场景

在 Java 中,直接使用线程而不使用线程池的情形相对较少,因为线程池提供了许多优点,包括资源复用、减少线程创建和销毁的开销、控制并发度等。然而,在某些特定场景下,直接使用线程可能是必要的或更合适的。以下是一些可能的情景:
短任务:如果你的任务非常短暂,使用线程池可能带来的开销(如线程上下文切换、任务排队等)可能会超过直接创建线程的开销。
任务数量非常少:如果任务数量非常少,那么使用线程池可能并不划算,因为线程池本身也需要一定的资源来管理线程。例如如果你需要执行一次性的任务,且之后不会再重复执行类似的任务,那么直接创建线程可能比启动线程池更简便。
任务之间高度依赖:如果任务之间存在高度的依赖关系,需要严格控制任务的执行顺序和同步,那么使用线程池可能会增加管理和调度的复杂性。此时,直接创建线程并手动管理它们可能更易于理解和控制。
需要精确控制线程创建和销毁:在某些应用中,可能需要精确控制线程的创建和销毁时间,或者需要为每个任务创建一个唯一的线程。
测试环境:在编写单元测试或集成测试时,有时可能希望直接控制线程的创建和执行,以更好地隔离测试用例。
资源受限的环境:在某些嵌入式设备或资源极其有限的环境下,可能没有足够资源来维护一个线程池。在这种情况下,直接使用线程可能是唯一的选择。
与外部服务交互:如果你的代码需要与外部服务交互,而这些服务有自己的线程模型或要求,你可能需要直接创建线程来满足这些要求。
尽管如此,直接使用线程通常不是推荐的做法,因为这涉及到手动管理线程的生命周期,容易引入错误。线程池不仅简化了线程管理,还能帮助开发者更好地控制并发程度,防止系统过载。因此,除非确实有必要,一般建议优先考虑使用线程池。

案例 1 ShutdownHook

在 Java 中,addShutdownHook 方法用于在 JVM 关闭时执行一些清理操作。通过注册一个 shutdown hook,你可以在 JVM 正常关闭或接收到终止信号(如 `SIGTERM`)时执行特定的代码。
ShutdownHook 对应的场景属于短任务 + 任务数量非常少。
需要注意我们调用 Runtime.getRuntime().addShutdownHook 时,入参必须是线程对象。

代码

public class ShutdownHookExample {
public static void main(String[] args) {
// 创建一个Runnable任务,作为shutdown hook
Runnable shutdownHook = () -> {
System.out.println("JVM is shutting down...");
// 这里可以添加任何需要在JVM关闭时执行的清理操作
System.out.println("Cleanup actions are being performed...");
};
// 注册shutdown hook
Runtime.getRuntime().addShutdownHook(new Thread(shutdownHook));
// 主线程继续执行任务
System.out.println("Main thread continues to run...");
// 模拟长时间运行的任务
try {
Thread.sleep(5000); // 休眠5秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Main thread is finishing...");
}
}
在这个示例中:
1. 我们创建了一个 Runnable 对象 shutdownHook,它包含了我们希望在 JVM 关闭时执行的代码。
2. 使用 Runtime.getRuntime().addShutdownHook(new Thread(shutdownHook))`将这个 Runnable`注册为一个 shutdown hook。
3. 主线程继续执行其它任务,比如打印一条消息并模拟一个长时间运行的任务。
4. 当 JVM 关闭时(例如,用户按下 Ctrl+C),会触发 shutdown hook,执行我们在 shutdownHook 中定义的代码。

注意事项

shutdown hook 是在 JVM 关闭之前执行的,但它们不能保证一定会执行。例如,如果 JVM 崩溃或者被强制终止,那么 shutdown hook 可能不会有机会运行。
shutdown hook 应该尽量简单和快速,因为它们是在 JVM 关闭过程中执行的,可能会影响关闭的速度。
每个应用程序只能注册有限数量的 shutdown hook,具体数量取决于 JVM 实现。

案例 2 3 个线程循环打印 123

这个例子我们在面试中经常遇到,属于任务之间存在高度依赖的场景。

分析问题

1 三个线程不能同时运行,并且程序开始后必须是线程 1 先运行
2 线程 1 运行后通知线程 2 运行,线程 2 运行后通知线程 3 运行,线程 3 运行后通知线程 1 运行,循环进行
3 通过一个共享状态控制 3 个线程的运行
4 共享状态需要保证线程安全性,所以要加锁
5 多个线程需要使用同一个锁 Lock,所以也要使用同一个 Condition
6 线程打印完后需要暂停运行,需要使用保护性暂挂模式,只有序号在前面的线程打印后才能继续运行
7 保护性暂挂模式需要使用 Condition 的 await 和 signalAll
8 因为是多个线程等待,为了代码通用性使用 signalAll 唤醒所有等待的线程
9 通过共享变量实现可以运行的条件,并且唤醒所有等待的线程后只有某个线程满足可以运行的条件

代码

public class Print123 {
public static void main(String[] args) {
// 三个线程交叉顺序打印当前线程ID 123123123123123
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
MyThread thread1 = new MyThread(1, lock, condition);
MyThread thread2 = new MyThread(2, lock, condition);
MyThread thread3 = new MyThread(3, lock, condition);
preRunId = 3;
thread1.start();
thread2.start();
thread3.start();
}
public static volatile int preRunId;
public static class MyThread extends Thread {
private int name;
private Lock lock;
private Condition condition;
public MyThread(int name, Lock lock, Condition condition) {
this.name = name;
this.lock = lock;
this.condition = condition;
}
public void run() {
// 每次运行时都要加锁,所以需要在while内部加锁,而不是while外部
while (true) {
try {
System.out.println(this.name + " before lock");
lock.lock();
System.out.println(this.name + " locked");
while (canRun() == false) {
System.out.println(this.name + " await");
condition.await();
}
System.out.println(this.name + " await done");
System.out.println(this.name);
preRunId = this.name;
condition.signalAll();
System.out.println(this.name + " notifyAll done");
} catch (Exception exp) {
//暂不处理
} finally {
lock.unlock();
System.out.println(this.name + " unlock");
}
}
}
private boolean canRun() {
if (preRunId == 3 && this.name == 1) {
return true;
}
return (preRunId + 1) == this.name;
}
}
}

分析运行

进程开始运行,线程 1 抢占到锁,其它线程没有抢占到锁,处于等待获取锁状态,判断共享变量是否满足条件 (满足),打印,执行 Condition.signalAll,其它线程没有处于条件等待状态,所以 Condition.signalAll 没有对其它线程产生影响
线程 1 释放锁,其它线程收到释放锁通知:假定线程 3 抢占到锁 (线程 1+2 没有抢占到锁),线程 3 开始运行,判断共享变量是否满足条件 (不满足),进入等待队列,并释放锁;
假定线程 2 抢占到锁 (线程 1+3 没有抢占到锁),线程 2 开始运行,判断共享变量是否满足条件 (满足),继续运行,打印,执行 Condition.signalAll,并释放锁
线程 3 收到 signal 信号,从等待队列被移除,尝试获取锁
假定线程 1 抢占到锁 (线程 2+3 没有抢占到锁),线程 1 开始运行,判断共享变量是否满足条件 (不满足),进入等待队列,并释放锁
假定线程 3 抢占到锁 (线程 1+2 没有抢占到锁),线程 3 开始运行,判断共享变量是否满足条件 (满足),继续运行,打印,执行 Condition.signalAll,并释放锁

案例 3 需要精确控制线程创建和销毁

在 Java 中,如果你需要精确控制线程的创建和销毁,可以使用 Thread 类直接管理线程。下面是一个示例代码,展示了如何手动创建、启动和管理线程的生命周期。
假设我们有三个任务:Task1、Task2 和 Task3。Task2 依赖于 Task1 的完成,Task3 依赖于 Task2 的完成。我们将使用 join() 方法来确保每个任务在其依赖的任务完成后再执行。

代码

public class DependentTasksExample {
public static void main(String[] args) {
// 创建并启动 Task1 线程
Thread task1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Task1 is running");
try {
// 模拟任务执行时间
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Task1 is completed");
}
});
task1.start();
// 创建并启动 Task2 线程,等待 Task1 完成
Thread task2 = new Thread(new Runnable() {
@Override
public void run() {
try {
task1.join(); // 等待 Task1 完成
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Task2 is running");
try {
// 模拟任务执行时间
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Task2 is completed");
}
});
task2.start();
// 创建并启动 Task3 线程,等待 Task2 完成
Thread task3 = new Thread(new Runnable() {
@Override
public void run() {
try {
task2.join(); // 等待 Task2 完成
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Task3 is running");
try {
// 模拟任务执行时间
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Task3 is completed");
}
});
task3.start();
// 主线程等待所有任务完成
try {
task1.join();
task2.join();
task3.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("All tasks are completed");
}
}
在这个示例中:
我们创建了三个独立的线程 task1, task2, 和 task3,每个线程都执行一个特定的任务。task2 使用 task1.join() 来等待 task1 完成,task3 使用 task2.join() 来等待 task2 完成。这确保了任务之间的依赖关系。主线程使用 task1.join(), task2.join(), 和 task3.join() 来等待所有任务完成。每个任务在开始时打印一条消息,并在完成后打印另一条消息。
这种方法提供了对线程生命周期的精确控制,但需要注意线程同步和异常处理,以确保程序的正确性和健壮性。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
  • 解释
  • 总结
仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《Java 线程池源码解读与实践》
立即购买
登录 后留言

精选留言

由作者筛选后的优质留言将会公开显示,欢迎踊跃留言。
收起评论
大纲
固定大纲
使用场景
案例 1 ShutdownHook
代码
注意事项
案例 2 3 个线程循环打印 123
分析问题
代码
分析运行
案例 3 需要精确控制线程创建和销毁
代码
显示
设置
留言
收藏
1
沉浸
阅读
分享
手机端
快捷键
回顶部