李林超博客
首页
归档
留言
友链
动态
关于
归档
留言
友链
动态
关于
首页
Java
正文
22.并发编程之ReentrantLock简介
Leefs
2022-10-25 PM
1239℃
0条
[TOC] ### 一、概述 ReentrantLock是一种基于AQS框架的应用实现,是JDK中的一种线程并发访问的同步手段,它的功能类似于synchronized是一种互斥锁,可以保证线程安全。 **相对于 synchronized 它具备如下特点:** - **可中断** + synchronized锁加上去不能中断,a线程应用锁,b线程不能取消掉它 - **可以设置超时时间** + synchronized它去获取锁时,如果对方持有锁,那么它就会进入entryList一直等待下去。而ReentrantLock可以设置超时时间,规定时间内如果获取不到锁,就放弃锁。 - **可以设置为公平锁** + 防止线程饥饿的情况,即先到先得。如果争抢的人比较多,则可能会发生永远都得不到锁。 - **支持多个条件变量(相当于有多个EntryList)** + synchronized只支持同一个waitset。 - **与 synchronized 一样,都支持可重入** ### 二、基本语法 **步骤** (1)创建一个`ReentrantLock`对象; (2)调用`ReentrantLock`对象的`lock()`方法; (3)将临界区的代码写在try代码块中; (4)将`ReentrantLock`对象的`unlock()`方法写在finally代码块中。 ```java private Lock lock = new ReentrantLock(); // 获取锁 lock.lock(); try { // 临界区 } finally { // 释放锁 lock.unlock(); } ``` + **synchronized是在关键字的级别来保护临界区,而reentrantLock是在对象的级别保护临界区。**临界区即访问共享资源的那段代码。 + `finally`中表明不管将来是否出现异常,都会释放锁,释放锁即调用unlock方法。否则无法释放锁,其它线程就永远也获取不了锁。 注意:`lock.lock();`与try代码块之间**不要有空行**或者其它逻辑,且`lock.unlock();`要写在finally代码块的**第一行**。 ### 三、可重入 #### 3.1 概述 + 可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁; + 如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住。 #### 3.2 代码示例 ```java import lombok.extern.slf4j.Slf4j; import java.util.concurrent.locks.ReentrantLock; /** * @author lilinchao * @date 2022-10-25 * @description ReentrantLock 可重入 **/ @Slf4j(topic = "c.Test03") public class Test03 { static ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) { method1(); } public static void method1(){ lock.lock(); try { log.debug("execute method1"); method2(); } finally { lock.unlock(); } } public static void method2(){ lock.lock(); try { log.debug("execute method2"); method3(); }finally { lock.unlock(); } } public static void method3(){ lock.lock(); try { log.debug("execute method3"); }finally { lock.unlock(); } } } ``` **运行结果** ``` 21:58:48.257 c.Test03 [main] - execute method1 21:58:48.259 c.Test03 [main] - execute method2 21:58:48.259 c.Test03 [main] - execute method3 ``` 从运行结果可以看出,当前线程在执行时多次获取锁, 并不会被锁挡住, 而是正常运行 *注意:加锁与解锁是必须**匹配**的,只有当解锁次数等于加锁次数时,锁才会被正确释放。* ### 四、可打断 #### 4.1 概述 可打断是指, 当前线程在等待锁的时候, 可以被其他的线程使用 `interrupt()` 方法打断。synchronized是不可中断锁,而ReentrantLock则提供了中断功能。 **方法说明** + **lock.lockInterruptibly()**:尝试获取锁,如果获取不到锁,进入等待;等待过程中可以被打断。 + **lock.lock():**等待锁的过程是不可以被打断的。 #### 4.2 代码示例 ```java import lombok.extern.slf4j.Slf4j; import java.util.concurrent.locks.ReentrantLock; import static com.lilinchao.thread.utils.Sleeper.sleep; /** * @author lilinchao * @date 2022-10-25 * @description 打断ReentrantLock锁 **/ @Slf4j(topic = "c.Test04") public class Test04 { public static void main(String[] args) { ReentrantLock lock = new ReentrantLock(); Thread t1 = new Thread(() -> { log.debug("start..."); try { lock.lockInterruptibly(); } catch (InterruptedException e) { e.printStackTrace(); log.debug("等锁的过程中被打断"); return; } try { log.debug("获得了锁"); }finally { lock.unlock(); } },"t1"); lock.lock(); log.debug("获得了锁"); t1.start(); try { sleep(1); t1.interrupt(); log.debug("执行打断"); } finally { lock.unlock(); } } } ``` **运行结果** ``` 22:21:02.967 c.Test04 [main] - 获得了锁 22:21:02.969 c.Test04 [t1] - start... 22:21:03.984 c.Test04 [main] - 执行打断 java.lang.InterruptedException at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898) at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222) at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335) at com.lilinchao.thread.demo06.Test04.lambda$main$0(Test04.java:22) at java.lang.Thread.run(Thread.java:748) 22:21:03.985 c.Test04 [t1] - 等锁的过程中被打断 ``` **说明** main线程首先获得锁,因此被创建出的线程t1启动后无法获得锁,之后,main线程打断线程t1,使得线程t1结束等待。 #### 4.3 不可中断模式 注意如果是不可中断模式,那么即使使用了 interrupt 也不会让等待中断 ```java import lombok.extern.slf4j.Slf4j; import java.util.concurrent.locks.ReentrantLock; import static com.lilinchao.thread.utils.Sleeper.sleep; /** * @author lilinchao * @date 2022-10-25 * @description 不可中断模式 **/ @Slf4j(topic = "c.Test05") public class Test05 { public static void main(String[] args) { ReentrantLock lock = new ReentrantLock(); Thread t1 = new Thread(() -> { log.debug("start..."); lock.lock(); try { log.debug("获得了锁"); }finally { lock.unlock(); } },"t1"); lock.lock(); log.debug("获得了锁"); t1.start(); try { sleep(1); t1.interrupt(); log.debug("执行打断"); sleep(1); } finally { log.debug("释放了锁"); lock.unlock(); } } } ``` **运行结果** ``` 22:24:52.756 c.Test05 [main] - 获得了锁 22:24:52.758 c.Test05 [t1] - start... 22:24:53.771 c.Test05 [main] - 执行打断 22:24:54.777 c.Test05 [main] - 释放了锁 22:24:54.777 c.Test05 [t1] - 获得了锁 ``` ### 五、锁超时 #### 5.1 概述 可打断,是一种被动的打断,需要其他的线程来进行打断。 而锁超时可以通过主动方式,来解决线程无限制的等待下去。如果当前线程在等待了一段时间之后,还没有获取锁,将不在继续等待,继续向下执行。 通过设置获得锁的等待时间,当不能在等待时间内获得锁的时候释放锁,就能够避免死锁的问题。 #### 5.2 设置超时时间API ReetrantLock提供了两个获取锁并快速返回的方法,不会一直等待,无论成功失败都将立即返回。 | 方法 | 说明 | | ------------------------------------ | ------------------------------------------------------------ | | tryLock() | 尝试获得锁,如果成功了,就获得了锁,返回true,如果失败了,可以不去进入阻塞队列等待,返回false,表示没有获得锁。 | | tryLock(long timeout, TimeUnit unit) | 可以设置尝试获取锁的最大等待时间,如果超过了最大等待时间,则获取不到锁。这个方法返回一个布尔值,如果得到了锁,则返回true否则返回false。 | #### 5.3 代码示例 + **无参tryLock()方法** ```java import lombok.extern.slf4j.Slf4j; import java.util.concurrent.locks.ReentrantLock; import static com.lilinchao.thread.utils.Sleeper.sleep; /** * @author lilinchao * @date 2022-10-25 * @description 锁超时 - 立刻失败 **/ @Slf4j(topic = "c.Test06") public class Test06 { public static void main(String[] args) { ReentrantLock lock = new ReentrantLock(); Thread t1 = new Thread(() -> { log.debug("start..."); if (!lock.tryLock()) { log.debug("获取立即失败,返回"); return; } try { log.debug("获得了锁"); }finally { lock.unlock(); } },"t1"); lock.lock(); log.debug("获得了锁"); t1.start(); try { sleep(2); }finally { lock.unlock(); } } } ``` **运行结果** ``` 22:40:21.990 c.Test06 [main] - 获得了锁 22:40:21.992 c.Test06 [t1] - start... 22:40:21.992 c.Test06 [t1] - 获取立即失败,返回 ``` + **带参tryLock方法** ```java import lombok.extern.slf4j.Slf4j; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import static com.lilinchao.thread.utils.Sleeper.sleep; /** * @author lilinchao * @date 2022-10-25 * @description 超时失败 **/ @Slf4j(topic = "c.Test07") public class Test07 { public static void main(String[] args) { ReentrantLock lock = new ReentrantLock(); Thread t1 = new Thread(() -> { log.debug("start..."); try { if (!lock.tryLock(1, TimeUnit.SECONDS)) { log.debug("获取等待 1s 后失败,返回"); return; } } catch (InterruptedException e) { e.printStackTrace(); } try { log.debug("获得了锁"); }finally { lock.unlock(); } },"t1"); lock.lock(); log.debug("获得了锁"); t1.start(); try { //主线程等待2s sleep(2); }finally { lock.unlock(); } } } ``` **运行结果** ``` 22:42:41.537 c.Test07 [main] - 获得了锁 22:42:41.539 c.Test07 [t1] - start... 22:42:42.544 c.Test07 [t1] - 获取等待 1s 后失败,返回 ``` **说明** 代码执行时,main线程先获得了锁,进入到2s的睡眠当中,此时t1线程启动,执行到`lock.tryLock(1, TimeUnit.SECONDS)`,等待获取到lock锁后继续向下执行。1s后t1线程未获得所,将放弃继续获取锁,t1线程退出。 #### 5.4 锁超时解决哲学家就餐问题 ```java import com.lilinchao.thread.utils.Sleeper; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.locks.ReentrantLock; /** * @author lilinchao * @date 2022-10-25 * @description **/ public class Test08 { public static void main(String[] args) { Chopstick2 c1 = new Chopstick2("1"); Chopstick2 c2 = new Chopstick2("2"); Chopstick2 c3 = new Chopstick2("3"); Chopstick2 c4 = new Chopstick2("4"); Chopstick2 c5 = new Chopstick2("5"); new Philosopher2("苏格拉底", c1, c2).start(); new Philosopher2("柏拉图", c2, c3).start(); new Philosopher2("亚里士多德", c3, c4).start(); new Philosopher2("赫拉克利特", c4, c5).start(); new Philosopher2("阿基米德", c5, c1).start(); } } @Slf4j(topic = "c.Philosopher2") class Philosopher2 extends Thread { Chopstick2 left; Chopstick2 right; public Philosopher2(String name, Chopstick2 left, Chopstick2 right) { super(name); this.left = left; this.right = right; } @Override public void run() { while (true) { // 尝试获得左手筷子 if (left.tryLock()) { try { // 尝试获得右手筷子 if (right.tryLock()) { try { eat(); } finally { right.unlock(); } } } finally { // 如果没有获得右手的筷子,则释放自己手里的筷子 left.unlock(); } } } } private void eat() { log.debug("eating..."); Sleeper.sleep(1); } } class Chopstick2 extends ReentrantLock { String name; public Chopstick2(String name) { this.name = name; } @Override public String toString() { return "筷子{" + name + '}'; } } ``` **运行结果** ``` 22:56:17.080 c.Philosopher2 [亚里士多德] - eating... 22:56:17.080 c.Philosopher2 [苏格拉底] - eating... 22:56:18.093 c.Philosopher2 [柏拉图] - eating... 22:56:18.093 c.Philosopher2 [赫拉克利特] - eating... 22:56:19.093 c.Philosopher2 [苏格拉底] - eating... 22:56:19.093 c.Philosopher2 [亚里士多德] - eating... 22:56:20.101 c.Philosopher2 [亚里士多德] - eating... 22:56:20.101 c.Philosopher2 [阿基米德] - eating... ........... 程序会一直向下执行,不会产生死锁 ``` **说明** 可以看到,需要使用 `tryLock` 方法去获取左筷子和右筷子, 如果获取失败直接结束, 另外在成功获取锁后,要在 finally 里释放锁。 ### 六、公平锁 #### 6.1 概念 + **公平锁**:是指多个线程按照申请锁的顺序来获取锁,通过队列FIFO,先进先出,类似排队打饭,先来后到。 + **非公平锁**:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。在高并发的情况下,有可能会造成优先级反转或者饥饿现象。 #### 6.2 语法 ReentrantLock默认是非公平锁 ```java ReentrantLock lock = new ReentrantLock(true); // true:公平锁 lock.lock(); try { // todo } finally { lock.unlock(); } ``` **说明** - 初始化构造函数入参,选择是否为初始化公平锁。 - 其实一般情况下并不需要公平锁,除非你的场景中需要保证顺序性。 - 使用 ReentrantLock 切记需要在 finally 中关闭,lock.unlock()。 ##### 公平锁和非公平锁的选择 一点源码 ```java public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); } ``` - 构造函数中选择公平锁(FairSync)、非公平锁(NonfairSync)。 #### 6.3 代码示例 + **非公平锁** ```java import java.util.concurrent.locks.ReentrantLock; /** * Created by lilinchao * Date 2022/10/25 * Description 公平锁/非公平锁 */ public class FairLockDemo { public static void main(String[] args) throws InterruptedException { ReentrantLock lock = new ReentrantLock(false); lock.lock(); for (int i = 0; i < 500; i++) { new Thread(() -> { lock.lock(); try { System.out.println(Thread.currentThread().getName() + " running..."); } finally { lock.unlock(); } }, "t" + i).start(); } // 1s 之后去争抢锁 Thread.sleep(1000); new Thread(() -> { System.out.println(Thread.currentThread().getName() + " start..."); lock.lock(); try { System.out.println(Thread.currentThread().getName() + " running..."); } finally { lock.unlock(); } }, "强行插入").start(); lock.unlock(); } } ``` 强行插入,有机会在中间输出 **运行结果** ![22.并发编程之ReentrantLock简介01.jpg](https://lilinchao.com/usr/uploads/2022/10/1348863162.jpg) 改为公平锁后 ``` ReentrantLock lock = new ReentrantLock(true); ``` 强行插入,总是在最后输出 ![22.并发编程之ReentrantLock简介02.jpg](https://lilinchao.com/usr/uploads/2022/10/1041095818.jpg) **说明** 开启公平锁后,所有的线程在entrylist中按照开始的时间顺序执行,不会出现插队现象,所以公平锁能够解决饥饿现象。不开启公平锁,当上一个线程结束后,随机从entrylist中执行一个线程。 #### 6.4 总结 **公平锁** + 优点:吞吐率较高。 + 缺点:从申请者个体的角度来看,这些申请者获得相应资源的独占权所需时间的偏差可能比较大,即有的线程很快就能申请到资源,而有的线程则要经历若干次暂停与唤醒才能成功申请到资源,`极端情况下可能导致饥饿现象`。 **非公平锁** + 优点:从个体申请者的角度来看,这些申请者获得相应资源的独占权所需时间的偏差可能比较小,即每个资源申请者申请到资源所需的时间基本相同,并且不会导致饥饿现象。 + 缺点:吞吐率较低,这是其维护资源独占权的授予顺序的开销比较大(主要是线程的暂停与唤醒所导致的上下文切换)的结果。 ### 七、条件变量 #### 7.1 概述 + 关键字synchronized中也有条件变量,就是waitSet,可以理解为条件不满足时进入waitSet等待,一个synchronized只能对应一个waitSet。 + ReentrantLock可以支持多个条件变量,因此可以将不同条件的线程放入等待集合中,以便于后续进行专门的唤醒。在ReentrantLock中使用条件变量需要使用await()方法。 > ReentrantLock的条件变量比synchronized强大之处在于,它支持多个条件变量(对象)。 ##### 使用流程 - 使用`ReentrantLock`对象创建条件变量`condition`; - 执行`condition.await()`前需要先获取锁; - 执行`condition.await()`后,线程会释放锁,并进入`conditionObject`中等待; - 其它线程执行`condition.signal()`或者`condition.signalAll()`唤醒`conditionObject`中等待的线程; - 被唤醒后会重新竞争锁 - 竞争锁成功后,会从await()后的代码处开始执行 **函数`await()`调用方式** | 函数签名 | 用法 | 返回值含义 | | ----------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------ | | `void await()` | 进入等待,直到被其它线程唤醒 | 无返回值 | | `boolean await(long time, TimeUnit unit)` | 进入等待,直到被其它线程唤醒或超时 | 如果等待时间耗尽依然没有被唤醒,则会返回false | | `void awaitUninterruptibly()` | 该次等待**不可以被打断**,其余都是可以被打断的 | 无返回值 | | `boolean awaitUntil(Date deadline)` | 进入等待,直到被其它线程唤醒或到达某一时间节点 | 如果到达指定时间依然没有被唤醒,则会返回false | | `long awaitNanos(long nanosTimeout)` | 进入等待,并返回剩余等待的时间 | 剩余等待的时间,如果为正值则可以继续等待,为负值则说明等待已经超时 | #### 7.2 代码示例 > - t1需要等待烟过来, 否则就一直等待 > - t2需要等待早餐, 否则就一直等待 ```java import lombok.extern.slf4j.Slf4j; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; import static com.lilinchao.concurrent.utils.Sleeper.sleep; /** * Created by lilinchao * Date 2022/10/25 * Description 条件变量 */ @Slf4j(topic = "c.ConditionTest") public class ConditionTest { static ReentrantLock lock = new ReentrantLock(); static Condition waitCigaretteQueue = lock.newCondition(); static Condition waitbreakfastQueue = lock.newCondition(); static volatile boolean hasCigrette = false; static volatile boolean hasBreakfast = false; public static void main(String[] args) { new Thread(() -> { try { // 如果没有拿到锁的话, 线程就会阻塞在这, 不会向下执行 lock.lock(); while (!hasCigrette) { // 不满足条件就到对应的 waitSet 等待 try { waitCigaretteQueue.await(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug("等到了它的烟"); } finally { lock.unlock(); } },"t1").start(); new Thread(() -> { try { lock.lock(); while (!hasBreakfast) { try { waitbreakfastQueue.await(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug("等到了它的早餐"); } finally { lock.unlock(); } },"t2").start(); sleep(1); sendBreakfast(); sleep(1); sendCigarette(); } private static void sendCigarette() { lock.lock(); try { log.debug("送烟来了"); hasCigrette = true; waitCigaretteQueue.signal(); } finally { lock.unlock(); } } private static void sendBreakfast() { lock.lock(); try { log.debug("送早餐来了"); hasBreakfast = true; waitbreakfastQueue.signal(); } finally { lock.unlock(); } } } ``` **运行结果** ``` 23:52:23.927 c.ConditionTest [main] - 送早餐来了 23:52:23.930 c.ConditionTest [t2] - 等到了它的早餐 23:52:24.931 c.ConditionTest [main] - 送烟来了 23:52:24.931 c.ConditionTest [t1] - 等到了它的烟 ```
标签:
并发编程
非特殊说明,本博所有文章均为博主原创。
如若转载,请注明出处:
https://lilinchao.com/archives/2523.html
上一篇
21.并发编程之活跃性分析
下一篇
23.同步模式之顺序控制
评论已关闭
栏目分类
随笔
2
Java
326
大数据
229
工具
31
其它
25
GO
47
NLP
4
标签云
前端
链表
数据结构
GET和POST
Spark SQL
ajax
并发线程
队列
MyBatis-Plus
Java编程思想
稀疏数组
容器深入研究
Http
MySQL
持有对象
二叉树
Livy
锁
Ubuntu
序列化和反序列化
高并发
Thymeleaf
MyBatisX
Elastisearch
数学
哈希表
Kafka
工具
Python
VUE
友情链接
申请
范明明
庄严博客
Mx
陶小桃Blog
虫洞
评论已关闭