ReentrantLock

在面对一些线程安全问题时,需要加入一把锁来进行防范,而 synchronized 就是这方面的代表之一。可能你会有所疑惑,既然有了 synchronized 这把锁,为什么JDK中还会有 Lock 的定义呢?其实这个还和 synchronized 的历史背景有关。

在 JDK 早期版本中,synchronized 关键字性能确实不佳,每一次加锁都需要和操作系统内核打交道,所以后来 JDK 作者索性自己写了一个Lock接口,减少和内核态之间的交道,降低这种性能的损耗。

但是自从 JDK1.6 以后,JDK 作者对 synchronized 进行了优化之后,synchronized 又开始被推荐使用,何况性能问题通过优化,大多都是可以解决的,所以性能并不是Lock接口出现的原因。

其实 Lock 接口至今一直被大众所喜爱,主要不在于说它的性能方面,而是它更加具备有灵活性。这一点我们从 Lock 的接口定义上可以看到明显的效果,具体代码如下所示:

public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}

通过这些接口函数的定义,我们可以发现 Lock 要比 synchronized 更具备一些灵活性,尤其是在它的以下几个特性方面:

  • 支持获取锁超时机制;

  • 支持非阻塞方式获取锁;

  • 支持可中断方式获取锁。

在JDK包中,Lock 实现类的代表莫过于是 ReentrantLock,下边我们重点以 ReentrantLock 为具体类进行深入介绍。

非阻塞方式获取锁

ReentrantLock 中提供了一个非阻塞方式获取锁的接口,名字叫 tryLock,其具体使用方式为:

