深入探究 ReentrantLock 的应用和原理

news/2024/10/17 22:16:31/

博主介绍: ✌博主从事应用安全和大数据领域,有8年研发经验,5年面试官经验,Java技术专家✌

Java知识图谱点击链接:体系化学习Java(Java面试专题)

💕💕 感兴趣的同学可以收藏关注下不然下次找不到哟💕💕

在这里插入图片描述

文章目录

  • 1、ReentrantLock 简介
  • 2、ReentrantLock 的使用
    • 2.1、基本使用
    • 2.2、tryLock 的使用
    • 2.3、Condition 实现等待/通知机制
    • 2.4、多 Condition 使用
    • 2.5、公平锁
    • 2.6、可重入锁
  • 3、究根问底 - ReentrantLock 源码分析
  • 4、ReentrantLock 实际应用

1、ReentrantLock 简介

ReentrantLock 是 Java 中的一个锁实现,它提供了与 synchronized 关键字类似的功能,但是它更加灵活和可扩展。ReentrantLock 是一个可重入锁,也就是说同一个线程可以多次获得同一个锁而不会发生死锁。

ReentrantLock 的主要方法包括:

  1. lock():获取锁,如果锁已经被其他线程获取,则当前线程会被阻塞。
  2. unlock():释放锁。
  3. tryLock():尝试获取锁,如果锁已经被其他线程获取,则立即返回 false。
  4. tryLock(long timeout, TimeUnit unit):尝试获取锁,如果锁已经被其他线程获取,则当前线程会被阻塞,直到超时或者获取到锁。
  5. newCondition():创建一个 Condition 对象,用于实现等待/通知机制。

ReentrantLock 的优点之一是它提供了更细粒度的锁控制,可以通过 lockInterruptibly() 方法实现可中断的锁获取,也可以通过 tryLock() 方法实现非阻塞的锁获取。此外,ReentrantLock 还支持公平锁和非公平锁,可以通过构造函数来指定锁的类型。

需要注意的是,使用 ReentrantLock 时需要手动释放锁,否则可能会导致死锁。通常使用 try-finally 语句块来确保锁一定会被释放。

2、ReentrantLock 的使用

2.1、基本使用

首先看一段没有加锁的代码:

package com.pany.camp.reentrantLock;import java.util.concurrent.locks.ReentrantLock;/**** @description:  ReentrantLock 基本使用案例* @copyright: @Copyright (c) 2022 * @company: Aiocloud* @author: pany* @version: 1.0.0 * @createTime: 2023-06-25 20:47*/
public class ReentrantLockDemo {private static final ReentrantLock lock = new ReentrantLock();public static void main(String[] args) {Thread t1 = new Thread(() -> {try {          System.out.println("Thread 1 acquired the start.");Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();} finally { System.out.println("Thread 1 released the end.");}});Thread t2 = new Thread(() -> {try {System.out.println("Thread 2 acquired the start.");Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();} finally {System.out.println("Thread 2 released the end.");}});t1.start();t2.start();}
}

执行后的结果如下:

Thread 2 acquired the start.
Thread 1 acquired the start.
Thread 2 released the end.
Thread 1 released the end.Process finished with exit code 0

两个线程同时开始,同时结束。接下来我们使用 ReentrantLock 加上锁,看看效果。

package com.pany.camp.reentrantLock;import java.util.concurrent.locks.ReentrantLock;/**** @description:  ReentrantLock 基本使用案例* @copyright: @Copyright (c) 2022 * @company: Aiocloud* @author: pany* @version: 1.0.0 * @createTime: 2023-06-25 20:47*/
public class ReentrantLockDemo {private static final ReentrantLock lock = new ReentrantLock();public static void main(String[] args) {Thread t1 = new Thread(() -> {try {lock.lock();System.out.println("Thread 1 acquired the start.");Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();System.out.println("Thread 1 released the end.");}});Thread t2 = new Thread(() -> {try {lock.lock();System.out.println("Thread 2 acquired the start.");Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();System.out.println("Thread 2 released the end.");}});t1.start();t2.start();}
}

输出结果如下:

Thread 1 acquired the start.
Thread 1 released the end.
Thread 2 acquired the start.
Thread 2 released the end.Process finished with exit code 0

