0
点赞
收藏
分享

微信扫一扫

ReentrantLock介绍及使用(超详细)


目录

​​一、介绍​​

​​1. 简介​​

​​2. 是什么类型的锁​​

​​3. 优点​​

​​4. 原理​​

​​5. 主要方法​​

​​6. 使用时注意事项​​

​​二、实际应用​​

​​1. 案例一​​

​​2. 案例二​​

一、介绍

1. 简介

    ReentrantLock是一种基于​​AQS​​(Abstract Queued Synchronizer)框架的应用实现,是JDK中一种线程并发访问的同步手段,它的功能类似于synchronized是一种互斥锁,可以保证线程安全。

2. 是什么类型的锁

    (1) 公平锁或非公平锁(下面案例中有实际代码,自己执行一遍更容易理解)

    a. 公平锁:先来的线程先执行,排成排按顺序。

  • 优点:所有的线程都能得到资源,不会饿死在队列中。
  • 缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。

    b. 非公平锁:后来的线程有可能先执行,可插队不一定按顺序。

  • 优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
  • 缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。

    (2) 互斥锁

    一次只能执行一个线程。

    (3) 可重入锁

    同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。

    比较抽象,解释下:假如景区有3个收费项目,如果每个项目单独玩都需要收费。但是你买了个VIP。进入景区大门的时候工作人员为了方便,直接给你挂了个牌子,里面三个项目的工作人员看到你的牌子,就认为你已经买过该项目的门票,不在向你收费,进去就行。

3. 优点

    (1) 可中断并且可以设置超时时间。

    (2) 可以根据业务场景使用公平锁或非公平锁。

    (3) 获取锁可设置超时。

    (4) 可绑定多个条件(Condition)。

   关于他的优点肯定是和其他锁比较得来的,一般都是和synchronized(synchronized介绍及使用 这篇文章稍后上传)比较。

4. 原理

    ReentrantLock是怎么实现锁同步的呢?咱们先看代码:

ReentrantLock介绍及使用(超详细)_开发语言

ReentrantLock介绍及使用(超详细)_公平锁_02

ReentrantLock介绍及使用(超详细)_加锁_03

ReentrantLock介绍及使用(超详细)_加锁_04

ReentrantLock介绍及使用(超详细)_开发语言_05

   点来点去发现,具体的加锁逻辑的实现在AQS这个抽象类中,所以想要了解其实现原理,还得看看AbstractQueuedSynchronizer(AQS详解 这篇文章稍后上传)

5. 主要方法

    getHoldCount():当前线程调用 lock() 方法的次数。

    getQueueLength():当前正在等待获取 Lock 锁的线程的估计数。

    getWaitQueueLength(Condition condition):当前正在等待状态的线程的估计数,需要传入 Condition 对象。

    hasWaiters(Condition condition):查询是否有线程正在等待与 Lock 锁有关的 Condition 条件。

    hasQueuedThread(Thread thread):查询指定的线程是否正在等待获取 Lock 锁。

    hasQueuedThreads():查询是否有线程正在等待获取此锁定。

    isFair():判断当前 Lock 锁是不是公平锁。

    isHeldByCurrentThread():查询当前线程是否保持此锁定。

    isLocked():查询此锁定是否由任意线程保持。

    tryLock():线程尝试获取锁,如果获取成功,则返回 true,如果获取失败(即锁已被其他线程获取),则返回 false。

    tryLock(long timeout,TimeUnit unit):线程如果在指定等待时间内获得了锁,就返回true,否则返回 false。

    lockInterruptibly():如果当前线程未被中断,则获取该锁定,如果已经被中断则出现异常。

    方法详细介绍可以看最下面参考文章:ReentrantLock类中的方法

6. 使用时注意事项

    (1) 高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。

说明:尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用RPC方法。

    (2) 对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁。

  说明:线程一需要对表A、B、C依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序也必须是A、B、C,否则可能出现死锁。

    (3) 在使用阻塞等待获取锁的方式中,必须在try代码块之外,并且在加锁方法与try代码块之间没有任何可能抛出异常的方法调用,避免加锁成功后,在finally中无法解锁。

    a. 在lock方法与try代码块之间的方法调用抛出异常,无法解锁,造成其它线程无法成功获取锁。

    b. 如果lock方法在try代码块之内,可能由于其它方法抛出异常,导致在finally代码块中,unlock对未加锁的对象解锁,它会调用AQS的tryRelease方法(取决于具体实现类),抛出IllegalMonitorStateException异常。

    c. 在Lock对象的lock方法实现中可能抛出unchecked异常,产生的后果与说明二相同。

    d. 在使用尝试机制来获取锁的方式中,进入业务代码块之前,必须先判断当前线程是否持有锁。锁的释放规则与锁的阻塞等待方式相同。

说明:Lock对象的unlock方法在执行时,它会调用AQS的tryRelease方法(取决于具体实现类),如果当前线程不持有锁,则抛出IllegalMonitorStateException异常。

上面就是阿里规约中对锁的使用一些注意事项,感兴趣可以学习下!​​阿里巴巴编码规范学习及应用​​

二、实际应用

    某个方法或者共享变量不能被多个线程同时操作,可以用ReentrantLock进行加锁。当然这只是其中的一种解决方式。

注意:使用之前你要知道,使用锁的话执行会相对较慢,因为在加锁的代码块内,每次只能执行一个线程。举个栗子(简单看看就行。​​git地址​​):

