JUC Lock 锁入门

文章目录

  • 死锁(Deadlock)
    • 通过 Visualvm 等工具排查死锁
  • 活锁
  • park & unpark
    • 与 wait & notify 的区别
    • park & unpark 实现:点外卖
  • Lock 对象
  • ReentrantLock 可重入锁
    • 可重入
    • lockInterruptibly 方法上锁(可打断)
    • tryLock方法获取锁(锁超时)
  • 公平锁
  • 条件变量 Condition

死锁(Deadlock)

两个或多个线程互相等待对方释放资源,从而导致它们永远无法继续执行。死锁通常涉及多个锁,线程之间在等待对方释放锁时都会被阻塞
比如:t1线程已经获得A锁,进而请求B锁,t2线程已经获得B锁,进而请求A锁。导致两个线程永久阻塞。

import lombok.extern.slf4j.Slf4j;@Slf4j
public class DeadlockTest {public static void main(String[] args) {final Object A = new Object();final Object B = new Object();new Thread(()->{synchronized (A){log.debug("成功获取 A 锁准备获取 B 锁");synchronized (B){log.debug("执行成功");}}},"t1").start();new Thread(()->{synchronized (B){log.debug("成功获取 B 锁准备获取 A 锁");synchronized (A){log.debug("执行成功");}}},"t2").start();}}

通过 Visualvm 等工具排查死锁

在这里插入图片描述
在这里插入图片描述

可使用顺序加锁的方式来解决此死锁问题(不要交错获取锁,不然容易形成死锁)。

注:在 wait 或 join 方法上无限等待的线程,既不是死锁也不是活锁,因为我们可以通过其他线程调用 interrupt 方法来打断此无限等待的情况。如下面的示例:

import lombok.extern.slf4j.Slf4j;@Slf4j
public class WaitTest {private static Thread t1,t2;public static void main(String[] args) {Object obj = new Object();// 线程t1和t2的结束条件都是等对方先执行t1 = new Thread(() -> {synchronized (obj) {try {log.debug("t1 准备让 t2 执行");t2.join();log.debug("t1 执行完毕");} catch (InterruptedException e) {throw new RuntimeException(e);}}}, "t1");t2 = new Thread(() -> {synchronized (obj) {try {log.debug("t2 准备让 t1 执行");t1.join();log.debug("t2 执行完毕");} catch (InterruptedException e) {throw new RuntimeException(e);}}}, "t2");t1.start();t2.start();//        t1.interrupt(); // 调用 interrupt 方法可打断}}

活锁

线程的状态不断的被改变(自身或其他线程修改),导致该线程一直在执行且无法结束。

import lombok.extern.slf4j.Slf4j;import java.util.concurrent.TimeUnit;@Slf4j
public class AliveLockTest {private static Integer count = 100;public static void main(String[] args) {// count 大于 0 执行线程 t1,并 count--// count 小于 50 执行线程 t2,并 count++new Thread(()->{synchronized (count) {while (count > 0) {try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {throw new RuntimeException(e);}count--;}}log.debug("线程 t1 执行结束");},"t1").start();new Thread(()->{synchronized (count) {while (count < 50) {try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {throw new RuntimeException(e);}count++;}}log.debug("线程 t2 执行结束");},"t2").start();}}

死锁和活锁都会导致线程无法停止,但死锁是程序被阻塞不执行,活锁是程序一直执行不结束。

活锁解决通常让其中一个线程先执行完,则另一个线程就能执行完了。

注:此示例如果不加 sleep 有很大概率可以执行结束,但其一定会经历大量的循环执行

  • 饥饿(Starvation)

某些线程一直无法获得所需的资源(由于资源抢占的不公平性),导致一直无法执行。

解决线程饥饿问题通常采用公平锁的方式来解决。

park & unpark

