JUC从实战到源码:LockSupport

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) 则可以唤醒一个被阻塞的线程。这种机制比传统的 waitnotify 方法更高效且更易于实现。

简单来说他就是用于创建和其他同步类的基本线程阻塞原语。

线程阻塞与唤醒机制

接下来我们用三个方式来实现对线程的阻塞与唤醒。

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类提供了一种强大的工具,可以帮助开发者实现更高效和灵活的线程同步,但同时也要求开发者对线程的生命周期和状态有更深入的理解。通过本文的学习和实践,希望读者能够更好地掌握这些线程控制机制,并在实际开发中做出恰当的选择。


转发请携带作者信息 @怒放吧德德 @一个有梦有戏的人
持续创作很不容易,作者将以尽可能的详细把所学知识分享各位开发者,一起进步一起学习。
👍创作不易,如有错误请指正,感谢观看!记得点赞哦!👍
谢谢支持!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/2431.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

Linux之信号量

前言 IPC中介绍过信号量, 为了让进程间通信, 从而多个执行流看到同一份公共资源, 对于并发访问造成数据不一致问题, 就需要把公共资源保护起来, 从而就需要同步与互斥. 信号量共有三个特性: 1. 本质是一把用于描述临界资源资源的数目的计数器 2. 每一个执行流想访问公共资源内…

eval长度限制绕过

我把他的叙述写成代码&#xff0c;大概如下&#xff1a; <?php $param $_REQUEST[param]; if(strlen($param)<17 && stripos($param,eval) false && stripos($param,assert) false) {eval($param); } ?> 那么这个代码怎么拿到webshell&#xf…

Linux - 进程间通信(管道)

文章目录 一、进程间通信的目的二、进程间通信的本质三、管道1、介绍2、匿名管道3、命名管道 一、进程间通信的目的 数据传输&#xff1a;一个进程需要将它的数据发送给另一个进程资源共享&#xff1a;多个进程之间共享同样的资源。通知事件&#xff1a;一个进程需要向另一个或…

【软考】反规范化技术

论反规范化技术 反规范化有这几种技术&#xff0c;增加冗余列&#xff0c;增加派生列&#xff0c;重组表和分割表。其中冗余列是指同一个字段在另外的表中存储一份&#xff0c;减少连表操作。增加派生列是基于另外一个列或者多个列&#xff0c;计算得到一个新的列&#xff0c;可…

SpringBoot day 1104

ok了家人们这周学习SpringBoot的使用&#xff0c;和深入了解&#xff0c;letgo 一.SpringBoot简介 1.1 设计初衷 目前我们开发的过程当中&#xff0c;一般采用一个单体应用的开发采用 SSM 等框架进行开发&#xff0c;并在 开发的过程当中使用了大量的 xml 等配置文件&#x…

Python | Leetcode Python题解之第528题按权重随机选择

题目&#xff1a; 题解&#xff1a; class Solution:def __init__(self, w: List[int]):self.pre list(accumulate(w))self.total sum(w)def pickIndex(self) -> int:x random.randint(1, self.total)return bisect_left(self.pre, x)

C++ | Leetcode C++题解之第528题按权重随机选择

题目&#xff1a; 题解&#xff1a; class Solution { private:mt19937 gen;uniform_int_distribution<int> dis;vector<int> pre;public:Solution(vector<int>& w): gen(random_device{}()), dis(1, accumulate(w.begin(), w.end(), 0)) {partial_sum(…

弹簧质点系统求Hessian

Verification https://www.matrixcalculus.org/ (1-l0/norm2(p-q))*(p-q)

游游的游戏大礼包