public static void main(String[] args) {
// 设置执行次数
int executeCount = 2;
// 定义方法开始时间,单位: ms
long startTime = System.currentTimeMillis();
// java.util.concurrent提供的API,在该案例中主要是为了让主线程等待子线程结束后进行打印
CountDownLatch countDownLatch = new CountDownLatch(executeCount);

ReentrantLock lock = new ReentrantLock();
for (int index = 0; index < executeCount; index++) {
new Thread(() -> {
LOGGER.info("current thread name: {} start.", Thread.currentThread().getName());
// 加锁
lock.lock();
try {
// 休眠1秒
sleep(1);
} finally {
// 一定要在finally解锁
lock.unlock();
}

/// 休眠1秒,如果测试不加锁耗时可将注释打开并对上面加锁逻辑进行注释
// sleep(1);
LOGGER.info("current thread name: {} end.", Thread.currentThread().getName());
countDownLatch.countDown();
}).start();
}

try {
// 主线程等待子线程结束后打印log信息
countDownLatch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}

LOGGER.info("cost time: {}ms", System.currentTimeMillis() - startTime);
}

通过上面的案例可知:加锁耗时2075ms,不加锁耗时1058ms。这有点耗时啊,所以如果必须加锁的话一定要把锁的范围控制在最小,避免浪费太多时间。但是这个加锁方式有个小问题,什么问题呢?我们继续往下看!

1. 案例一

(1) 场景:

    王涛是公司的开发人员,他写了一个远程调用的方法,并且在调用这个方法前加了锁,调用之后解锁。他在本地测试没啥问题,所以就上线了。突然有一天很多客户点击王涛写的方法,结果发现一直加载中...没有任何提示。这线上问题一发生,搞的王涛和经理连夜加班找bug,之后进行如下修改。

(2) 代码:​​git地址​​

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

/**
* ReentrantLockCase1
* 使用ReentrantLock调用方法超时处理
*
* @author wxy
* @date 2023-02-16
*/
public class ReentrantLockCase1 {
private static final Logger LOGGER = LoggerFactory.getLogger(ReentrantLockCase1.class);

public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
for (int index = 0; index < 2; index++) {
new Thread(() -> {
/*---修改前代码---*/
/*try {
lock.lock();
// 调用某个方法,这个方法会超时60s(偶现)
timeoutApi();
} finally {
lock.unlock();
}*/

/*---修改后代码---*/
try {
// 设置如果线程1正在调用,线程2等待5秒,5秒后你可以对线程2进行处理: 比如返回提示、线程处理...
// 如果你不设置超时时间,那么所有的线程就会等待前一个线程解锁,具体怎么等待请看AQS详解
// 备注: 正常情况下超时时间应该在配置文件中配置,可以按照业务随时进行调整
if (lock.tryLock(5, TimeUnit.SECONDS)) {
// 调用某个方法,这个方法会超时60s(偶现)
timeoutApi();
} else {
/// 你可以写一些业务逻辑,来处理超时的线程2和超时期间的后续线程
LOGGER.info("operation timeout");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 一定要在finally解锁
lock.unlock();
}
}).start();
}
}

/**
* 超时的API
*/
private static void timeoutApi() {
LOGGER.info("timeout api start.");
sleep(60);
LOGGER.info("timeout api end.");
}

/**
* 设置超时时间
*
* @param timeOut 超时时间(秒)
*/
private static void sleep(long timeOut) {
try {
TimeUnit.SECONDS.sleep(timeOut);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}

    当然,这是一种处理方式,还有其他的处理方式,欢迎评论区留言。

2. 案例二

(1) 场景

    写了一下公平锁和非公平锁在执行过程中的打印。

(2) 代码:​​git地址​​

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.locks.ReentrantLock;

/**
* ReentrantLockCase2
* 演示公平锁和非公平锁区别
*
* @author wxy
* @date 2023-02-16
*/
public class ReentrantLockCase2 {
private static final Logger LOGGER = LoggerFactory.getLogger(ReentrantLockCase1.class);

public static void main(String[] args) {
/*
公平锁,执行结果:
Thread-1, num : 1
Thread-0, num : 1
Thread-3, num : 1
Thread-2, num : 1
Thread-4, num : 1
Thread-1, num : 2
Thread-0, num : 2
Thread-3, num : 2
Thread-2, num : 2
Thread-4, num : 2

5个线程很有规律,你一次我一次。
*/
ReentrantLock lock = new ReentrantLock(true);

/*
非公平锁,执行结果:
Thread-0, num : 1
Thread-0, num : 2
Thread-1, num : 1
Thread-1, num : 2
Thread-2, num : 1
Thread-2, num : 2
Thread-3, num : 1
Thread-3, num : 2
Thread-4, num : 1
Thread-4, num : 2

一个线程执行完毕后,下一个线程才能开始执行。为什么出现这样的情况呢?
非公平锁就是谁能抢谁先来,由于Thread-0线程刚执行完一次,线程还处于活跃状态,
而其他线程需要被唤醒才能执行,所以相对于其他线程来说他更活跃,所以再次抢到锁并执行。
*/
// ReentrantLock lock = new ReentrantLock(false);
for (int index = 0; index < 5; index++) {
new Thread(() -> {
for (int num = 1; num <= 2; num++) {
lock.lock();
try {
LOGGER.info("thread name: {}, num : {}", Thread.currentThread().getName(), num);
} finally {
lock.unlock();
}
}
}).start();
}
}
}

参考文章

    1. ​​ReentrantLock类中的方法​​

举报

相关推荐

0 条评论