Java 的多线程编程提供了强大的工具,例如 wait()
和 notify()
方法,用于线程间的协调。然而,这些方法必须在同步块中调用,否则会抛出 IllegalMonitorStateException
异常。此外,ThreadLocal
提供了一种简单的方式为每个线程存储独立的数据,但若使用不当,可能导致内存泄漏问题。
多线程编程一直是软件开发中极具挑战性的领域。如何在多个线程之间协调工作?为什么某些方法必须在特定上下文中使用?为什么一个看似简单的工具如 ThreadLocal
,可能隐藏着难以发现的内存泄漏风险?
1. 为什么 wait()
和 notify()
必须在同步块中调用?
1.1 wait()
和 notify()
的作用
wait()
和 notify()
是 Object 类中的方法,用于线程间的通信。wait()
会使当前线程进入等待状态,直到被其他线程唤醒;而 notify()
则负责唤醒一个等待中的线程。
代码:
输出可能为:
1.2 必须在同步块中调用的原因
调用 wait()
和 notify()
时,线程需要持有目标对象的监视器(即锁)。这是因为:
- 线程安全:多线程环境下,多个线程可能同时访问同一个对象,若没有锁机制,
wait()
和notify()
的调用顺序可能被打乱,导致不可预期的行为。 - 确保原子性:通过同步块或同步方法,Java 保证了这些方法的调用和锁的释放/获取是原子的。
如果在非同步块中调用,会抛出以下异常:
1.3 在非同步块中调用
以下代码会导致异常:
2. ThreadLocal
的内存泄漏问题
2.1 什么是 ThreadLocal
?
ThreadLocal
提供了一种机制,用于为每个线程保存独立的变量副本。其典型使用场景包括:
- 数据库连接管理
- 用户会话存储
- 线程上下文变量
代码:
输出:
2.2 内存泄漏的原因
ThreadLocal
的潜在问题来自其实现机制。每个线程维护一个 ThreadLocalMap
,键为 ThreadLocal
对象,值为具体存储的数据。如果 ThreadLocal
对象没有被及时清理,可能导致以下问题:
- 强引用泄漏:
ThreadLocalMap
使用的是弱引用,但其值为强引用,如果线程长时间存在,值对象无法被回收。 - 线程池问题:在线程池中,线程会被重复利用,导致旧数据可能长期存在。
2.3 导致内存泄漏的代码
如果线程长时间不终止或线程池重复使用该线程,内存可能无法释放。
3. 避免 ThreadLocal
内存泄漏的最佳实践
3.1 使用 remove()
方法
显式调用 remove()
方法清理数据:
3.2 避免存储大对象
尽量避免使用 ThreadLocal
存储大体积对象,如缓存或文件数据。
3.3 使用框架自带的工具
某些框架提供了更安全的上下文变量管理工具,例如 Spring 的 RequestContextHolder
。
总结
Java 的 wait()
和 notify()
方法以及 ThreadLocal
的设计初衷是为开发者提供强大的多线程工具,但它们的使用需要小心。同步块是 wait()
和 notify()
正常工作的基础,确保线程间通信的安全性;而 ThreadLocal
虽然简化了线程内数据管理,但不当的使用可能导致严重的内存泄漏。通过掌握这些机制和最佳实践,可以避免常见的陷阱,提高代码的健壮性和效率。