  • LockSupport.park():在某一个线程中调用,表示暂停当前线程。
  • LockSupport.unpark(Thread thread):恢复已暂停的线程

park & unpark 方法底层都是使用的 sun.misc.Unsafe UNSAFE 对象,对应为 native 方法。

与 wait & notify 的区别

  • wait & notify 必须与 synchronized 关键字一起使用,park & unpark 没有这个限制(这也导致相应的代码块是没有同步的)
  • notify 唤醒的线程是随机的,而 unpark 可以指定要唤醒的线程
  • unpark 可以在 park 之前调用,且会生效,后调用的 park 方法也会被取消暂停。而 notify 不能先于 wait 方法调用。

park & unpark 实现:点外卖

import java.util.concurrent.locks.LockSupport;/*** 点外卖:* 线程1:商家* 线程2:买家* 线程3:骑手* 使用 park & unpark 实现*/
public class ParkUnparkTest {/*** 0:未点餐* 1:已点餐,未制作* 2:制作完成,骑手未送货* 3:骑手送货成功,可以开始干饭了*/private static int state = 0;private static Thread t1,t2,t3;public static void main(String[] args) {// 商家t1 = new Thread(()->{while (state < 1){// 没人点餐就等待(需要循环等待,使用 if 会导致虚假唤醒问题(notifyAll 同时唤醒了骑手和买家,但是买家抢到了锁))System.out.println(Thread.currentThread().getName()+":没人点餐,等待中");LockSupport.park();}// 有人点餐就制作System.out.println(Thread.currentThread().getName()+":已接单,制作中");try {// 模拟商家制作时间Thread.sleep(10000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(Thread.currentThread().getName()+":制作成功");state = 2;LockSupport.unpark(t3);// 通知骑手接单(这里可以精确指定骑手线程)},"商家");// 买家t2 = new Thread(()->{long start = System.currentTimeMillis();if(state == 0){state = 1; // 下单System.out.println(Thread.currentThread().getName()+":下单成功,等待送餐");LockSupport.unpark(t1);// 通知卖家线程制作}LockSupport.park();// 等待送餐while (state < 3){// 没有送到,就等待System.out.println(Thread.currentThread().getName()+":外卖没送到,等待中");LockSupport.park();// 等待送餐}// 送到了,就开始干饭System.out.println(Thread.currentThread().getName()+":外卖送到了,开始干饭");// 买家结束(无需再次唤醒商家和卖家)System.out.println("下单到就餐耗时:" + (System.currentTimeMillis() - start));},"买家");// 骑手t3 = new Thread(()->{while (state < 1){// 没人点餐,等待System.out.println(Thread.currentThread().getName()+":没人点餐,等待中");LockSupport.park();// 没人点餐等待}while (state < 2){// 没有制作完成,等待System.out.println(Thread.currentThread().getName()+":没有制作完成,等待中");LockSupport.park();// 没有制作完成,等待}// 制作完成了开始送货System.out.println(Thread.currentThread().getName()+":接到外卖,送货中");try {// 模拟骑手送货Thread.sleep(10000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(Thread.currentThread().getName()+":送货完成");state = 3;LockSupport.unpark(t2);// 通知买家收货},"骑手");t1.start();t2.start();t3.start();}
}

可能的一个结果:

商家:没人点餐,等待中
买家:下单成功,等待送餐 <---- 1
骑手:没人点餐,等待中 <----- 2
商家:已接单,制作中
商家:制作成功
骑手:接到外卖,送货中
骑手:送货完成
买家:外卖送到了,开始干饭
下单到就餐耗时:20021

这里 1 和 2 的打印顺序出现了问题,因为 park & unpark 只是暂停&取消暂停,并没有代码同步所以会有此情况出现。

Lock 对象

java.util.concurrent.locks.Lock 是一个类似于synchronized 块的线程同步机制。但是 Lock比 synchronized 块更加灵活。Lock是个接口,它有多种实现类。其使用方式如下:

