java ReentrantLock并发锁使用详解

2022-10-13 451阅读

温馨提示:这篇文章已超过554天没有更新,请注意相关的内容是否还可用!

这篇文章主要为大家介绍了java ReentrantLock并发锁使用详解,有需要的朋友可以借鉴参考下,希望能够有所帮助!

一、ReentrantLock是什么

ReentrantLock是一种基于AQS框架的应用实现,是JDK中的一种线程并发访问的同步手段,它的功能类似于synchronized是一种互斥锁,可以保证线程安全。 相对于 synchronized, ReentrantLock具备如下特点:

  • 可中断

  • 可以设置超时时间

  • 可以设置为公平锁

  • 支持多个条件变量

  • 与 synchronized 一样,都支持可重入

进入源码可以看到,其实现了公平锁和非公平锁

内部实现了加锁的操作,并且支持重入锁。不用我们再重写

解锁操作

1-1、ReentrantLock和synchronized区别

synchronized和ReentrantLock的区别:

  • synchronized是JVM层次的锁实现,ReentrantLock是JDK层次的锁实现;

  • synchronized的锁状态是无法在代码中直接判断的,但是ReentrantLock可以通过ReentrantLock#isLocked判断;

  • synchronized是非公平锁,ReentrantLock是可以是公平也可以是非公平的;

  • synchronized是不可以被中断的,而ReentrantLock#lockInterruptibly方法是可以被中断的;

  • 在发生异常时synchronized会自动释放锁,而ReentrantLock需要开发者在finally块中显示释放锁;

  • ReentrantLock获取锁的形式有多种:如立即返回是否成功的tryLock(),以及等待指定时长的获取,更加灵活;

  • synchronized在特定的情况下对于已经在等待的线程是后来的线程先获得锁(回顾一下sychronized的唤醒策略),而ReentrantLock对于已经在等待的线程是先来的线程先获得锁;

1-2、ReentrantLock的使用

1-2-1、ReentrantLock同步执行,类似synchronized

使用ReentrantLock需要注意的是:一定要在finally中进行解锁,方式业务抛出异常,无法解锁

public class ReentrantLockDemo {
private static  int sum = 0;
private static ReentrantLock lock=new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 3; i++) {
Thread thread = new Thread(()->{
//加锁
lock.lock();
try {
// 临界区代码
// TODO 业务逻辑:读写操作不能保证线程安全
for (int j = 0; j < 10000; j++) {
sum++;
}
} finally {
// 解锁--一定要在finally中解锁,防止业务代码异常,无法释放锁
lock.unlock();
}
});
thread.start();
}
Thread.sleep(2000);
System.out.println(sum);
}
}

测试结果:

1-2-2、可重入锁

可重入锁就是 A(加锁)-->调用--->B(加锁)-->调用-->C(加锁),从A到C即使B/C都有加锁,也可以进入

@Slf4j
public class ReentrantLockDemo2 {
public 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();
}
}
}

执行结果:

1-2-3、锁中断

可以使用lockInterruptibly来进行锁中断 lockInterruptibly()方法能够中断等待获取锁的线程。当两个线程同时通过lock.lockInterruptibly()获取某个锁时,假若此时线程A获取到了锁,而线程B只有等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。

public class ReentrantLockDemo3 {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
log.debug("t1启动...");
try {
lock.lockInterruptibly();
try {
log.debug("t1获得了锁");
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("t1等锁的过程中被中断");
}
}, "t1");
lock.lock();
try {
log.debug("main线程获得了锁");
t1.start();
//先让线程t1执行
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t1.interrupt();
log.debug("线程t1执行中断");
} finally {
lock.unlock();
}
}
}

执行结果:

1-2-4、获得锁超时失败

可以让线程等待指定的时间,如果还未获取锁则进行失败处理。 如下代码,首先让主线程获得锁,然后让子线程启动尝试获取锁,但是由于主线程获取锁之后,让线程等待了2秒,而子线程获得锁的超时时间只有1秒,如果未获得锁,则进行return失败处理

public class ReentrantLockDemo4 {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
log.debug("t1启动...");
//超时
try {
if (!lock.tryLock(1, TimeUnit.SECONDS)) {
log.debug("等待 1s 后获取锁失败,返回");
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
return;
}
try {
log.debug("t1获得了锁");
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
try {
log.debug("main线程获得了锁");
t1.start();
//先让线程t1执行
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
lock.unlock();
}
}
}

执行结果:

1-2-5、公平锁

ReentrantLock 默认是不公平的

首先启动500次for循环创建500个线程,然后进行加锁操作,并同时启动了。这样这500个线程就依次排队等待加锁的处理 下面500个线程也是等待加锁操作 如果使用公平锁,下面500的线程只有等上面500个线程运行完成之后才能获得锁。

@Slf4j
public class ReentrantLockDemo5 {
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock(true); //公平锁
for (int i = 0; i < 500; i++) {
new Thread(() -> {
lock.lock();
try {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug(Thread.currentThread().getName() + " running...");
} finally {
lock.unlock();
}
}, "t" + i).start();
}
// 1s 之后去争抢锁
Thread.sleep(1000);
for (int i = 0; i < 500; i++) {
new Thread(() -> {
lock.lock();
try {
log.debug(Thread.currentThread().getName() + " running...");
} finally {
lock.unlock();
}
}, "强行插入" + i).start();
}
}
}

测试结果(后进入的线程都在等待排队)

使用非公平锁的情况下,就可以看到下面500线程有些线程就可以抢占锁了

那ReentrantLock为什么默认使用非公平锁呢?实际上就是为了提高性能,如果使用公平锁,当前锁对象释放之后,还需要去队列中获取第一个排队的线程,然后进行加锁处理。而非公平锁,可能再当前对象释放锁之后,正好有新的线程在获取锁,这样就可以直接进行加锁操作,不必再去队列中读取。