我们创建了一个 ReentrantLock 对象,并在两个线程中使用它来控制共享资源的访问。在每个线程的 run 方法中,我们首先使用 lock() 方法获取锁,然后执行一些临界区代码,最后使用 unlock() 方法释放锁。由于 ReentrantLock 是可重入锁,因此同一个线程可以多次获取同一个锁,不会发生死锁。

2.2、tryLock 的使用

接下来我们看下 tryLock 的使用:

package com.pany.camp.reentrantLock;import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;/**** @description:  tryLock 的使用* @copyright: @Copyright (c) 2022 * @company: Aiocloud* @author: pany* @version: 1.0.0 * @createTime: 2023-06-25 20:55*/
public class TryLockDemo {private static final ReentrantLock lock = new ReentrantLock();public static void main(String[] args) {Thread t1 = new Thread(() -> {try {if (lock.tryLock(2000, TimeUnit.MILLISECONDS)) {try {System.out.println("Thread 1 acquired the lock.");Thread.sleep(3000);} finally {lock.unlock();System.out.println("Thread 1 released the lock.");}} else {System.out.println("Thread 1 failed to acquire the lock.");}} catch (InterruptedException e) {e.printStackTrace();}});Thread t2 = new Thread(() -> {try {if (lock.tryLock(2000, TimeUnit.MILLISECONDS)) {try {System.out.println("Thread 2 acquired the lock.");Thread.sleep(1000);} finally {lock.unlock();System.out.println("Thread 2 released the lock.");}} else {System.out.println("Thread 2 failed to acquire the lock.");}} catch (InterruptedException e) {e.printStackTrace();}});t1.start();t2.start();}
}

我们首先创建了一个 ReentrantLock 对象,并在两个线程中使用它来控制共享资源的访问。然后,在每个线程的 run 方法中,我们使用 tryLock 方法尝试获取锁,等待 2000 毫秒。如果获取锁成功,就执行临界区代码,然后释放锁。如果获取锁失败,就输出一条失败信息。
输出结果如下:

Thread 1 acquired the lock.
Thread 2 failed to acquire the lock.
Thread 1 released the lock.Process finished with exit code 0

这表明线程 1 成功获取了锁并执行了临界区代码,然后释放了锁。而线程 2 在等待 2000 毫秒后仍然无法获取锁,因此输出了一条失败信息。需要注意的是,在使用 tryLock 方法时,如果获取锁失败,线程并不会一直等待,而是会立即返回。这使得我们可以避免线程因为长时间等待锁而被阻塞的情况。

2.3、Condition 实现等待/通知机制

案例如下:

package com.pany.camp.reentrantLock;import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;/**** @description:  Condition 实现等待/通知机制* @copyright: @Copyright (c) 2022 * @company: Aiocloud* @author: pany* @version: 1.0.0 * @createTime: 2023-06-25 20:59*/
public class ConditionDemo {private static final ReentrantLock lock = new ReentrantLock();private static final Condition condition = lock.newCondition();private static boolean flag = false;public static void main(String[] args) {Thread t1 = new Thread(() -> {lock.lock();try {while (!flag) {System.out.println("Thread 1 is waiting.");condition.await();}System.out.println("Thread 1 is notified.");} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}});Thread t2 = new Thread(() -> {lock.lock();try {flag = true;System.out.println("Thread 2 is notifying.");condition.signal();} finally {lock.unlock();}});t1.start();t2.start();}
}

我们首先创建了一个 ReentrantLock 对象和一个 Condition 对象。然后,在线程 1 中,我们使用 while 循环来等待条件 flag 的满足,并在每次循环中使用 condition.await() 方法来释放锁并等待通知。在线程 2 中,我们设置条件 flag 为 true,并使用 condition.signal() 方法来通知线程 1,然后释放锁。

输出结果如下:

Thread 1 is waiting.
Thread 2 is notifying.
Thread 1 is notified.Process finished with exit code 0

线程 1 在等待 flag 条件满足时被阻塞,并在线程 2 通知后被唤醒。需要注意的是,在使用 Condition 实现等待/通知机制时,必须在获取锁之后才能使用 await 和 signal 方法,否则会抛出 IllegalMonitorStateException 异常。

