深入理解Java并发编程中的LockSupport
1. 简介
LockSupport类是Java并发包中的一个工具类,用于线程的阻塞和唤醒操作。它提供了一种灵活且强大的线程同步工具,使得线程之间的协作更加简单和高效。
LockSupport类的作用和重要性体现在以下几个方面:
- 线程阻塞与唤醒:LockSupport类提供了park()和unpark(Thread thread)两个静态方法,可以实现线程的阻塞和唤醒。通过park()方法,线程可以暂停自己的执行,等待某个条件的发生;而通过unpark(Thread thread)方法,可以唤醒被park()阻塞的线程,使其恢复执行。
- 替代传统的synchronized和wait/notify机制:相较于传统的同步机制(如synchronized关键字、wait()和notify()方法),LockSupport类更加灵活和安全。它不依赖于对象的监视器,避免了因为监视器嵌套而引起的死锁问题,并且提供了更精细的线程控制能力。
- 支持线程中断:与Object类的wait()方法不同,park()方法不会抛出InterruptedException异常。因此,在线程被阻塞的情况下,即使其他线程调用了interrupt()方法中断了该线程,也不会抛出异常,保持了线程的阻塞状态,这种特性使得LockSupport更适合在并发环境下使用。
2. LockSupport的基本概念
-
park()方法:
- park()方法使当前线程进入等待状态,直到被唤醒或者中断。
- 如果调用park()时,当前线程的许可证(permit)是可用的,则会立即消费该许可证并继续执行;否则,线程将被阻塞,直到许可证可用或者线程被中断。
-
unpark(Thread thread)方法:
- unpark()方法用于唤醒被park()方法阻塞的线程。
- 如果给定的线程之前已经被调用过unpark()方法,那么该线程的许可证仍然有效;否则,该线程将在调用unpark()方法后被唤醒。
基础使用
@Test @DisplayName("测试LockSupport") public void test1() throws InterruptedException { Thread thread = new Thread(() -> { logger.info("park......."); // 阻塞当前线程 // LockSupport.park(); // 阻塞当前线程,直到调用LockSupport.unpark(thread)方法 LockSupport.park(); logger.info("unpark....."); logger.info("打断状态...{}", Thread.currentThread().isInterrupted()); }, "t1"); thread.start(); Thread.sleep(1000); // 打断线程 并重置打断标记 thread.interrupt(); }
3. park()方法的原理和实现
3.1. park()方法的实现原理
- 许可证机制:
- 每个线程都有一个关联的许可证(permit),park()方法的实现依赖于这个许可证的状态。
- 如果一个线程的许可证可用,调用park()方法时线程会立即消费这个许可证并继续执行。
- 如果许可证不可用,线程将被阻塞,等待许可证的释放或者被中断。
- 线程状态切换:
- 当线程调用park()方法时,它会检查自己的许可证状态。
- 如果许可证可用,park()方法会立即返回;否则,线程将被阻塞。
- 被阻塞的线程会进入WAITING状态,暂停执行,并等待许可证的可用性或者被其他线程唤醒。
- 底层实现机制:
- LockSupport类使用了操作系统提供的底层同步机制来实现线程的阻塞和唤醒。
- 在大多数操作系统中,包括Linux、Windows等,底层的线程阻塞和唤醒机制通常依赖于操作系统的内核调度器。
- 当一个线程调用park()方法被阻塞时,实际上是将自己挂起,并告知操作系统不再调度它,直到条件满足或者被唤醒。
- 与操作系统的交互:
- 调用park()方法时,LockSupport类会与操作系统进行交互,通过操作系统提供的系统调用来实现线程的阻塞。
- 操作系统会将被阻塞的线程从可执行状态转换为阻塞状态,并将其放入等待队列中,直到被条件满足或者被唤醒。
- 许可证的释放:
- 当线程被唤醒或者条件满足时,许可证会被释放,并且调用park()方法的线程将恢复执行。
总的来说,LockSupport类中park()方法的实现原理涉及到JVM底层和操作系统的交互,它使用操作系统提供的底层同步机制来实现线程的阻塞和唤醒。
4. unpark()方法的原理和实现
4.1. unpark()方法的实现原理
- 许可证状态的修改:
- 当调用unpark(Thread thread)方法时,会修改指定线程的许可证状态。
- 如果指定线程之前未被调用过unpark()方法,那么调用unpark()方法将使得该线程的许可证状态变为可用(即未被消费的状态)。
- 如果指定线程之前已经被调用过unpark()方法,那么再次调用unpark()方法不会改变其许可证状态。
- 许可证的释放:
- 调用unpark(Thread thread)方法将释放指定线程的许可证。
- 如果线程的许可证状态是可用的,那么调用unpark()方法不会产生任何作用。
- 如果线程的许可证状态是不可用的,那么调用unpark()方法会使得线程的许可证状态变为可用。
- 线程的唤醒:
- 当调用unpark(Thread thread)方法后,被阻塞的线程将从阻塞状态转换为可运行状态。
- 如果线程当前处于阻塞状态(WAITING状态),那么调用unpark()方法会唤醒线程,使其恢复执行。
- 如果线程在调用park()方法之前就被中断,那么调用unpark()方法会清除中断状态,并且不会唤醒线程。
- 唤醒顺序:
- 如果多次调用unpark()方法,被阻塞线程只会被唤醒一次。
- 即使调用了多次unpark()方法,线程只需要一个许可证就能被唤醒。
总的来说,unpark()方法通过修改线程的许可证状态,使得被阻塞的线程从阻塞状态转换为可运行状态,从而实现线程的唤醒。unpark()方法的调用会影响线程的许可证状态,并且能够唤醒处于阻塞状态的线程,使其恢复执行。
5. LockSupport与其他同步机制的比较
- 与synchronized关键字的比较:
- LockSupport是一种更加灵活和低级别的线程同步工具,而synchronized关键字是Java语言提供的高级别的同步机制。
- LockSupport不依赖于对象的监视器,因此避免了由于监视器嵌套而可能引发的死锁问题。
- 使用LockSupport可以更加精细地控制线程的阻塞和唤醒,而synchronized关键字是基于对象的监视器实现的,具有更高的语义性但也更加受限。
- 与wait()和notify()方法的比较:
- wait()和notify()方法是Object类提供的同步机制,用于实现线程之间的通信和同步。
- LockSupport提供的park()和unpark()方法相比之下更加灵活和安全,不依赖于特定的对象,避免了由于等待和通知方法的调用顺序不当而导致的程序死锁或假死问题。
- 支持线程中断的处理:
- LockSupport类的park()方法不会抛出InterruptedException异常,但是会响应Thread.interrupt()方法,线程被中断后不会抛出异常,但是会从阻塞状态中恢复,因此可以更加灵活地处理线程中断。
- wait()方法会在线程等待过程中被中断会抛出InterruptedException异常。
- 精细控制线程阻塞和唤醒:
- LockSupport提供了一种更加灵活和精细的线程阻塞和唤醒机制,可以在任意位置调用park()和unpark()方法实现线程的阻塞和唤醒。
- wait()和notify()方法需要在synchronized块或者synchronized方法中使用,且只能在同步的代码块内部调用。
6. 应用场景与示例代码
6.1. 自定义同步器(简单的累加)
实现一个简单的同步器功能,通过 LockSupport 实现了线程的阻塞和唤醒。
public class MySync {
private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private volatile int count = 0; // 共享资源
private Thread mainThread; // 主线程
public MySync() {
mainThread = Thread.currentThread();
}
// 同步方法,增加count
public void add() {
while (!Thread.currentThread().isInterrupted()) {
synchronized (this) {
count++;
logger.error(Thread.currentThread().getName() + " add, count = " + count);
}
// 阻塞当前线程
LockSupport.park();
}
}
// 唤醒线程
public void unpark(Thread thread) {
LockSupport.unpark(thread); // 唤醒主线程
}
@Test
@DisplayName("基于LockSupport实现同步器功能,并且实现线程的阻塞和唤醒")
public void test() {
MySync sync = new MySync();
// 创建并启动子线程
Thread thread1 = new Thread(sync::add, "Thread-1");
Thread thread2 = new Thread(sync::add, "Thread-2");
Thread thread3 = new Thread(sync::add, "Thread-3");
thread1.start();
thread2.start();
thread3.start();
try {
// 主线程睡眠一段时间后唤醒子线程
Thread.sleep(2000);
sync.unpark(thread1); // 唤醒子线程
// 主线程睡眠一段时间后再次唤醒子线程
Thread.sleep(2000);
sync.unpark(thread2); // 再次唤醒子线程
// 主线程睡眠一段时间后再次唤醒子线程
Thread.sleep(2000);
sync.unpark(thread3); // 再次唤醒子线程
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
总结
这段代码展示了如何利用 LockSupport 实现线程的阻塞和唤醒,实现了简单的同步器功能。
- add() 方法:这是一个同步方法,每次执行时会增加共享资源 count 的值,并记录日志。在增加完 count 后,调用 LockSupport.park() 阻塞当前线程。
- unpark() 方法:用于唤醒线程,通过 LockSupport.unpark() 方法唤醒指定的线程。
- test() 方法:测试方法创建了三个子线程,分别执行 add() 方法。在主线程中,通过调用 unpark() 方法依次唤醒了三个子线程,使其执行相应的操作。
7. 注意事项
在使用 LockSupport 时,需要注意以下问题和注意事项
- 不要调用 unpark() 多次:每个线程最多只能被 unpark() 唤醒一次,多次调用 unpark() 只会生效一次,因此需要谨慎使用,以避免不必要的混乱。
- 避免线程挂起和唤醒顺序问题:由于 unpark() 可能在 park() 之前调用,因此需要确保线程在 park() 之前被 unpark(),以免导致线程永远挂起的问题。
- 避免线程中断导致的异常:在使用 park() 阻塞线程时,要注意处理线程中断的情况,否则线程被中断时可能会出现异常情况。
- 不要阻塞主线程:LockSupport 主要用于线程间的同步,不建议在主线程中使用 park() 阻塞主线程,否则可能导致程序无法正常运行。
- 使用 LockSupport 前先考虑其他同步机制:LockSupport 是一种底层的同步工具,使用它需要谨慎考虑场景和需求,有时候其他更高级的同步机制如 synchronized、ReentrantLock 等可能更适合解决问题。
- 注意避免死锁:由于 LockSupport 没有内置的锁释放机制,如果不小心在程序中引入了死锁的情况,可能会导致程序无法继续执行。