李林超博客
首页
归档
留言
友链
动态
关于
归档
留言
友链
动态
关于
首页
Java
正文
22.并发编程之ReentrantLock简介
Leefs
2022-10-25 PM
590℃
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
标签云
DataX
Docker
Typora
JavaWeb
Git
Spark
Flink
BurpSuite
Flume
Sentinel
Spring
Ubuntu
ajax
持有对象
nginx
Tomcat
正则表达式
Hive
Quartz
MyBatisX
数学
二叉树
Filter
RSA加解密
设计模式
Spark SQL
Java工具类
递归
Spark RDD
Livy
友情链接
申请
范明明
庄严博客
Mx
陶小桃Blog
虫洞