Java系列|ReentrantLock 可重入锁

基本介绍 ReentrantLock 相对于 synchronized 它具备如下特点

  • 可中断
  • 可以设置超时时间
  • 可以设置为公平锁
  • 支持多个条件变量
  • 与 synchronized 一样,都支持可重入
// 获取锁 reentrantLock.lock(); try { // 临界区 } finally { // 释放锁 reentrantLock.unlock(); }

可重入 可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁 如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住
package cn.knightzz.reentrantlock; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.locks.ReentrantLock; /** * @author 王天赐 * @title: TestReentrantLock * @projectName hm-juc-codes * @description: 测试可重入锁 * @website http://knightzz.cn/ * @github https://github.com/knightzz1998 * @create: 2022-07-22 17:23 */ @SuppressWarnings("all") @Slf4j(topic = "c.TestReentrantLock") public class TestReentrantLock {/** * 可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁 * 如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住 */static ReentrantLock lock = new ReentrantLock(); public static void method01() {lock.lock(); try { log.debug("execute method01"); method02(); } finally { // 释放锁 lock.unlock(); }}private static void method02() { lock.lock(); try { log.debug("execute method02"); method03(); } finally { // 释放锁 lock.unlock(); } }private static void method03() {lock.lock(); try { log.debug("execute method03"); } finally { // 释放锁 lock.unlock(); } }public static void main(String[] args) { method01(); }}

Java系列|ReentrantLock 可重入锁
文章图片

【Java系列|ReentrantLock 可重入锁】可以看到执行结果, 当前线程在执行时多次获取锁, 并不会被锁挡住, 而是正常运行
可打断 可打断就是, 当前线程t1在等待锁的时候, 可以被其他的线程t2使用 t1.interrupt() 方法打断.
lock.lockInterruptibly()尝试获取锁, 并且这个等待锁是可以被打断的
/** * 除非当前线程被中断,否则获取锁。 * 如果没有被另一个线程持有,则获取锁并立即返回,将锁持有计数设置为 1。 * 如果当前线程已经持有这个锁,那么持有计数加一并且方法立即返回。 * 如果锁被另一个线程持有,那么当前线程将被禁用以用于线程调度目的并处于休眠状态,直到发生以下两种情况之一: * 锁被当前线程获取;或者 * 其他一些线程中断当前线程。 * 如果当前线程获取了锁,则锁持有计数设置为 1。 * 如果当前线程: * 在进入此方法时设置其中断状态;或者 * 在获取锁时被中断, * 然后抛出InterruptedException并清除当前线程的中断状态。 * 在此实现中,由于此方法是显式中断点,因此优先响应中断而不是正常或可重入获取锁。 * 抛出: * InterruptedException – 如果当前线程被中断 */ lock.lockInterruptibly();

我们通过下面的代码去演示
package cn.knightzz.reentrantlock; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.locks.ReentrantLock; import static java.lang.Thread.sleep; /** * @author 王天赐 * @title: TestLockInterruptibly * @projectName hm-juc-codes * @description: 测试可打断 * @website http://knightzz.cn/ * @github https://github.com/knightzz1998 * @create: 2022-07-23 14:01 */ @SuppressWarnings("all") @Slf4j(topic = "c.TestLockInterruptibly") public class TestLockInterruptibly {private static ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) {Thread t1 = new Thread(() -> { try { // 尝试获取锁, 并且这个锁是可以被打断的 如果被打断就会抛出异常 // 如果有竞争就进入阻塞队列 lock.lockInterruptibly(); } catch (InterruptedException e) { e.printStackTrace(); log.debug("获取锁的过程中被打断"); return; }try { log.debug("获得锁... "); } finally { lock.unlock(); } }, "t1"); Thread t2 = new Thread(() -> {try { lock.lock(); log.debug("获取锁 ... "); sleep(1); log.debug("打断t1线程的等待锁的过程!"); t1.interrupt(); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { lock.unlock(); } }, "t2"); t2.start(); t1.start(); } }

Java系列|ReentrantLock 可重入锁
文章图片

我们可以看到 t2 线程先获取锁, 然后t1线程在等待锁获取的过程中被t2线程打断
注意如果是不可中断模式,那么即使使用了 interrupt 也不会让等待中断
修改下上面的代码
Thread t1 = new Thread(() -> { try { // 尝试获取锁, 并且这个锁是可以被打断的 如果被打断就会抛出异常 // 如果有竞争就进入阻塞队列 // lock.lockInterruptibly(); lock.lock(); } catch (Exception e) { e.printStackTrace(); log.debug("没有获得锁, 返回"); return; }try { log.debug("获得锁... "); } finally { lock.unlock(); } }, "t1");

Java系列|ReentrantLock 可重入锁
文章图片

可以看到, 即使执行了 interrupt 但是实际上还是没有打断
锁超时 锁超时就是, 如果无法获取锁, 不仅如此阻塞队列, 直接结束
// 仅当调用时没有被另一个线程持有时才获取锁。 // 如果没有被另一个线程持有,则获取锁,并立即返回值为true ,将锁持有计数设置为 1。 // 即使此锁已设置为使用公平排序策略,调用tryLock()也会立即获取该锁(如果可用),无论其他线程当前是否正在等待该锁。 // 这种“闯入”行为在某些情况下可能很有用,即使它破坏了公平性。 // 如果您想尊重此锁的公平设置,请使用几乎等效的tryLock(0, TimeUnit.SECONDS) (它也检测中断)。 // 如果当前线程已经持有这个锁,那么持有计数加一并且该方法返回true 。 // 如果锁被另一个线程持有,则此方法将立即返回值为false 。 // 返回:true锁是空闲的并且被当前线程获取,或者锁已经被当前线程持有,则返回 true;否则falsepublic boolean tryLock() { return sync.nonfairTryAcquire(1); }

package cn.knightzz.reentrantlock; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; /** * @author 王天赐 * @title: TestTimeOutLock * @projectName hm-juc-codes * @description: 测试锁超时 * @website http://knightzz.cn/ * @github https://github.com/knightzz1998 * @create: 2022-07-23 15:29 */ @SuppressWarnings("all") @Slf4j(topic = "c.TestTimeOutLock") public class TestTimeOutLock {private static ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) { Thread t1 = new Thread(() -> { log.debug("尝试获取锁"); if (!lock.tryLock()) { log.debug("获取锁失败!, 直接跑路"); return; }try { log.debug("获得了锁 !"); } finally { lock.unlock(); } }, "t1"); lock.lock(); log.debug("获取锁"); t1.start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { lock.unlock(); log.debug("释放锁..."); } } }

Java系列|ReentrantLock 可重入锁
文章图片

可以看到这里获取锁失败直接就退出了 , 我们也可以使用 tryLock(long, TimeUnit) 方法来设置尝试的时间
package cn.knightzz.reentrantlock; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; /** * @author 王天赐 * @title: TestTimeOutLock * @projectName hm-juc-codes * @description: 测试锁超时 * @website http://knightzz.cn/ * @github https://github.com/knightzz1998 * @create: 2022-07-23 15:29 */ @SuppressWarnings("all") @Slf4j(topic = "c.TestTimeOutLock") public class TestTimeOutLock {private static ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) { Thread t1 = new Thread(() -> { log.debug("尝试获取锁"); try { if (!lock.tryLock(2, TimeUnit.SECONDS)) { log.debug("获取锁失败!, 直接跑路"); return; } log.debug("获得了锁 !"); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { lock.unlock(); } }, "t1"); lock.lock(); log.debug("获取锁"); t1.start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { lock.unlock(); log.debug("释放锁..."); } } }

Java系列|ReentrantLock 可重入锁
文章图片

可以看到, 主线程在获得锁后 1s 后释放锁, 而 t1 线程等待2s ,在t1线程释放锁后, 第一时间获取了锁
解决哲学家就餐问题 筷子类
需要继承 ReentrantLock 类
package cn.knightzz.reentrantlock; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.locks.ReentrantLock; /** * @author 王天赐 * @title: Chopstick * @projectName hm-juc-codes * @description: 筷子类 * @website http://knightzz.cn/ * @github https://github.com/knightzz1998 * @create: 2022-07-22 14:37 */ @Slf4j(topic = "c.Chopstick") public class Chopstick extends ReentrantLock {private String name; public Chopstick(String name) { this.name = name; }@Override public String toString() { return "筷子{" + "name='" + name + '\'' + '}'; } }

哲学家类
package cn.knightzz.reentrantlock; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.TimeUnit; /** * @author 王天赐 * @title: Philosopher * @projectName hm-juc-codes * @description: 哲学家类 * @website http://knightzz.cn/ * @github https://github.com/knightzz1998 * @create: 2022-07-22 14:37 */ @Slf4j(topic = "c.Philosopher") @SuppressWarnings("all") public class Philosopher extends Thread {private Chopstick left; private Chopstick right; public Philosopher(String name, Chopstick left, Chopstick right) { super(name); this.left = left; this.right = right; }public void eat() { log.debug("eat ... "); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { throw new RuntimeException(e); } }@Override public void run() { while (true) { if (left.tryLock()) { try { if (right.tryLock()) { try { eat(); } finally { right.unlock(); } }} finally { left.unlock(); } } } } }

可以看到, 我们需要使用 tryLock 方法去获取左筷子和右筷子, 如果获取失败直接结束, 另外在成功获取锁后.
要在 finally 里释放锁
测试类
package cn.knightzz.reentrantlock; import lombok.extern.slf4j.Slf4j; /** * @author 王天赐 * @title: TestPhilosopher * @projectName hm-juc-codes * @description: 哲学家进餐问题 * @website http://knightzz.cn/ * @github https://github.com/knightzz1998 * @create: 2022-07-22 15:24 */ @SuppressWarnings("all") @Slf4j(topic = "TestPhilosopher") public class TestPhilosopher {public static void main(String[] args) {Chopstick c1 = new Chopstick("1"); Chopstick c2 = new Chopstick("2"); Chopstick c3 = new Chopstick("3"); Chopstick c4 = new Chopstick("4"); Chopstick c5 = new Chopstick("5"); new Philosopher("苏格拉底", c1, c2).start(); new Philosopher("柏拉图", c2, c3).start(); new Philosopher("亚里士多德", c3, c4).start(); new Philosopher("赫拉克利特", c4, c5).start(); new Philosopher("阿基米德", c5, c1).start(); } }

公平锁 公平锁一般没有必要,会降低并发度
ReentrantLock 默认是不公平的 , 也就是说并不是按照阻塞队列中先来先得的顺序得到锁的, 随机分配锁的
package cn.knightzz.reentrantlock; import java.util.concurrent.locks.ReentrantLock; /** * @author 王天赐 * @title: TestFairLock * @projectName hm-juc-codes * @description: 测试公平锁 * @website http://knightzz.cn/ * @github https://github.com/knightzz1998 * @create: 2022-07-23 16:31 */ @SuppressWarnings("all") public class TestFairLock {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(); } }

Java系列|ReentrantLock 可重入锁
文章图片

注意:该实验不一定总能复现
如果是公平锁, 随机插入一定是在最后插入
条件变量 基本介绍
  • synchronized 中也有条件变量,就是 waitSet 休息室,当条件不满足时进入 waitSet 等待
  • ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比 synchronized 是那些不满足条件的线程都在一间休息室等消息,而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒
使用要点
  • await 前需要获得锁
  • await 执行后,会释放锁,进入 conditionObject 等待
  • await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁
  • 竞争 lock 锁成功后,从 await 后继续执行
代码案例
package cn.knightzz.reentrantlock; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; import static java.lang.Thread.sleep; /** * @author 王天赐 * @title: TestConditionLock * @projectName hm-juc-codes * @description: 测试条件变量 * @website http://knightzz.cn/ * @github https://github.com/knightzz1998 * @create: 2022-07-23 16:45 */ @SuppressWarnings("all") @Slf4j(topic = "c.TestConditionLock") public class TestConditionLock {static ReentrantLock lock = new ReentrantLock(); static Condition waitCigaretteQueue = lock.newCondition(); static Condition waitBreakfastQueue = lock.newCondition(); static boolean hasCigarette = false; static boolean hasBreakfast = false; private static void sendCigarette() { lock.lock(); try { log.debug("烟送来了 ..."); hasCigarette = true; // 唤醒对应waitset阻塞的线程 waitCigaretteQueue.signal(); } finally { lock.unlock(); }}private static void sendBreakfast() { lock.lock(); try { log.debug("早餐送来了 ..."); hasBreakfast = true; // 唤醒对应waitset阻塞的线程 waitBreakfastQueue.signal(); } finally { lock.unlock(); } }public static void main(String[] args) throws InterruptedException {new Thread(() -> { try { // 如果没有拿到锁的话, 线程就会阻塞在这, 不会向下执行, 和 synchronized 类似 lock.lock(); while (!hasCigarette) { // 不满足条件就到对应的 waitSet 等待 try { waitCigaretteQueue.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } } log.debug("等到烟了"); } finally { lock.unlock(); } }, "小南").start(); new Thread(() -> { try { // 如果没有拿到锁的话, 线程就会阻塞在这, 不会向下执行, 和 synchronized 类似 lock.lock(); while (!hasBreakfast) { // 不满足条件就到对应的 waitSet 等待 try { waitBreakfastQueue.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } } log.debug("等到早餐了"); } finally { lock.unlock(); } }, "小白").start(); sleep(1000); sendCigarette(); sleep(1000); sendBreakfast(); } }

  • 小南需要等待烟过来, 否则就一直等待
  • 小白需要等待早餐, 否则就一直等待
Java系列|ReentrantLock 可重入锁
文章图片

    推荐阅读