Java 的多线程编程提供了强大的工具,例如 wait()notify() 方法,用于线程间的协调。然而,这些方法必须在同步块中调用,否则会抛出 IllegalMonitorStateException 异常。此外,ThreadLocal 提供了一种简单的方式为每个线程存储独立的数据,但若使用不当,可能导致内存泄漏问题。

多线程编程一直是软件开发中极具挑战性的领域。如何在多个线程之间协调工作?为什么某些方法必须在特定上下文中使用?为什么一个看似简单的工具如 ThreadLocal,可能隐藏着难以发现的内存泄漏风险?

为什么Java的wait()和notify()必须在同步块调用?Java中使用ThreadLocal存储数据是否会导致内存泄漏?_数据

1. 为什么 wait()notify() 必须在同步块中调用?

1.1 wait()notify() 的作用

wait()notify() 是 Object 类中的方法,用于线程间的通信。wait() 会使当前线程进入等待状态,直到被其他线程唤醒;而 notify() 则负责唤醒一个等待中的线程。

代码:

class SharedResource {
    public synchronized void produce() {
        System.out.println("Producing...");
        try {
            wait();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        System.out.println("Resumed after being notified!");
    }

    public synchronized void consume() {
        System.out.println("Notifying...");
        notify();
    }
}

public class WaitNotifyDemo {
    public static void main(String[] args) {
        SharedResource resource = new SharedResource();
        Thread producer = new Thread(resource::produce);
        Thread consumer = new Thread(resource::consume);

        producer.start();
        consumer.start();
    }
}
  • 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.

输出可能为:

Producing...
Notifying...
Resumed after being notified!
  • 1.
  • 2.
  • 3.
1.2 必须在同步块中调用的原因

调用 wait()notify() 时,线程需要持有目标对象的监视器(即锁)。这是因为:

  1. 线程安全:多线程环境下,多个线程可能同时访问同一个对象,若没有锁机制,wait()notify() 的调用顺序可能被打乱,导致不可预期的行为。
  2. 确保原子性:通过同步块或同步方法,Java 保证了这些方法的调用和锁的释放/获取是原子的。

如果在非同步块中调用,会抛出以下异常:

Exception in thread "main" java.lang.IllegalMonitorStateException
  • 1.
1.3 在非同步块中调用

以下代码会导致异常:

class IncorrectUsage {
    public void waitWithoutLock() {
        try {
            wait(); // 没有同步块
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

2. ThreadLocal 的内存泄漏问题

2.1 什么是 ThreadLocal

ThreadLocal 提供了一种机制,用于为每个线程保存独立的变量副本。其典型使用场景包括:

  • 数据库连接管理
  • 用户会话存储
  • 线程上下文变量

代码:

public class ThreadLocalExample {
    private static ThreadLocal<Integer> threadLocalValue = ThreadLocal.withInitial(() -> 0);

    public static void main(String[] args) {
        Runnable task = () -> {
            threadLocalValue.set(threadLocalValue.get() + 1);
            System.out.println(Thread.currentThread().getName() + ": " + threadLocalValue.get());
        };

        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);

        thread1.start();
        thread2.start();
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.

输出:

Thread-0: 1
Thread-1: 1
  • 1.
  • 2.
2.2 内存泄漏的原因

ThreadLocal 的潜在问题来自其实现机制。每个线程维护一个 ThreadLocalMap,键为 ThreadLocal 对象,值为具体存储的数据。如果 ThreadLocal 对象没有被及时清理,可能导致以下问题:

  1. 强引用泄漏ThreadLocalMap 使用的是弱引用,但其值为强引用,如果线程长时间存在,值对象无法被回收。
  2. 线程池问题:在线程池中,线程会被重复利用,导致旧数据可能长期存在。
2.3 导致内存泄漏的代码
public class ThreadLocalLeakExample {
    private static ThreadLocal<byte[]> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        Runnable task = () -> {
            threadLocal.set(new byte[1024 * 1024 * 100]); // 分配 100MB 内存
            System.out.println("Task complete");
        };

        Thread thread = new Thread(task);
        thread.start();
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

如果线程长时间不终止或线程池重复使用该线程,内存可能无法释放。

3. 避免 ThreadLocal 内存泄漏的最佳实践

3.1 使用 remove() 方法

显式调用 remove() 方法清理数据:

public class ThreadLocalBestPractice {
    private static ThreadLocal<Integer> threadLocalValue = ThreadLocal.withInitial(() -> 0);

    public static void main(String[] args) {
        try {
            threadLocalValue.set(42);
            System.out.println("Value: " + threadLocalValue.get());
        } finally {
            threadLocalValue.remove();
        }
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
3.2 避免存储大对象

尽量避免使用 ThreadLocal 存储大体积对象,如缓存或文件数据。

3.3 使用框架自带的工具

某些框架提供了更安全的上下文变量管理工具,例如 Spring 的 RequestContextHolder

总结

Java 的 wait()notify() 方法以及 ThreadLocal 的设计初衷是为开发者提供强大的多线程工具,但它们的使用需要小心。同步块是 wait()notify() 正常工作的基础,确保线程间通信的安全性;而 ThreadLocal 虽然简化了线程内数据管理,但不当的使用可能导致严重的内存泄漏。通过掌握这些机制和最佳实践,可以避免常见的陷阱,提高代码的健壮性和效率。