什么时候使用线程就可以了
肖文英
使用场景
在 Java 中,直接使用线程而不使用线程池的情形相对较少,因为线程池提供了许多优点,包括资源复用、减少线程创建和销毁的开销、控制并发度等。然而,在某些特定场景下,直接使用线程可能是必要的或更合适的。以下是一些可能的情景:
短任务:如果你的任务非常短暂,使用线程池可能带来的开销(如线程上下文切换、任务排队等)可能会超过直接创建线程的开销。
任务数量非常少:如果任务数量非常少,那么使用线程池可能并不划算,因为线程池本身也需要一定的资源来管理线程。例如如果你需要执行一次性的任务,且之后不会再重复执行类似的任务,那么直接创建线程可能比启动线程池更简便。
任务之间高度依赖:如果任务之间存在高度的依赖关系,需要严格控制任务的执行顺序和同步,那么使用线程池可能会增加管理和调度的复杂性。此时,直接创建线程并手动管理它们可能更易于理解和控制。
需要精确控制线程创建和销毁:在某些应用中,可能需要精确控制线程的创建和销毁时间,或者需要为每个任务创建一个唯一的线程。
测试环境:在编写单元测试或集成测试时,有时可能希望直接控制线程的创建和执行,以更好地隔离测试用例。
资源受限的环境:在某些嵌入式设备或资源极其有限的环境下,可能没有足够资源来维护一个线程池。在这种情况下,直接使用线程可能是唯一的选择。
与外部服务交互:如果你的代码需要与外部服务交互,而这些服务有自己的线程模型或要求,你可能需要直接创建线程来满足这些要求。
尽管如此,直接使用线程通常不是推荐的做法,因为这涉及到手动管理线程的生命周期,容易引入错误。线程池不仅简化了线程管理,还能帮助开发者更好地控制并发程度,防止系统过载。因此,除非确实有必要,一般建议优先考虑使用线程池。
案例 1 ShutdownHook
在 Java 中,addShutdownHook 方法用于在 JVM 关闭时执行一些清理操作。通过注册一个 shutdown hook,你可以在 JVM 正常关闭或接收到终止信号(如 `SIGTERM`)时执行特定的代码。
ShutdownHook 对应的场景属于短任务 + 任务数量非常少。
需要注意我们调用 Runtime.getRuntime().addShutdownHook 时,入参必须是线程对象。
代码
在这个示例中:
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 通过共享变量实现可以运行的条件,并且唤醒所有等待的线程后只有某个线程满足可以运行的条件
代码
分析运行
进程开始运行,线程 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() 方法来确保每个任务在其依赖的任务完成后再执行。
代码
在这个示例中:
我们创建了三个独立的线程 task1, task2, 和 task3,每个线程都执行一个特定的任务。task2 使用 task1.join() 来等待 task1 完成,task3 使用 task2.join() 来等待 task2 完成。这确保了任务之间的依赖关系。主线程使用 task1.join(), task2.join(), 和 task3.join() 来等待所有任务完成。每个任务在开始时打印一条消息,并在完成后打印另一条消息。
这种方法提供了对线程生命周期的精确控制,但需要注意线程同步和异常处理,以确保程序的正确性和健壮性。
公开
同步至部落
取消
完成
0/2000
笔记
复制
AI
- 深入了解
- 翻译
- 解释
- 总结
仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《Java 线程池源码解读与实践》
《Java 线程池源码解读与实践》
立即购买
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
登录 后留言
精选留言
由作者筛选后的优质留言将会公开显示,欢迎踊跃留言。
收起评论