2.4、多 Condition 使用

代码如下:

package com.pany.camp.reentrantLock;import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;/**** @description:  多 Condition 使用* @copyright: @Copyright (c) 2022 * @company: Aiocloud* @author: pany* @version: 1.0.0 * @createTime: 2023-06-25 21:06*/
public class MultiConditionDemo {private static final ReentrantLock lock = new ReentrantLock();private static final Condition condition1 = lock.newCondition();private static final Condition condition2 = lock.newCondition();private static boolean flag = false;public static void main(String[] args) {Thread t1 = new Thread(() -> {lock.lock();try {while (!flag) {System.out.println("Thread 1 is waiting for condition 1.");condition1.await();}System.out.println("Thread 1 is notified for condition 1.");condition2.signal();} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}});Thread t2 = new Thread(() -> {lock.lock();try {flag = true;System.out.println("Thread 2 is notifying for condition 1.");condition1.signal();while (!flag) {System.out.println("Thread 2 is waiting for condition 2.");condition2.await();}System.out.println("Thread 2 is notified for condition 2.");} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}});t1.start();t2.start();}
}

我们创建了一个 ReentrantLock 对象和两个 Condition 对象。在线程 1 中,我们使用 while 循环来等待条件 flag 的满足,并在每次循环中使用 condition1.await() 方法来释放锁并等待通知。在线程 2 中,我们设置条件 flag 为 true,并使用 condition1.signal() 方法来通知线程 1,然后等待条件 flag2 的满足,使用 condition2.await() 方法来释放锁并等待通知。在线程 1 中,我们使用 condition2.signal() 方法来通知线程 2。

输出如下:

Thread 1 is waiting for condition 1.
Thread 2 is notifying for condition 1.
Thread 2 is notified for condition 2.
Thread 1 is notified for condition 1.Process finished with exit code 0

线程 1 在等待条件 flag1 满足时被阻塞,并在线程 2 通知后被唤醒。然后线程 1 通知线程 2 等待的条件 flag2,线程 2 在等待条件 flag2 满足时被阻塞,并在线程 1 通知后被唤醒。需要注意的是,在使用多个 Condition 实现等待/通知机制时,每个 Condition 都应该有对应的等待和通知逻辑,并且必须在获取锁之后才能使用 await 和 signal 方法,否则会抛出 IllegalMonitorStateException 异常。

2.5、公平锁

公平锁是一种锁的实现方式,它可以保证多个线程按照它们请求锁的顺序获取锁。也就是说,如果一个线程请求锁,但是锁已经被其他线程占用了,那么这个线程将被放置在一个等待队列中,直到它的请求被处理并且锁被释放。然后,等待队列中的下一个线程才能获取锁。这种方式可以保证线程获取锁的公平性,避免线程饥饿现象的发生。但是,公平锁的实现可能会导致性能下降,因为它需要维护等待队列并进行线程切换。因此,在实现锁时需要根据具体情况来选择公平锁或非公平锁。

以下就是公平锁的使用:

package com.pany.camp.reentrantLock;import java.util.concurrent.locks.ReentrantLock;/**** @description:  公平锁* @copyright: @Copyright (c) 2022 * @company: Aiocloud* @author: pany* @version: 1.0.0 * @createTime: 2023-06-25 21:09*/
public class FairLockDemo {private static final ReentrantLock lock = new ReentrantLock(true);public static void main(String[] args) {Thread t1 = new Thread(() -> {lock.lock();try {System.out.println("Thread 1 acquired the lock.");Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}});Thread t2 = new Thread(() -> {lock.lock();try {System.out.println("Thread 2 acquired the lock.");} finally {lock.unlock();}});t1.start();t2.start();}
}

2.6、可重入锁

可重入锁是一种线程同步机制,也称为递归锁。它允许线程多次获取同一个锁,而不会出现死锁的情况。当一个线程获取了锁后,它可以再次获取锁,而不会被阻塞。当线程释放锁时,它必须释放相同次数的锁,否则其他线程将无法获取锁。可重入锁通常用于解决递归函数或多个方法之间的互斥访问问题。Java 中的 ReentrantLock 就是一种可重入锁的实现。