 Lock lock = ...;// Lock的实现类lock.lock();// 加锁try {// 访问受此锁保护的资源} finally {lock.unlock();//需要手动调用解锁(解锁操作需要在 finally 块中调用)}

主要方法

    /*** 加锁(不可打断)* 如果当前无法获得锁,则阻塞,直到获取到锁且不可被打断(容易死锁)*/void lock();/*** 加锁(可打断)* 如果当前无法获得锁,则阻塞,直到获取到锁或者被打断** @throws InterruptedException 被打断时抛出异常*/void lockInterruptibly() throws InterruptedException;/*** 直接尝试获得锁,如果成功返回 true,如果不成功返回 false* 其写法如下:*  Lock lock = ...;*  if (lock.tryLock()) {*    try {*      // 受保护的代码*    } finally {*      // 解锁*      lock.unlock();*    }*  } else {*    // 未获得锁的时候的操作,这里没有获得锁,不用解锁*  }** @return 获得锁返回true,未获得锁返回false*/boolean tryLock();/*** 尝试获取锁,但其会等待参数所设置的时间,在该时间内如果获得锁返回 true,如果没有获得锁返回 false* @param time 等待锁的最长时间* @param unit 时间参数的单位* @return 如果获取了锁,则为true;如果在获取锁之前经过了等待时间,则为false* @throws InterruptedException 如果当前线程在获取锁时被打断抛出此异常*/boolean tryLock(long time, TimeUnit unit) throws InterruptedException;/*** 释放锁* 注:如果释放的是非当前线程拥有的锁,将抛出 unchecked 异常 IllegalMonitorStateException*/void unlock();/*** 创建条件对象*/Condition newCondition();

ReentrantLock 可重入锁

ReentrantLock 是 Lock 接口的一个实现类

可重入

每一个锁关联一个线程持有者和计数器,当计数器为 0 时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为 1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为 0,则释放该锁。

简单的说就是当持有该锁的线程再次尝试获取该锁时,不会被阻塞(因为他已经持有该锁),而另外的线程尝试获取该锁时将被阻塞。

import lombok.extern.slf4j.Slf4j;import java.util.concurrent.locks.ReentrantLock;@Slf4j
public class ReentrantLockTest1 {private static ReentrantLock lock = new ReentrantLock();public static void main(String[] args) {new Thread(()->{test1();},"t1").start();}private static void test1(){lock.lock();try{log.debug("进入第1层");test2();}finally {lock.unlock();}}private static void test2(){lock.lock();try{log.debug("进入第1层");test3();}finally {lock.unlock();}}private static void test3(){lock.lock();try{log.debug("进入第1层");}finally {lock.unlock();}}}

lockInterruptibly 方法上锁(可打断)

import lombok.extern.slf4j.Slf4j;import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;@Slf4j
public class LockInterruptiblyTest {private static ReentrantLock lock = new ReentrantLock();private static Thread t1,t2;public static void main(String[] args) {t1 = new Thread(()->{try {lock.lockInterruptibly();} catch (InterruptedException e) {log.debug("被打断表示未获得锁");}try{log.debug("成功获得锁");TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {throw new RuntimeException(e);} finally {lock.unlock();log.debug("释放锁");}},"t1");t1.start();t2 = new Thread(()->{try {lock.lockInterruptibly();} catch (InterruptedException e) {log.debug("被打断表示未获得锁");// 本示例 t2 线程会被打断,进入此块// 这里未获取锁,要么重试,要么结束,否则会执行下面的代码,且没有加锁// 这样的话最后调用 unlock 方法时会抛出异常 IllegalMonitorStateException// 可打断的意义在于,避免 lock 方法的死等,避免死锁}try{log.debug("成功获得锁");TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {throw new RuntimeException(e);} finally {lock.unlock();log.debug("释放锁");}},"t2");t2.start();try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {throw new RuntimeException(e);}// 2 秒后执行打断线程 t2 的操作// 由于 t1 获得锁后睡眠 5 秒,所以此处打断时,打断的是 t2 线程的 lockInterruptibly 方法,导致 t2 线程没有获得锁t2.interrupt();}}

注:注意查看代码中的注释

tryLock方法获取锁(锁超时)

import lombok.extern.slf4j.Slf4j;import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;@Slf4j
public class ReentrantLockTest {public static void main(String[] args) {ReentrantLock reentrantLock = new ReentrantLock();new Thread(()->{if(reentrantLock.tryLock()){try{// 持锁线程,持有锁2秒TimeUnit.SECONDS.sleep(2);log.debug("try 无时间");} catch (InterruptedException e) {e.printStackTrace();} finally {reentrantLock.unlock();}}},"t1").start();try {// 睡眠10毫秒,以希望第一个线程先执行获得锁(这样做并不能保证第一个线程一定获得锁,此处只为验证 tryLock 的特性)TimeUnit.MICROSECONDS.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}new Thread(()->{try {if(reentrantLock.tryLock(5,TimeUnit.SECONDS)){try{// 会打印,因为当前线程会在5秒时间内尝试获取锁,第一个线程持有锁的时间为2秒log.debug("try 有时间");} finally {reentrantLock.unlock();}}} catch (InterruptedException e) {e.printStackTrace();}},"t2").start();new Thread(()->{if(reentrantLock.tryLock()){try{// 不会打印,因为第一个线程持有所,当前线程尝试直接返回falselog.debug("try 第二个无时间");}finally {reentrantLock.unlock();}}},"t3").start();}}

执行结果:

15:37:12.409 [t1] DEBUG com.yyoo.thread.ReentrantLockTest - try 无时间
15:37:12.411 [t2] DEBUG com.yyoo.thread.ReentrantLockTest - try 有时间

t1 线程优先获得锁,所以会打印,t2 线程尝试获得锁且等待5秒,t1线程在2秒后就会释放锁,所以t2获得了锁。t3线程直接尝试获得锁,此时t1还没有释放锁,所以t3没有获得锁。

注:这里只是判断是否可以获得锁的示例所以这里使用的判断为 if 条件,请根据自身情况选择 if 还是 while

tryLock(long timeout, TimeUnit unit) 方法是可以被打断的,所以在 tryLock 等待时间内,如果被打断,依然会抛出 InterruptedException

公平锁

ReentrantLock 对象中有个 Sync 对象,其有两个实现:NonfairSync(不公平锁)、FairSync(公平锁),ReentrantLock 的两个构造函数定义如下:

public ReentrantLock() {sync = new NonfairSync();
}public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();
}

公平锁是按照先进入阻塞队列,先获得锁的思想来实现的。可以用于解决线程饥饿的问题。但也会带来并发度降低的问题。一般都不使用。

Sync 继承自 AbstractQueuedSynchronizer 也就是大名鼎鼎的 AQS,关于 AQS 我们会在后续文章中详解,此处先了解即可,我们先了解怎么用,再来说原理

条件变量 Condition

我们还是用前面送外卖的示例来说明。synchronized 关键字实际上表示一个条件变量,Condition 是可以支持多条件变量,控制粒度更细致。

import lombok.extern.slf4j.Slf4j;import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;@Slf4j
public class LockConditionTest {// 外卖订单private static ReentrantLock order = new ReentrantLock();// 买家条件private static Condition buyer = order.newCondition();// 卖家条件private static Condition seller = order.newCondition();// 骑手条件private static Condition rider = order.newCondition();/*** 0:未点餐* 1:已点餐,未制作* 2:制作完成,骑手未送货* 3:骑手送货成功,可以开始干饭了*/private static int state = 0;public static void main(String[] args) {new Thread(()->{order.lock();try{if(state == 0) {log.debug("没人点餐,等待!");seller.await();}if(state == 1){log.debug("开始制作");TimeUnit.SECONDS.sleep(5);// 模拟制作时间state = 2;log.debug("制作成功");rider.signal();// 通知骑手送货}} catch (InterruptedException e) {// await 方法可以被打断throw new RuntimeException(e);} finally {order.unlock();}},"卖家").start();new Thread(()->{order.lock();try{if(state == 0) {log.debug("没人点餐,等待!");rider.await();}if(state == 1){log.debug("外卖制作中,等待!");rider.await();}if(state == 2){log.debug("取到外卖,开始送货");TimeUnit.SECONDS.sleep(5);// 模拟送货时间state = 3;log.debug("外卖送到");buyer.signal();// 通知买家收货}} catch (InterruptedException e) {// await 方法可以被打断throw new RuntimeException(e);} finally {order.unlock();}},"骑手").start();new Thread(()->{order.lock();try{log.debug("开始点餐");state = 1;log.debug("点餐成功");seller.signalAll();// 通知卖家接单制作log.debug("等待收外卖");buyer.await();// 等待收外卖log.debug("收到外卖,开始干饭");} catch (InterruptedException e) {// await 方法可以被打断throw new RuntimeException(e);} finally {order.unlock();}},"买家").start();}}

执行结果

17:08:44.346 [卖家] DEBUG com.yyoo.thread.LockConditionTest - 没人点餐,等待!
17:08:44.348 [骑手] DEBUG com.yyoo.thread.LockConditionTest - 没人点餐,等待!
17:08:44.348 [买家] DEBUG com.yyoo.thread.LockConditionTest - 开始点餐
17:08:44.348 [买家] DEBUG com.yyoo.thread.LockConditionTest - 点餐成功
17:08:44.348 [买家] DEBUG com.yyoo.thread.LockConditionTest - 等待收外卖
17:08:44.348 [卖家] DEBUG com.yyoo.thread.LockConditionTest - 开始制作
17:08:49.363 [卖家] DEBUG com.yyoo.thread.LockConditionTest - 制作成功
17:08:49.363 [骑手] DEBUG com.yyoo.thread.LockConditionTest - 取到外卖,开始送货
17:08:54.368 [骑手] DEBUG com.yyoo.thread.LockConditionTest - 外卖送到
17:08:54.368 [买家] DEBUG com.yyoo.thread.LockConditionTest - 收到外卖,开始干饭

await 和 signal 以及 signalAll 方法和 wait & notify & notifyAll 方法类似,await & signal 必须要有与之关联的锁,wait & notify 只能在 synchronized 块中使用。

点外卖这个示例,其实就是实际中的线程执行的顺序性问题的解决方案之一。

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

语义分割的应用及发展

语义分割(Semantic Segmentation)是一种计算机视觉领域的任务&#xff0c;旨在将一张图像中的每一个像素都分配一个语义标签&#xff0c;即将图像中的每个物体区域进行精确的分类划分。例如&#xff0c;在一张街景图中&#xff0c;语义分割可以将人、车、路、天空等每个像素分别…

十大排序算法归纳

目录 排序算法的分类 插入排序算法模板 选择排序算法模板 冒泡排序算法模板 希尔排序算法模板 快速排序算法模板 归并排序算法模板 堆排序算法模板 基数排序算法模板 计算排序算法模板 桶排序算法模板 排序算法的分类 插入&#xff1a;插入&#xff0c;折半插入&am…

非科班,培训出身,怎么进大厂?

今天分享一下我是怎么进大厂的经历&#xff0c;希望能给大家带来一点点启发&#xff01; 阿七毕业于上海一所大学的管理学院&#xff0c;在读期间没写过一行 Java 代码。毕业之后二战考研失利。 回过头来看&#xff0c;也很庆幸这次考研失利&#xff0c;因为这个时候对社会一…

linux go环境安装 swag

下载依赖包 go get -u github.com/swaggo/swag编译 移动到下载的go-swagger包目录,一般在$GOPATH/pkg/mod下 查看 GOPATH echo $GOPATHcd /root/GolangProjects/pkg/mod/github.com/swaggo/swagv1.16.2go install ./cmd/swag/不出意外&#xff0c;$GOPATH/bin下 已经有了sw…

基于CMake的大型C++工程组织

此文适合大型C工程&#xff0c;涉及到多个自定义库&#xff0c;多个第三方库&#xff0c;以及还有给第三方用户进行二次开发的需求下&#xff0c;应对这种复杂编译环境下的工程组织方式的一些经验介绍&#xff0c;希望给大型工业软件的开发者一些参考 一个大型工程&#xff0c…

数据结构—树的应用

文章目录 11.树的应用(1).Huffman树#1.加权外部路径长度#2.Huffman算法#3.Huffman编码 (2).二叉搜索树#1.基本定义#2.查找#3.插入结点#4.构建树#5.查找最小值和最大值#6.删除结点#7.一个问题 (3).平衡搜索树#1.满二叉树、完全二叉树和丰满二叉树#2.平衡因子和平衡树#3.左旋与右…

图像中的傅里叶变换及低通与高通滤波

傅里叶变换 高频&#xff1a;在图像中变化剧烈的灰度分量&#xff0c;如边界。 低频&#xff1a;在图像中变化缓慢的灰度分量。 OpenCV中函数为cv2.dft()和cv2.idft()&#xff0c;输入图像要先转换成np.float32格式。得到的结果频率为0的部分会在左上角&#xff0c;为方便处理…

【ROS2】MOMO的鱼香ROS2(二)ROS2入门篇——ROS2初体验

ROS2初体验 引言专业术语认识1 认识ROS21.1 ROS2版本对照表1.2 ROS与ROS2对比1.3 ROS2架构1.3.1 DDS实现层1.3.2 ROS中间件接口&#xff08;RMW&#xff09;1.3.3 ROS2客户端库 RCL 2 安装ROS22.1 ROS安装&#xff08;一键式&#xff09;2.2 手动安装ROS22.2.1 添加ROS软件源2.…

FPGA-DE2-115-实验二-模块化多功能数字钟

模块化多功能数字钟 1.实验要求2.实现过程多功能数字钟的整体RTL视图2.1 顶层模块clock2.2 按键消抖模块key_filiter2.3 数字钟1s/10ms时钟产生模块clk2.4 时间显示(模式0)与调整模块(模式3)clockdisplay2.5 计时(模式1)模块keeptime2.6 闹钟调整(模式2)模块alarmclock2.7 数码…

Unity中URP下精度修饰符real

文章目录 前言一、real是什么&#xff1f;1、我们在项目的Packages下找到如下文件&#xff1a;2、HAS_HALF(1代表有half精度&#xff0c;0代表没有half精度)3、PREFER_HALF4、REAL_IS_HALF5、如果 real is half6、否则为float 二、总结 前言 在使用雾效时&#xff0c;ComputeFo…

TCP服务器的编写(下)

我们现在开始对我们的客户端开始封装 我们的客户端&#xff0c;创建完套接字&#xff0c;需不需要bind呢&#xff1f;&#xff1f; 当然是不需要的&#xff0c;你本身是一个客户端&#xff0c;其他人写的应用也可能是客户端&#xff0c;如果我们bind&#xff0c;一定意味着我们…

2023年03月10日_GPT4发布前的一些消息

2023年3月10日 最近科技圈的消息感觉都要爆炸了 我们都知道 如今爆火的ChatGPT 是在GPT3.5的基础上改进得来的 而OpenAI很早就预告 GPT-4将会在今年发布 不过最近各家大厂争相入局的行动 似乎加快了这个进程 最新消息是 万众期待的GPT-4将于下周推出 在3月9日 微软德…