public void tryLockMethod() {
if (reentrantLock.tryLock()) {
try {
i++;
} catch (Exception e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
} else {
//todo
}
}

这种使用方式在获取锁时如果失败了,则不会继续等待,而是会立马返回一个布尔状态值。这一点相比于 synchronized 关键字而言要灵活些,synchronized 关键字在抢夺锁失败之后,只能够进入一个等待队列中,而 tryLock 可以迅速告知调用方结果,从而进入对应的程序分支中进行处理。

锁超时机制

Lock 支持锁超时机制,这个功能要比单纯的 lock 函数更加强大,当获取锁超过一定时间,便会主动退出等待,例如下边这段案例:

public void tryLockMethod_2() {
try {
if (reentrantLock.tryLock(1, TimeUnit.SECONDS)) {
try {
i++;
} catch (Exception e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
} else {
//todo
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}

在这段代码中,通过调用 tryLock 方法,当等待锁超过 3 秒便会自动退出抢夺。要注意,由于是调用了 tryLock 函数,所以程序不一定会获取锁,因此在 finally 模块中,如果不确定当前线程依然是持有锁的状态,那么就需要在释放前先调用 isLocked 函数进行判断。通常使用 tryLock 的时候需要注意可能会导致活锁发生,所以睡眠时间可以采用随机值,更合适一些。

对可中断方式获取锁

我认为 ReentrantLock.lockInterruptibly 是 Lock 接口定义中非常特别的一个函数,它在使用的时候基本和 Lock#lock 函数是相同效果,使用的案例代码如下所示:

public void lockInterruptiblyMethod() {
try {
reentrantLock.lockInterruptibly();
i++;
} catch (Exception e) {
e.printStackTrace();
} finally {
if (reentrantLock.isLocked()) {
reentrantLock.unlock();
}
}
}

lockInterruptibly 允许在等待时,由其它线程调用等待线程的 Thread.interrupt 方法来中断等待线程的等待,这一点和 Lock.lock 函数以及 synchronized 关键字是有所不同的。下边让我们通过一个实战案例来理解这几种方法在加锁过程中,遇到线程中断之后会有什么不同的表现。

package 并发编程04.锁中断案例;
import org.junit.jupiter.api.Test;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Author idea
* @Date created in 8:42 上午 2022/6/4
*/
public class LockInterruptDemo {
static class LockInterruptThread_1 implements Runnable {
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
try {
//外界可以中断这里面的处理
lock.lockInterruptibly();
try {
System.out.println("enter");
long startTime = System.currentTimeMillis();
for (; ; ) {
if (System.currentTimeMillis() - startTime >= 5000) {
break;
}
}
System.out.println("end");
} catch (Exception e) {
e.printStackTrace();
} finally {
if(lock.isLocked()){
lock.unlock();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
static class LockInterruptThread_2 implements Runnable {
@Override
public void run() {
synchronized (this) {
System.out.println("enter");
long startTime = System.currentTimeMillis();
for (; ; ) {
if (System.currentTimeMillis() - startTime >= 5000) {
break;
}
}
System.out.println("end");
}
}
}
static class LockInterruptThread_3 implements Runnable {
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
lock.lock();
try {
System.out.println("enter");
long startTime = System.currentTimeMillis();
for (; ; ) {
if (System.currentTimeMillis() - startTime >= 5000) {
break;
}
}
System.out.println("end");
} catch (Exception e) {
e.printStackTrace();
} finally {
if(lock.isLocked()){
lock.unlock();
}
}
}
}
@Test
public void testLockInterruptThread_1() throws InterruptedException {
LockInterruptThread_1 lockInterruptThread_1 = new LockInterruptThread_1();
//使用ReentrantLock#lockInterruptibly 在没有获取到锁处于等待过程中是可以被中断的。
Thread t1 = new Thread(lockInterruptThread_1);
Thread t2 = new Thread(lockInterruptThread_1);
t1.start();
t2.start();
Thread.sleep(200);
System.out.println("开始触发中断操作");
t2.interrupt();
System.out.println("发起了中断操作");
}
@Test
public void testLockInterruptThread_2() throws InterruptedException {
LockInterruptThread_2 lockInterruptThread_2 = new LockInterruptThread_2();
//使用synchronized关键字 在没有获取到锁处于等待过程中是无法随意中断的。
Thread t1 = new Thread(lockInterruptThread_2);
Thread t2 = new Thread(lockInterruptThread_2);
t1.start();
t2.start();
Thread.sleep(200);
System.out.println("开始触发中断操作");
t2.interrupt();
System.out.println("发起了中断操作");
}
@Test
public void testLockInterruptThread_3() throws InterruptedException {
LockInterruptThread_3 lockInterruptThread_3 = new LockInterruptThread_3();
//使用synchronized关键字 在没有获取到锁处于等待过程中是无法随意中断的。
Thread t1 = new Thread(lockInterruptThread_3);
Thread t2 = new Thread(lockInterruptThread_3);
t1.start();
t2.start();
Thread.sleep(200);
System.out.println("开始触发中断操作");
t2.interrupt();
System.out.println("发起了中断操作");
}
}

首先我们可以将这段代码执行一下,看看对应的效果。

下边是三种不同的测试用例在执行之后的效果:

img

img

img

通过实验结果可以发现,在多线程场景下,使用 lock函数和synchronized 关键字的代码块在尝试获取锁的过程中,如果获取失败了,就会进入等待队列中等待,而此时使用 Thread.interupt 是无法直接中断在等待状态中的线程的。

但是对于使用 lockInterruptibly 方法的线程来说,是可以在 Thread.interupt 调用之后立马进入到中断状态的。

我做一些简单的归纳,主要如下所示:

  • Lock 类中提供了 tryLock() 函数,这个方法支持线程以非阻塞性的方式去获取锁 ,如果获取失败则立马返回结果;

  • Lock 类中提供了 tryLock(long timeout, TimeUnit unit) 函数,这个方法支持锁的超时机制,当获取锁的时候会先进入等待状态,当等待时间达到预期之后才会返回结果。

  • lockInterruptibly 是一个支持线程中断的函数,当线程在执行该函数之后会进入等待状态,在等待的过程中是可以通过 Thread.interupt 函数去进行中断的。

posted @   Dazzling!  阅读(30)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
点击右上角即可分享
微信分享提示