博主介绍: ✌博主从事应用安全和大数据领域,有8年研发经验,5年面试官经验,Java技术专家✌
Java知识图谱点击链接:体系化学习Java(Java面试专题)
💕💕 感兴趣的同学可以收藏关注下 ,不然下次找不到哟💕💕
1、ReentrantLock 简介
ReentrantLock 是 Java 中的一个锁实现,它提供了与 synchronized 关键字类似的功能,但是它更加灵活和可扩展。ReentrantLock 是一个可重入锁,也就是说同一个线程可以多次获得同一个锁而不会发生死锁。
ReentrantLock 的主要方法包括:
- lock():获取锁,如果锁已经被其他线程获取,则当前线程会被阻塞。
- unlock():释放锁。
- tryLock():尝试获取锁,如果锁已经被其他线程获取,则立即返回 false。
- tryLock(long timeout, TimeUnit unit):尝试获取锁,如果锁已经被其他线程获取,则当前线程会被阻塞,直到超时或者获取到锁。
- 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 源码分析
- 构造函数
ReentrantLock 的构造函数有两个重载版本,分别是无参构造函数和带公平性参数的构造函数。其中,公平性参数用于指定是否使用公平锁,默认为非公平锁。
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
- 加锁与解锁
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());
else
acquire(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。
- 其他方法
除了加锁和解锁操作之外,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中的一个锁实现类,它可以用于多线程并发控制,常用于以下场景:
临界区控制
当多个线程需要同时访问共享资源时,需要使用锁来保证线程安全。ReentrantLock可以用于控制临界区的访问,避免多个线程同时访问共享资源导致的数据竞争和线程安全问题。死锁避免
ReentrantLock支持可重入性,即同一个线程可以多次获取该锁。这种特性可以避免死锁的发生,因为如果一个线程已经持有了该锁,再次获取该锁时不会被阻塞,而是直接返回。公平锁控制
ReentrantLock提供了公平锁和非公平锁两种模式。公平锁可以保证多个线程按照先后顺序获取锁,避免线程饥饿问题。非公平锁则不保证线程获取锁的顺序,可能会导致某些线程一直无法获取到锁。条件变量控制
ReentrantLock还提供了Condition接口,可以用于实现线程间的协作。通过Condition接口的await()和signal()方法,可以实现线程的等待和唤醒操作,从而更加灵活地控制线程的执行顺序。
总之,ReentrantLock是一个非常实用的多线程并发控制工具,可以用于各种场景下的线程同步和互斥操作。
💕💕 本文由激流丶创作,原创不易,感谢支持!
💕💕喜欢的话记得点赞收藏啊!