游游的游戏大礼包 import java.util.*; public class Main {public static void main(String[] args) {Scanner in new Scanner(System.in);long n in.nextInt();long m in.nextInt();long a in.nextInt();long b in.nextInt();long ret 0;for(long x 0; x < Math.…

详解ARM汇编条件标志

版权归作者所有&#xff0c;如有转发&#xff0c;请注明文章出处&#xff1a;https://cyrus-studio.github.io/blog/ 条件标志 在 ARM 指令集中&#xff0c;条件标志是控制指令执行的一种机制&#xff0c;它们用于实现条件分支、比较和其他逻辑操作。 我们平时使用 IDA 调试程…

Navicat Premium安装卸载及使用教程教程

Navicat Premium 17 安装卸载及使用教程教程 0. 卸载 没安装过 Navicat 直接跳过本步骤即可。 正常卸载顺序即可&#xff0c;网上很多教程&#xff0c;这里不演示了 如果怕卸载不干净&#xff0c;最后时候可以执行一下压缩包里面的无限试用 Navicat.bat 即可成功删除Navicat…

Backbone网络详解

Backbone 网络&#xff08;主干网络&#xff09;是深度学习模型中的一个重要组成部分&#xff0c;尤其在计算机视觉任务中。Backbone 网络的主要作用是从输入数据中提取有用的特征&#xff0c;为后续的任务&#xff08;如分类、检测、分割等&#xff09;提供强大的特征表示。常…

Jenkins找不到maven构建项目

有的可能没有出现maven这个选项 解决办法&#xff1a;需要安装Maven项目插件 输入​Maven Integration plugin​

【339】基于springboot的新能源充电系统

毕 业 设 计&#xff08;论 文&#xff09; 题目&#xff1a;新能源充电系统的设计与实现 摘 要 如今社会上各行各业&#xff0c;都喜欢用自己行业的专属软件工作&#xff0c;互联网发展到这个时候&#xff0c;人们已经发现离不开了互联网。新技术的产生&#xff0c;往往能解…

Godot Zelda教程练习1

提示&#xff1a;B站链接&#xff1a;Godot Zelda教程练习 资产链接&#xff1a;项目资产 Godot版本&#xff1a;4.3 文章目录 一、新建项目1、创建项目2、设置项目标签3、项目基本设置4、导入资产 二、图块集和自动平铺1、创建TileMapLayer2、创建Terrian Set1、Match Siders(…

【案例】旗帜飘动

开发平台&#xff1a;Unity 6.0 开发工具&#xff1a;Shader Graph 参考视频&#xff1a;Unity Shader Graph 旗帜飘动特效   一、效果图 二、Shader Graph 路线图 三、案例分析 核心思路&#xff1a;顶点偏移计算 与 顶点偏移忽略 3.1 纹理偏移 视觉上让旗帜保持动态飘动&a…

Android亮屏Job的功耗优化方案

摘要: Job运行时会带来持锁的现象,目前灭屏放电Job的锁托管已经有doze和绿盟标准监管,但是亮屏时仍旧存在过长的持锁现象,故为了优化功耗和不影响用户体验下,新增亮屏放电下如果满足冻结和已运行过一次Job,则进行job限制,当非冻结时恢复的策略 1.现象: (gms_schedu…

Java面试经典 150 题.P55. 跳跃游戏(009)

本题来自&#xff1a;力扣-面试经典 150 题 面试经典 150 题 - 学习计划 - 力扣&#xff08;LeetCode&#xff09;全球极客挚爱的技术成长平台https://leetcode.cn/studyplan/top-interview-150/ 题解&#xff1a; class Solution {public boolean canJump(int[] nums) {int…

源码解析篇 | YOLO11:计算机视觉领域的新突破 !对比YOLOv8如何 ?

前言&#xff1a;Hello大家好&#xff0c;我是小哥谈。在2024年9月27日盛大举行的YOLO Vision 2024活动上&#xff0c;Ultralytics公司震撼发布了YOLO系列的最新成员—YOLO11。作为Ultralytics YOLO系列实时目标检测器的最新迭代&#xff0c;YOLO11凭借尖端的准确性、速度和效率…

mac m1 docker本地部署canal 监听mysql的binglog日志

mac m1 docker本地部署canal监听mysql的binglog日志(虚拟机同理) 根据黑马视频部署 1.docker 部署mysql 1.docker拉取mysql 镜像 因为m1是arm架构.需要多加一条信息 正常拉取 docker pull mysql:tagm1拉取 5.7的版本. tag需要自己指定版本 docker pull --platform linux/x…