LockSupport学习与使用
😄生命不息,写作不止
🔥 继续踏上学习之路,学之分享笔记
👊 总有一天我也能像各位大佬一样
🏆 博客首页 @怒放吧德德 To记录领地
🌝分享学习心得,欢迎指正,大家一起学习成长!
转发请携带作者信息 @怒放吧德德 @一个有梦有戏的人
文章目录
- LockSupport学习与使用
- 引言
- LockSupport的基本概念
- 线程阻塞与唤醒机制
- wait与notify方法
- wait
- notify
- 案例
- await与signal实现
- await
- signal
- 案例
- park与unpark方法
- park
- unpark
- 案例
- 与其他线程控制机制的对比
- 结论
引言
在多线程编程中,线程间的协调是一个复杂而又至关重要的话题。Java提供了多种机制来实现线程间的同步和通信,以确保数据的一致性和线程的安全执行。在这些机制中,LockSupport类提供了一种高效且灵活的方式来控制线程的阻塞和唤醒。本文将深入探讨LockSupport类及其与Java中其他线程控制机制的对比,包括传统的wait和notify方法以及基于Condition的await和signal方法。我们将通过代码示例和分析,详细解释这些机制的工作原理和使用场景,以及它们之间的差异和最佳实践。
LockSupport的基本概念
LockSupport
是 Java 中的一个工具类,位于 java.util.concurrent.locks
包中。它提供了一些低级别的线程阻塞和唤醒功能,主要用于实现其他高层并发结构,如锁和信号量。LockSupport
通过使用许可(permits)来控制线程的状态,使得开发者能够在自定义的锁或线程管理中灵活使用。例如,LockSupport.park()
方法可以使当前线程阻塞,而 LockSupport.unpark(Thread thread)
则可以唤醒一个被阻塞的线程。这种机制比传统的 wait
和 notify
方法更高效且更易于实现。
简单来说他就是用于创建锁和其他同步类的基本线程阻塞原语。
线程阻塞与唤醒机制
接下来我们用三个方式来实现对线程的阻塞与唤醒。
wait与notify方法
wait()
和 notify()
方法是 Java 中用于线程间通信的重要机制,主要在对象监视器(也称为锁)中使用。这些方法允许线程在特定条件下等待和唤醒,从而实现更复杂的多线程交互。
首先我们通过API文档来了解这两个方法,这两个是Object类下的两个方法。
wait
public final void wait() throws InterruptedException
这个方法会导致当前线程等待,直到另一个线程调用此对象的notify()方法或notifyAll()方法。
当前线程必须拥有这个对象的监视器。线程释放该监视器的所有权,并等待,直到另一个线程通过调用notify方法或notifyAll方法通知等待该对象监视器的线程唤醒。然后,线程等待,直到它可以重新获得监视器的所有权并继续执行。
notify
public final void notify()
唤醒正在等待对象监视器的单个线程。 如果任何线程正在等待这个对象,其中一个被选择被唤醒。 选择是任意的,并且由实施的判断发生。 线程通过调用wait方法之一等待对象的监视器。
唤醒的线程将无法继续,直到当前线程放弃此对象上的锁定为止。 唤醒的线程将以通常的方式与任何其他线程竞争,这些线程可能正在积极地竞争在该对象上进行同步; 例如,唤醒的线程在下一个锁定该对象的线程中没有可靠的权限或缺点。
该方法只能由作为该对象的监视器的所有者的线程调用。
线程以三种方式之一成为对象监视器的所有者:
- 通过执行该对象的同步实例方法。
- 通过执行在对象上synchronized语句的正文。
- 对于类型为Class的对象,通过执行该类的同步静态方法。
案例
现在通过一个案例来学习,以下有两个线程,因为需要同一个监视器中执行,所以这里需要通过synchronized进行包裹。
public class LockWn {public static void main(String[] args) {// 同一把锁Object lockKey = new Object();new Thread(() -> {synchronized (lockKey) {System.out.println("线程" + Thread.currentThread().getName() + "\t进入...");try {// 阻塞lockKey.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("线程" + Thread.currentThread().getName() + "\t被唤醒...");}}, "T1").start();new Thread(() -> {// 线程T2释放synchronized (lockKey) {lockKey.notify();System.out.println("线程" + Thread.currentThread().getName() + "\t发起通知唤起...");}}, "T2").start();}
}
以上代码执行后就能够看到线程T1阻塞之后,线程T2发起通知去释放。
我们在以上代码加了synchronized,那么如果不加synchronized会怎样?
我们不妨自己动手试一下,将synchronized去掉试一下。
结果是会抛出java.lang.IllegalMonitorStateException
异常。这个API文档中,就说过了**只能由拥有此对象监视器的线程调用,如果当前线程不是对象监视器的所有者则会抛出IllegalMonitorStateException**
我们也可以看一下代码上的注释。
这个案例是先执行了wait再去执行notify,那么如果换一下顺序效果将会是怎样的呢?
public class LockWn2 {public static void main(String[] args) {// 同一把锁Object lockKey = new Object();new Thread(() -> {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (lockKey) {System.out.println("线程" + Thread.currentThread().getName() + "\t进入...");try {// 阻塞lockKey.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("线程" + Thread.currentThread().getName() + "\t被唤醒...");}}, "T1").start();new Thread(() -> {// 线程T2释放synchronized (lockKey) {lockKey.notify();System.out.println("线程" + Thread.currentThread().getName() + "\t发起通知唤起...");}}, "T2").start();}
}
我们通过这个案例,在T1获取锁执行wait之前调用休眠1s,这是为了保证T2线程先执行。也就是限制性了notify,再去执行wait。这时候我们看一下输出结果。
我们可以看到,程序已经执行了T2中的notify,之后也执行了wait方法,但是最后结果确实程序还在消耗,T1依旧被阻塞住,并没有释放。这显然我们可以得出,将notify卸载wait之前,是不会导致被唤起的。
如果我们在LockWn2#main代码后面加上以下代码
// 如果在尾部加上T3来执行notify
try {Thread.sleep(3000);
} catch (InterruptedException e) {throw new RuntimeException(e);
}
new Thread(() -> {// 线程T3释放synchronized (lockKey) {lockKey.notify();System.out.println("线程" + Thread.currentThread().getName() + "\t发起通知唤起...");}
}, "T3").start();
最终T1是能够被唤醒的。
注:wait和notify都要放在synchronized代码块中,wait要先执行才执行notify才会有效果。
await与signal实现
第二种实现方式采用Condition接口中的await和signal方法实现线程的等待唤醒。
Condition接口是Java并发包java.util.concurrent.locks中的一部分,它为线程同步提供了更灵活的机制,类似于传统的Object类中的wait()、notify()和notifyAll()方法。Condition结合Lock接口使用,以实现复杂的线程间协调,主要依赖await()和signal()方法。
await
await()
的作用类似于Object.wait()
,当一个线程调用await()
时:
- 该线程会被挂起(阻塞),并且释放当前持有的锁。
- 线程进入等待队列,直到被其他线程通过
signal()
或signalAll()
唤醒,或者被中断。
InterruptedException - 如果当前线程被中断(并且支持线程中断的中断)
signal
signal()
的作用类似于Object.notify()
,它用来唤醒等待该Condition
的某个线程:
- 只能唤醒一个等待的线程。
- 被唤醒的线程仍需要重新获取锁,才能从
await()
后继续执行。
当调用此方法时,实现可能(通常是)要求当前线程保存与此Condition的锁。 执行必须记录此前提条件,如果不保持锁定,则采取任何措施。 通常情况下,一个异常如IllegalMonitorStateException将被抛出。
案例
我们还是一样通过案例来认识一下await与signal,我们是首先需要一把锁Lock lock = new ReentrantLock();
,然后整体与Object.wait与Object.notify是差不多的。
public class LockAs {public static void main(String[] args) throws InterruptedException {Lock lock = new ReentrantLock();Condition condition = lock.newCondition();new Thread(() -> {lock.lock();System.out.println("线程" + Thread.currentThread().getName() + "\t进入...");try {condition.await();} catch (InterruptedException e) {throw new RuntimeException(e);} finally {lock.unlock();}System.out.println("线程" + Thread.currentThread().getName() + "\t被唤醒...");}, "T1").start();Thread.sleep(1000);new Thread(() -> {lock.lock();try {condition.signal();} finally {lock.unlock();}System.out.println("线程" + Thread.currentThread().getName() + "\t发起通知唤起...");}, "T2").start();}
}
从结果可以看出与Object.wait与Object.notify的结果是一样的。
我们还是老办法,先将锁给注释掉,结果如下
会抛出IllegalMonitorStateException异常,因为这两个方法都是要求当前线程保存与此Condition的锁。
接下来,我们还是把await与signal倒序执行。
public class LockAs2 {public static void main(String[] args) throws InterruptedException {Lock lock = new ReentrantLock();Condition condition = lock.newCondition();new Thread(() -> {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}lock.lock();System.out.println("线程" + Thread.currentThread().getName() + "\t进入...");try {condition.await();} catch (InterruptedException e) {throw new RuntimeException(e);} finally {lock.unlock();}System.out.println("线程" + Thread.currentThread().getName() + "\t被唤醒...");}, "T1").start();new Thread(() -> {lock.lock();try {condition.signal();} finally {lock.unlock();}System.out.println("线程" + Thread.currentThread().getName() + "\t发起通知唤起...");}, "T2").start();}
}
结果还是和wait与notify方法是一样,会出现T2通知唤起T1,但是T1却一直没有唤醒,线程一直阻塞状态。
park与unpark方法
接下来就到使用park与unpark两个方法。首先,我们要先了解一下LockSupport,他是用于创建锁和其他同步类的基本线程阻塞原语。 这个类与每个使用它的线程相关联,一个许可证(在Semaphore类的意义上)。 如果许可证可用,则呼叫park将park返回,在此过程中消耗它; 否则可能会阻止。 如果没提供许可证,则致电unpark获得许可。 (*与信号量不同,许可证不能累积,最多只有一个)
方法park和unpark提供了阻止和解除阻塞线程的有效手段,该方法不会遇到导致不推荐使用的方法Thread.suspend和Thread.resume目的不能使用的问题:一个线程调用park和另一个线程之间的尝试unpark线程将保持活跃性,由于许可证。 另外,如果调用者的线程被中断, park将返回,并且支持超时版本。 park方法也可以在任何其他时间返回,因为“无理由”,因此一般必须在返回之前重新检查条件的循环中被调用。 在这个意义上, park作为一个“忙碌等待”的优化,不浪费时间旋转,但必须与unpark配对才能有效。
park
park()
方法用于阻塞当前线程,禁用当前线程进行线程调度,直到指定的等待时间,除非许可证可用。也就是直到被其他线程调用unpark()
方法唤醒。
使用park()
时,线程将被放入一个等待状态,释放 CPU,直到以下条件之一发生:
- 当前线程被调用
unpark()
唤醒。 - 当前线程被中断。
- 当前线程由于其他原因被唤醒(例如 JVM 的实现细节)。
通过代码可以看到,实际上是使用了UNSAFE的方法。
public static void park() {UNSAFE.park(false, 0L);
}
unpark
unpark(Thread thread)
方法用于唤醒指定的线程。如果该线程正在等待(被调用了park()
),则unpark()
将使其继续执行。如果该线程没有被阻塞,unpark()
会将其标记为可以运行,确保线程在下次调度时能够执行。(许可证不会累积!)
public static void unpark(Thread thread) {if (thread != null)UNSAFE.unpark(thread);
}
案例
使用LockSupport.park();先将当前线程阻塞,再通过另一个线程执行LockSupport.unpark(t1);来将提供许可证,是的t1线程能够继续使用。我们从以下代码可以看出,使用LockSupport是不用再使用同步锁的。
public class LockPark {public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {System.out.println("线程" + Thread.currentThread().getName() + "\t进入...");LockSupport.park();System.out.println("线程" + Thread.currentThread().getName() + "\t被唤醒...");}, "t1");t1.start();Thread.sleep(1000);Thread t2 = new Thread(() -> {LockSupport.unpark(t1);System.out.println("线程" + Thread.currentThread().getName() + "\t发起通知唤起...");}, "t2");t2.start();}
}
从结果可以看出,线程的阻塞与唤醒能够正常达到目的。
接下来我们还是与上文一样,做了一些反例。我们将unpark与park顺序做一个对换。
public class LockPark2 {public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {try {Thread.sleep(3000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("线程" + Thread.currentThread().getName() + "\t进入...");LockSupport.park();System.out.println("线程" + Thread.currentThread().getName() + "\t被唤醒...");}, "t1");t1.start();Thread t2 = new Thread(() -> {LockSupport.unpark(t1);System.out.println("线程" + Thread.currentThread().getName() + "\t发起通知唤起...");}, "t2");t2.start();}
}
通过运行结果我们可以得知,其效果是一致的。
但是,如果我们多次park呢?假如在一个线程中,我们park了两次,并且执行了多次unpark,那会是怎么样的结果呢?
public class LockPark2 {public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {try {Thread.sleep(3000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("线程" + Thread.currentThread().getName() + "\t进入...");LockSupport.park();System.out.println("线程" + Thread.currentThread().getName() + "\t被唤醒1...");LockSupport.park();System.out.println("线程" + Thread.currentThread().getName() + "\t被唤醒2...");}, "t1");t1.start();Thread t2 = new Thread(() -> {LockSupport.unpark(t1);LockSupport.unpark(t1);System.out.println("线程" + Thread.currentThread().getName() + "\t发起通知唤起...");}, "t2");t2.start();Thread t3 = new Thread(() -> {LockSupport.unpark(t1);System.out.println("线程" + Thread.currentThread().getName() + "\t发起通知唤起...");}, "t3");t3.start();}
}
如上面代码,我们是先执行了unpark,先颁布许可证,我们在t2线程执行了2次unpark,在t3线程中执行了1次,我们运行一下。
转发请携带作者信息 @怒放吧德德 @一个有梦有戏的人
我们可以看到,第一个park解开了,第二个并没有,也就是说明了许可证只会有一个,并不会进行积累。
与其他线程控制机制的对比
这样我们可以得到以下的区别
wait与notify方法
- 需要锁
- 顺序不能颠倒(不能先唤醒后等待)
await与signal实现
- 需要锁
- 顺序不能颠倒(不能先唤醒后等待)
park与unpark方法
- 不需要锁
- 也不一定需要顺序执行(先唤醒后等待也能正常执行)
- 许可证不会累计,最多只有一个
那么为什么可以调到顺序呢?
因为unpark是提供了一个许可证,等调用park的时候得到了凭证,就直接释放了,就不会阻塞。
为什么唤醒两次在阻塞两次却不会唤醒呢?
因为许可证最多只能有一个,连续调用两次的许可证是不会进行累计。
转发请携带作者信息 @怒放吧德德 @一个有梦有戏的人
结论
通过对LockSupport类及其提供的park和unpark方法的深入分析,我们可以看到,这些方法为线程控制提供了一种无需锁的阻塞和唤醒机制。与wait和notify方法以及await和signal方法相比,park和unpark方法具有更高的灵活性和效率。它们不需要依赖于锁对象,也不受顺序执行的限制,使得它们在某些场景下更为适合。然而,需要注意的是,park和unpark方法中的许可证不会累积,这意味着即使多次调用unpark,也只有一个许可证可用,这与其他机制的行为有所不同。
总的来说,选择合适的线程控制机制需要根据具体的应用场景和需求来决定。LockSupport类提供了一种强大的工具,可以帮助开发者实现更高效和灵活的线程同步,但同时也要求开发者对线程的生命周期和状态有更深入的理解。通过本文的学习和实践,希望读者能够更好地掌握这些线程控制机制,并在实际开发中做出恰当的选择。
转发请携带作者信息 @怒放吧德德 @一个有梦有戏的人
持续创作很不容易,作者将以尽可能的详细把所学知识分享各位开发者,一起进步一起学习。
👍创作不易,如有错误请指正,感谢观看!记得点赞哦!👍
谢谢支持!