以下是可重入锁的代码:

package com.pany.camp.reentrantLock;import java.util.concurrent.locks.ReentrantLock;/**** @description:  可重入锁* @copyright: @Copyright (c) 2022 * @company: Aiocloud* @author: pany* @version: 1.0.0 * @createTime: 2023-06-25 21:11*/
public class ReentrantLockDemo1 {private static final ReentrantLock lock = new ReentrantLock();public static void main(String[] args) {Thread t1 = new Thread(() -> {lock.lock();try {System.out.println("Thread 1 acquired the lock.");Thread.sleep(1000);lock.lock();try {System.out.println("Thread 1 acquired the lock again.");} finally {lock.unlock();}} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}});Thread t2 = new Thread(() -> {lock.lock();try {System.out.println("Thread 2 acquired the lock.");} finally {lock.unlock();}});t1.start();t2.start();}
}

3、究根问底 - ReentrantLock 源码分析

  1. 构造函数

ReentrantLock 的构造函数有两个重载版本,分别是无参构造函数和带公平性参数的构造函数。其中,公平性参数用于指定是否使用公平锁,默认为非公平锁。

public ReentrantLock() {sync = new NonfairSync();
}public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();
}
  1. 加锁与解锁

ReentrantLock 的加锁和解锁操作都是通过内部类 Sync 来实现的。Sync 是一个抽象类,它有两个子类 NonfairSync 和 FairSync,分别对应非公平锁和公平锁。

abstract static class Sync extends AbstractQueuedSynchronizer {// 加锁abstract void lock();// 解锁protected final boolean tryRelease(int releases) {int c = getState() - releases;if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;if (c == 0) {free = true;setExclusiveOwnerThread(null);}setState(c);return free;}
}static final class NonfairSync extends Sync {// 加锁final void lock() {if (compareAndSetState(0, 1))setExclusiveOwnerThread(Thread.currentThread());elseacquire(1);}// 尝试加锁protected final boolean tryAcquire(int acquires) {return nonfairTryAcquire(acquires);}
}static final class FairSync extends Sync {// 加锁final void lock() {acquire(1);}// 尝试加锁protected final boolean tryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}} else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0)throw new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;}
}

在 NonfairSync 中,加锁操作首先尝试使用 compareAndSetState 方法将 state 从 0 修改为 1,如果成功则表示获取锁成功,否则就调用 acquire 方法进入等待队列。在 FairSync 中,加锁操作直接调用 acquire 方法进入等待队列,如果前面有等待的线程,则当前线程会进入等待队列,直到前面所有线程都执行完毕后才会获取锁。

在解锁操作中,会判断当前线程是否为持有锁的线程,如果不是则抛出 IllegalMonitorStateException 异常,否则就将 state 减去 releases,并判断是否需要释放锁。如果 state 减去 releases 后等于 0,则表示当前线程已经释放了锁,需要将 exclusiveOwnerThread 置为 null。

  1. 其他方法

除了加锁和解锁操作之外,ReentrantLock 还提供了一些其他方法,如 tryLock、tryLock(long time, TimeUnit unit)、lockInterruptibly、newCondition 等。

其中,tryLock 方法用于尝试获取锁,如果成功则返回 true,否则返回 false。tryLock(long time, TimeUnit unit) 方法用于在指定的时间内尝试获取锁,如果成功则返回 true,否则返回 false。lockInterruptibly 方法用于可中断地获取锁,如果当前线程被中断则会抛出 InterruptedException 异常。newCondition 方法用于创建一个 Condition 对象,用于实现线程间的协作。

4、ReentrantLock 实际应用

ReentrantLock是Java中的一个锁实现类,它可以用于多线程并发控制,常用于以下场景:

  1. 临界区控制
    当多个线程需要同时访问共享资源时,需要使用锁来保证线程安全。ReentrantLock可以用于控制临界区的访问,避免多个线程同时访问共享资源导致的数据竞争和线程安全问题。

  2. 死锁避免
    ReentrantLock支持可重入性,即同一个线程可以多次获取该锁。这种特性可以避免死锁的发生,因为如果一个线程已经持有了该锁,再次获取该锁时不会被阻塞,而是直接返回。

  3. 公平锁控制
    ReentrantLock提供了公平锁和非公平锁两种模式。公平锁可以保证多个线程按照先后顺序获取锁,避免线程饥饿问题。非公平锁则不保证线程获取锁的顺序,可能会导致某些线程一直无法获取到锁。

  4. 条件变量控制

