LockSupport 详解
LockSupport 详解
LockSupport
是 Java 中提供的一组用于线程同步和线程控制的工具类。它属于 java.util.concurrent
包,旨在==提供一种更加高效且低级的线程挂起和恢复的机制==。LockSupport
通过实现“阻塞 - 唤醒”机制来实现线程的挂起与恢复,常用于实现自定义的并发控制器(如锁(Lock)、信号量(Semaphore)、条件变量(Condition)等)。
LockSupport
本身提供了比传统的 wait()
/notify()
更加灵活和高效的方式来控制线程挂起与唤醒,避免了 Object
的监视器锁(monitor)机制的性能瓶颋。
当我们想自己灵活控制线程的挂起和继续执行的时候,我们也可以使用 LockSupport
来实现,非常方便。
LockSupport
的主要方法
LockSupport.park()
park()
方法使当前线程挂起,直到被唤醒。线程会进入等待状态,并且被阻塞,直到满足以下条件之一:
- 线程被其他线程显式唤醒(通过
unpark()
)。 - 线程在调用
park()
时被中断。
public static void park();
功能:
- 使当前线程挂起,直到被唤醒或者发生中断。
- 如果调用线程被中断,
park()
会抛出InterruptedException
。 - 如果调用线程没有中断,它会在等待队列中等待。
LockSupport.parkNanos(long nanos)
parkNanos(long nanos)
方法使当前线程挂起指定时间(纳秒为单位)。如果线程没有被唤醒且时间到期,它将自动恢复执行。可以有效防止无人唤醒造成死锁
public static void parkNanos(long nanos);
我们可以通过 TimeUnit 的 toNanos 方法,轻松将其他单位的时间转化为纳秒,比如 TimeUnit.SECONDS.toNanos(2)
功能:
- 使线程挂起指定的纳秒数。
- 如果被中断,线程会立即返回。
- 如果超时,线程会自动恢复。
LockSupport.parkUntil(long deadline)
parkUntil(long deadline)
方法使当前线程挂起,直到指定的时间戳(绝对时间)。这个方法会挂起线程直到 deadline
到期为止。用的少,一般都是用 LockSupport.parkNanos(long nanos)
指定挂起的相对时间。
public static void parkUntil(long deadline);
功能:
- 线程挂起,直到当前时间达到给定的
deadline
时间戳。 - 和
parkNanos()
不同的是,parkUntil()
使用的是绝对时间,而非相对时间。
LockSupport.unpark(Thread thread)
unpark(Thread thread)
方法唤醒指定的线程。被唤醒的线程将会继续执行,前提是该线程之前调用了 park()
并挂起了。如果该线程之前没有被挂起,它会直接返回。
public static void unpark(Thread thread);
功能:
- 唤醒指定的线程,使其从挂起状态恢复。
- 如果该线程没有被挂起,则调用
unpark()
不会有任何效果。 - 可以多次调用
unpark()
来唤醒同一线程,但只会让该线程恢复一次。
LockSupport
的工作原理
LockSupport
类实现线程挂起和唤醒的方式其实就是调用 Unsafe
的 park
和 unpark
方法,而 Unsafe
的 park
和 unpark
方法底层依赖于操作系统的的指令(如 futex
、mutex
等)。当线程调用 park()
时,它会被挂起并进入内核的阻塞队列。线程将会保持挂起状态,直到满足以下条件之一:
- 被其他线程通过
unpark()
唤醒。 - 被中断(如果调用
park()
时发生中断)。 - 超过指定的等待时间或达到指定的时间戳。
挂起与唤醒的机制
- 挂起:
park()
方法让当前线程挂起,直到它被唤醒。调用时,线程会请求操作系统将其阻塞,而无需持有显式的锁或监视器。 - 唤醒:
unpark()
会唤醒指定线程,使其从阻塞状态中恢复。
使用场景
LockSupport
主要用于需要低级控制线程挂起与恢复的场景,它比传统的 Object.wait()
和 Object.notify()
更为灵活,特别是在高并发和性能敏感的应用中。
一些常见的使用场景包括:
实现线程池
LockSupport
常用于实现线程池的工作机制,尤其是在等待任务执行和空闲线程之间的调度。
// 示例:通过 LockSupport 实现线程的挂起与唤醒
Thread workerThread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
LockSupport.park();
// 执行任务...
}
});
workerThread.start();
// 需要唤醒工作线程
LockSupport.unpark(workerThread);
自定义同步器
开发自定义的锁、信号量、条件变量等同步器时,LockSupport
提供了更低级和灵活的线程控制能力。例如,基于 LockSupport
可以构建一个类似 CountDownLatch
或 CyclicBarrier
的同步工具。
优化性能
由于 LockSupport
不依赖传统的基于对象监视器(monitor)的同步机制(即不使用 synchronized
和 wait/notify
),它避免了在高并发情况下的性能瓶颈。
非阻塞等待
LockSupport.parkNanos()
提供了一种非阻塞的等待机制,允许线程在等待期间进行一定时间的自旋,这对于实现高效的并发控制尤为重要。
与 wait()
/notify()
的比较
与 Object.wait()
和 Object.notify()
不同,LockSupport
的方法无需依赖于监视器锁。wait()
和 notify()
需要在 synchronized
代码块内调用,并且会持有对象锁,可能导致死锁和性能问题。而 LockSupport
是基于更底层的操作系统同步指令(如 futex
),避免了对象锁的开销。
特性 | LockSupport |
Object.wait() / notify() |
---|---|---|
依赖于监视器锁 | 否 | 是(synchronized ) |
能否中断线程 | 可以(通过中断) | 可以(通过 Thread.interrupted() ) |
唤醒方式 | 通过 unpark() |
通过 notify() 或 notifyAll() |
性能 | 更高效,依赖于操作系统的指令 | 可能有较大性能开销(上下文切换) |
使用场景 | 高效的线程挂起、唤醒与线程池实现 | 一般的线程同步和通信 |
示例代码
同时执行三个线程,然后在这三个线程内部将自己挂起,然后在主线程(main)中将这三个线程唤醒,三个县城继续往后执行
public static void main(String[] args) {
List<Thread> threads = new ArrayList<>();
int rounds = 10;
for (int i = 0; i < 3; i++) {
int finalI = i;
Thread thread = new Thread(() -> {
int nowCount = 0;
String threadName = "Thread " + finalI;
System.out.println(LocalDateTime.now() + "-" + threadName + " started");
while (nowCount < rounds) {
if (nowCount == 3) {
// 需要线程自己调用方法主动挂起自己
LockSupport.park();
System.out.println(LocalDateTime.now() + "-" + threadName + " 线程恢复");
}
try {
Thread.sleep(200);
} catch (InterruptedException e) {
// throw new RuntimeException(e);
}
System.out.println(LocalDateTime.now() + "-" + threadName + " now Round is:" + nowCount);
nowCount++;
}
});
threads.add(thread);
thread.start();
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
// throw new RuntimeException(e);
}
System.out.println("重新启动所有线程");
for (Thread thread : threads) {
// 从另一个线程来调用方法来唤醒那些自己挂起自己的线程
// 你自己挂起你自己 + 等待别人来唤醒你 这样的设计限制了挂起线程的操作的发起方,即自己线程自己才可以挂起自己,别人不行,其实简化了多线程编程的心智负担,
// 因为如果别的线程可以挂起别的线程,A 挂起了 B,同时 B 也挂起了 A,好家伙,相互挂起,死锁将更加普遍,
// 限制只有自己才能挂起自己,产生死锁的原因就变成了没有人来唤醒自己,
LockSupport.unpark(thread);
}
}
执行输出
2025-01-19T15:58:28.867800300-Thread 1 started
2025-01-19T15:58:28.867800300-Thread 2 started
2025-01-19T15:58:28.867800300-Thread 0 started
2025-01-19T15:58:29.081427500-Thread 0 now Round is:0
2025-01-19T15:58:29.081427500-Thread 1 now Round is:0
2025-01-19T15:58:29.081427500-Thread 2 now Round is:0
2025-01-19T15:58:29.303496800-Thread 2 now Round is:1
2025-01-19T15:58:29.303496800-Thread 1 now Round is:1
2025-01-19T15:58:29.303496800-Thread 0 now Round is:1
2025-01-19T15:58:29.509052200-Thread 2 now Round is:2
2025-01-19T15:58:29.509052200-Thread 0 now Round is:2
2025-01-19T15:58:29.509052200-Thread 1 now Round is:2
重新启动所有线程
2025-01-19T15:58:31.866113900-Thread 1 线程恢复
2025-01-19T15:58:31.866113900-Thread 2 线程恢复
2025-01-19T15:58:31.866113900-Thread 0 线程恢复
2025-01-19T15:58:32.073400200-Thread 1 now Round is:3
2025-01-19T15:58:32.073400200-Thread 0 now Round is:3
2025-01-19T15:58:32.073400200-Thread 2 now Round is:3
2025-01-19T15:58:32.277323700-Thread 2 now Round is:4
2025-01-19T15:58:32.277323700-Thread 0 now Round is:4
2025-01-19T15:58:32.277323700-Thread 1 now Round is:4
2025-01-19T15:58:32.482627900-Thread 1 now Round is:5
2025-01-19T15:58:32.483627100-Thread 0 now Round is:5
2025-01-19T15:58:32.483627100-Thread 2 now Round is:5
2025-01-19T15:58:32.685546400-Thread 1 now Round is:6
2025-01-19T15:58:32.685546400-Thread 0 now Round is:6
2025-01-19T15:58:32.686548100-Thread 2 now Round is:6
2025-01-19T15:58:32.890007700-Thread 1 now Round is:7
2025-01-19T15:58:32.890007700-Thread 0 now Round is:7
2025-01-19T15:58:32.890007700-Thread 2 now Round is:7
2025-01-19T15:58:33.096232200-Thread 2 now Round is:8
2025-01-19T15:58:33.096232200-Thread 0 now Round is:8
2025-01-19T15:58:33.096232200-Thread 1 now Round is:8
2025-01-19T15:58:33.300468100-Thread 1 now Round is:9
2025-01-19T15:58:33.300468100-Thread 0 now Round is:9
2025-01-19T15:58:33.300468100-Thread 2 now Round is:9
这里我们多说两句 Java 中的多线程 API 设计范式,其为:线程自己挂起自己 + 等待别人来唤醒它,
Object
的wait()
和notify()
(需要在synchronized
代码块内调用)Condition
的await()
和signal()
(需要当前线程执行Lock.lock()
发给发获取锁之后使用)- 还有
LockSupport
的park
和unpark
方法(不需要获取锁)
他们挂起线程和让线程继续执行的 API 都是这样设计的。
这样的设计限制了挂起线程的操作的发起方,即只有自己线程自己才可以挂起自己,别人不行,其实简化了多线程编程的心智负担,因为如果别的线程可以挂起别的线程,比如线程 A 挂起了 线程 B,同时 线程 B 也挂起了 线程 A,好家伙,相互挂起,死锁将更加普遍,限制只有自己才能挂起自己,产生死锁的原因就变成了没有人来唤醒自己。
然后上面三组 API,其挂起线程和让线程继续执行的效果都是一样的:执行完挂起自身那一行代码之后(比如LockSupport.park
),后续代码的不再执行,然后等其他线程取消此线程的挂起状态的时候(LockSupport.unpark
),此线程继续执行后续代码,在上面的例子中就是System.out.println(LocalDateTime.now() + "-" + threadName + " 线程恢复");
就好像执行完LockSupport.park
之后,线程睡了一觉,被人叫醒之后继续往后执行。
我们前面说过限制只有自己才能挂起自己,产生死锁的原因就变成了没有人来唤醒自己
。其实通过LockSupport.parkNanos(long nanos)
,没有人唤醒我们,我们也可以自己唤醒自己。
public static void main(String[] args) {
List<Thread> threads = new ArrayList<>();
int rounds = 10;
for (int i = 0; i < 1; i++) {
int finalI = i;
Thread thread = new Thread(() -> {
int nowCount = 0;
String threadName = "Thread " + finalI;
System.out.println(LocalDateTime.now() + "-" + threadName + " started");
while (nowCount < rounds) {
if (nowCount == 3) {
// 需要线程自己调用方法主动挂起自己
LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(2));
System.out.println(LocalDateTime.now() + "-" + threadName + " 线程恢复");
}
try {
Thread.sleep(200);
} catch (InterruptedException e) {
// throw new RuntimeException(e);
}
System.out.println(LocalDateTime.now() + "-" + threadName + " now Round is:" + nowCount);
nowCount++;
}
});
threads.add(thread);
thread.start();
}
}
执行输出
2025-01-19T16:07:11.445919900-Thread 0 started
2025-01-19T16:07:11.669957100-Thread 0 now Round is:0
2025-01-19T16:07:11.886799300-Thread 0 now Round is:1
2025-01-19T16:07:12.092065-Thread 0 now Round is:2
2025-01-19T16:07:14.099796300-Thread 0 线程恢复
2025-01-19T16:07:14.318365200-Thread 0 now Round is:3
2025-01-19T16:07:14.523887600-Thread 0 now Round is:4
2025-01-19T16:07:14.728722300-Thread 0 now Round is:5
2025-01-19T16:07:14.933144-Thread 0 now Round is:6
2025-01-19T16:07:15.139189-Thread 0 now Round is:7
2025-01-19T16:07:15.342590700-Thread 0 now Round is:8
2025-01-19T16:07:15.546135100-Thread 0 now Round is:9
其实 Object
和 Condition
也有类似的方法,比如 Object
的 wait(long timeout)
和 Condition
的 await(long time, TimeUnit unit)
方法。
总结
LockSupport
是一个灵活、轻量级的线程挂起和恢复工具,它不依赖于 synchronized
或 wait/notify
,可以在不占用监视器锁的情况下高效地进行线程管理。它适用于那些需要精细控制线程状态的场景,特别是在高并发的并发程序设计中。