ReentrantLock还提供了Condition接口,可以用于实现线程间的协作。通过Condition接口的await()和signal()方法,可以实现线程的等待和唤醒操作,从而更加灵活地控制线程的执行顺序。

总之,ReentrantLock是一个非常实用的多线程并发控制工具,可以用于各种场景下的线程同步和互斥操作。

在这里插入图片描述

💕💕 本文由激流原创,首发于CSDN博客,博客主页 https://blog.csdn.net/qq_37967783?spm=1010.2135.3001.5421
💕💕喜欢的话记得点赞收藏啊


http://www.ppmy.cn/news/573013.html

相关文章

Ajax技术的秘密揭秘:异步传输,高效交互

文章目录 I. 什么是AjaxAjax的定义和起源Ajax与传统的Web应用程序之间的区别 II. Ajax的工作原理Ajax的基本原理Ajax如何通过异步传输实现无需刷新页面 III. Ajax的应用场景在Web应用程序中应用Ajax的优势Ajax在哪些场景中使用 IV. Ajax的组成部分和APIXHR对象FormData对象Fetc…

C# 中使用枚举转换时需要注意的坑点及解决方案

在使用枚举进行转换时&#xff0c;需要注意一些细节&#xff0c;否则可能会出现一些意外情况。本文将介绍一些在枚举转换中需要注意的坑点&#xff0c;并提供一些解决方案。 1、枚举从 int 值转换的坑 在将 int 值转换成枚举类型时&#xff0c;可能会遇到一些问题。即使 int …

nginx 前端及接口代理配置

以下为总结配置 我这一块配置为了习惯统一化 不管前端还是接口配置 location后面都带上斜杠。 前端代理配置 我比较常用的为alias方式 # 演示root和alias两种配置静态资源的区别server {listen 80;server_name localhost;# 用root方式&#xff0c;location中的路径会拼加到r…

给COS挂上nginx代理

目录 前言&#xff1a; 解决思路&#xff1a; Nginx代理配置 关键配置讲解&#xff1a; 附录 前言&#xff1a; 最近研发同学反馈本地无法连上线上测试的COS文件服务器。由于安全问题&#xff0c;研发同学连接公司内部服务都是通过自己的VPN&#xff1b;经过排查之后发现…

基于Docker MinIO整合Nginx搭建反向代理

基于Docker MinIO整合Nginx搭建反向代理 docker拉去镜像安装和配置就不说了 主要说一下配置反向代理 第一次使用minio我陷入了一个误区&#xff0c;将nginx的data目录挂载到了minio的文件目录&#xff0c;这样是可以通过nginx访问minio文件&#xff0c;但是没有任何意义&…

某医院nginx 前置机(反向代理)配置

数据流图 外网访问-http://13*.*.*.12*:8087/&#xff08;在出口做dnat&#xff09;----http://10.*.*.230:8087/&#xff08;前置机反向代理到内网&#xff09;----10.1.*.230:8087(内网) 在下配置在 10.*.*.230机器上进行配置&#xff1a; 一、安装nginx软件&#xff0c;目…

使用Nginx搭建反向代理

引言&#xff1a;最近公司有台服务器遭受DDOS攻击&#xff0c;流量在70M以上&#xff0c;由于服务器硬件配置较高所以不需要DDOS硬件防火墙。但我们要知道&#xff0c;IDC机房是肯定不 允许这种流量一直处于这么高的&#xff0c;因为没法具体知道后面陆续攻击的流量会有多大&am…

secureCRT设置跳板机 + proxyfier代理

背景 目前有三台以下机器: 本地windows 服务器1&#xff1a;132.121.11.71 服务器2&#xff1a;132.122.11.75 服务器1 能直接访问 服务器2&#xff1b; 但是本地不能直接访问 服务器2; 现在想让本地能直接访问 服务器2中的数据库。 工具 secureCRTproxyfier 操作 1. 跳板…