ReentrantLock(重入锁)功能详解和应用演示
Posted eternityz
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ReentrantLock(重入锁)功能详解和应用演示相关的知识,希望对你有一定的参考价值。
目录
- 1. ReentrantLock简介
- 2.ReentrantLock和synchronized的比较
- 3.ReentrantLock相比synchronized的额外功能
- 4.结合Condition实现等待通知机制
- 5. 总结
一.ReentrantLock简介
jdk中独占锁的实现除了使用关键字synchronized外,还可以使用ReentrantLock。虽然在性能上ReentrantLock和synchronized没有什么区别,但ReentrantLock相比synchronized而言功能更加丰富,使用起来更为灵活,也更适合复杂的并发场景。
接口:
Modifier and Type | Method | Description |
---|---|---|
void | lock() | 获取锁 |
void | lockInterruptibly() | 除非当前线程被中断,否则获取锁定 |
Condition | newCondition() | 返回绑定到此Lock实例的新Condition实例 |
boolean | tryLock() | 只有在调用时它是空闲的才能获取锁 |
boolean | tryLock(long time, TimeUnit unit) | 如果在给定的等待时间内空闲并且当前线程未被中断,则获取锁 |
void | unlock() | 释放锁 |
二.ReentrantLock和synchronized的比较 ↑top
1.相同点 ↑top
- a.它们都是加锁方式同步;
- b.都是重入锁;
- c.阻塞式的同步;也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的
可重入性
public class ReentrantLockTest {
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
for (int i = 1; i <= 3; i++) {
lock.lock();
}
for(int i=1;i<=3;i++){
try {
} finally {
lock.unlock();
}
}
}
}
上面的代码通过lock()
方法先获取锁三次,然后通过unlock()
方法释放锁3次,程序可以正常退出。
从上面的例子可以看出,ReentrantLock是可以重入的锁,当一个线程获取锁时,还可以接着重复获取多次。在加上ReentrantLock的的独占性,我们可以得出以下ReentrantLock和synchronized的相同点。
- 1.ReentrantLock和synchronized都是独占锁,只允许线程互斥的访问临界区。但是实现上两者不同:synchronized加锁解锁的过程是隐式的,用户不用手动操作,优点是操作简单,但显得不够灵活。一般并发场景使用synchronized的就够了;ReentrantLock需要手动加锁和解锁,且解锁的操作尽量要放在finally代码块中,保证线程正确释放锁。ReentrantLock操作较为复杂,但是因为可以手动控制加锁和解锁过程,在复杂的并发场景中能派上用场。
- 2.ReentrantLock和synchronized都是可重入的。synchronized因为可重入因此可以放在被递归执行的方法上,且不用担心线程最后能否正确释放锁;而ReentrantLock在重入时要却确保重复获取锁的次数必须和重复释放锁的次数一样,否则可能导致其他线程无法获得该锁。
2.不同点 ↑top
比较方面 | SynChronized | ReentrantLock(实现了 Lock接口) |
---|---|---|
原始构成 | 它是java语言的关键字,是原生语法层面的互斥,需要jvm实现 | 它是JDK 1.5之后提供的API层面的互斥锁类 |
代码编写 | 采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用,更安全, | 而ReentrantLock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。需要lock()和unlock()方法配合try/finally语句块来完成, |
灵活性 | 锁的范围是整个方法或synchronized块部分 | Lock因为是方法调用,可以跨方法,灵活性更大 |
等待可中断 | 不可中断,除非抛出异常(释放锁方式: 1.代码执行完,正常释放锁; 2.抛出异常,由JVM退出等待 |
持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,(方法: 1.设置超时方法 tryLock(long timeout, TimeUnit unit),时间过了就放弃等待; 2.lockInterruptibly()放代码块中,调用interrupt()方法可中断,而synchronized不行) |
是否公平锁 | 非公平锁 | 两者都可以,默认公平锁,构造器可以传入boolean值,true为公平锁,false为非公平锁, |
条件Condition | 通过多次newCondition可以获得多个Condition对象,可以简单的实现比较复杂的线程同步的功能. | |
提供的高级功能 | 提供很多方法用来监听当前锁的信息,如: getHoldCount() getQueueLength() isFair() isHeldByCurrentThread() isLocked() |
三. ReentrantLock相比synchronized的额外功能 ↑top
3.1 ReentrantLock可以实现公平锁。 ↑top
公平锁是指当锁可用时,在锁上等待时间最长的线程将获得锁的使用权。而非公平锁则随机分配这种使用权。和synchronized一样,默认的ReentrantLock实现是非公平锁,因为相比公平锁,非公平锁性能更好。当然公平锁能防止饥饿,某些情况下也很有用。在创建ReentrantLock的时候通过传进参数true
创建公平锁,如果传入的是false
或没传参数则创建的是非公平锁
ReentrantLock lock = new ReentrantLock(true);
继续跟进看下源码
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
可以看到公平锁和非公平锁的实现关键在于成员变量sync
的实现不同,这是锁实现互斥同步的核心。
公平锁例子:
public class ReentrantLockTest {
static Lock lock = new ReentrantLock(true);
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<5;i++){
new Thread(new ThreadDemo(i)).start();
}
}
static class ThreadDemo implements Runnable {
Integer id;
public ThreadDemo(Integer id) {
this.id = id;
}
@Override
public void run() {
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int i=0;i<2;i++){
lock.lock();
System.out.println("获得锁的线程:"+id);
lock.unlock();
}
}
}
}
公平锁结果:
获得锁的线程:3
获得锁的线程:2
获得锁的线程:4
获得锁的线程:0
获得锁的线程:1
获得锁的线程:3
获得锁的线程:2
获得锁的线程:4
获得锁的线程:0
获得锁的线程:1
我们开启5个线程,让每个线程都获取释放锁两次。为了能更好的观察到结果,在每次获取锁前让线程休眠100毫秒。可以看到线程几乎是轮流的获取到了锁。如果我们改成非公平锁,再看下结果:
获得锁的线程:2
获得锁的线程:2
获得锁的线程:4
获得锁的线程:4
获得锁的线程:3
获得锁的线程:3
获得锁的线程:1
获得锁的线程:1
获得锁的线程:0
获得锁的线程:0
线程会重复获取锁。如果申请获取锁的线程足够多,那么可能会造成某些线程长时间得不到锁。这就是非公平锁的“饥饿”问题。
- 公平锁和非公平锁该如何选择
大部分情况下我们使用非公平锁,因为其性能比公平锁好很多。但是公平锁能够避免线程饥饿,某些情况下也很有用。
3.2ReentrantLock可响应中断 ↑top
当使用synchronized实现锁时,阻塞在锁上的线程除非获得锁否则将一直等待下去,也就是说这种无限等待获取锁的行为无法被中断。而ReentrantLock给我们提供了一个可以响应中断的获取锁的方法lockInterruptibly()
。该方法可以用来解决死锁问题。
public class ReentrantLockDemo02 implements Runnable {
private Lock lock = new ReentrantLock();
@Override
public void run() {
try {
lock.lockInterruptibly();
System.out.println(Thread.currentThread().getName() + " running");
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + " finished");
lock.unlock();
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " interrupted");
}
}
public static void main(String[] args) {
ReentrantLockDemo02 reentrantLockDemo = new ReentrantLockDemo02();
Thread thread01 = new Thread(reentrantLockDemo, "thread01");
Thread thread02 = new Thread(reentrantLockDemo, "thread02");
thread01.start();
thread02.start();
thread02.interrupt();
}
}
结果:
thread01 running
thread02 interrupted
thread01 finished
3.3ReentrantLock获取锁限时等待 ↑top
ReentrantLock还给我们提供了获取锁限时等待的方法tryLock()
,可以选择传入时间参数,表示等待指定的时间,无参则表示立即返回锁申请的结果:true表示获取锁成功,false表示获取锁失败。我们可以使用该方法配合失败重试机制来更好的解决死锁问题。
public class ReentrantLockDemo03 implements Runnable {
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
try {
if (lock.tryLock(2, TimeUnit.SECONDS)) {
System.out.println(Thread.currentThread().getName() + " 获取当前lock锁");
TimeUnit.SECONDS.sleep(4);
} else {
System.out.println(Thread.currentThread().getName()+ " 获取锁失败");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
public static void main(String[] args) {
ReentrantLockDemo03 reentrantLockDemo = new ReentrantLockDemo03();
Thread thread01 = new Thread(reentrantLockDemo, "thread01");
Thread thread02 = new Thread(reentrantLockDemo, "thread02");
thread01.start();
thread02.start();
}
}
运行结果:
thread02 获取当前lock锁
thread01 获取锁失败
线程2运行,首先获得锁,休眠4秒钟后释放所,线程1如果在2秒内如果没有获取到锁,则会输出获取锁失败
如果将线程2的休眠时间小于1秒或者线程1的等待时间大于4秒此时两者都会成功
thread02 获取当前lock锁
thread01 获取当前lock锁
四. 结合Condition实现等待通知机制 ↑top
4.1 Condition使用简介 ↑top
Condition由ReentrantLock对象创建,并且可以同时创建多个
static Condition notEmpty = lock.newCondition();
static Condition notFull = lock.newCondition();
Condition接口在使用前必须先调用ReentrantLock的lock()方法获得锁。之后调用Condition接口的await()将释放锁,并且在该Condition上等待,直到有其他线程调用Condition的signal()方法唤醒线程。使用方式和wait,notify类似。
ublic class ConditionTest {
static ReentrantLock lock = new ReentrantLock();
static Condition condition = lock.newCondition();
public static void main(String[] args) throws InterruptedException {
lock.lock();
new Thread(new SignalThread()).start();
System.out.println("主线程等待通知");
try {
condition.await();
} finally {
lock.unlock();
}
System.out.println("主线程恢复运行");
}
static class SignalThread implements Runnable {
@Override
public void run() {
lock.lock();
try {
condition.signal();
System.out.println("子线程通知");
} finally {
lock.unlock();
}
}
}
}
运行结果:
主线程等待通知
子线程通知
主线程恢复运行
4.2 使用Condition实现简单的阻塞队列 ↑top
阻塞队列是一种特殊的先进先出队列,它有以下几个特点
1.入队和出队线程安全
2.当队列满时,入队线程会被阻塞;当队列为空时,出队线程会被阻塞。
public class MyBlockingQueue<E> {
int size;//阻塞队列最大容量
ReentrantLock lock = new ReentrantLock();
LinkedList<E> list=new LinkedList<>();//队列底层实现
Condition notFull = lock.newCondition();//队列满时的等待条件
Condition notEmpty = lock.newCondition();//队列空时的等待条件
public MyBlockingQueue(int size) {
this.size = size;
}
public void enqueue(E e) throws InterruptedException {
lock.lock();
try {
while (list.size() ==size)//队列已满,在notFull条件上等待
notFull.await();
list.add(e);//入队:加入链表末尾
System.out.println("入队:" +e);
notEmpty.signal(); //通知在notEmpty条件上等待的线程
} finally {
lock.unlock();
}
}
public E dequeue() throws InterruptedException {
E e;
lock.lock();
try {
while (list.size() == 0)//队列为空,在notEmpty条件上等待
notEmpty.await();
e = list.removeFirst();//出队:移除链表首元素
System.out.println("出队:"+e);
notFull.signal();//通知在notFull条件上等待的线程
return e;
} finally {
lock.unlock();
}
}
}
测试代码:
public static void main(String[] args) throws InterruptedException {
MyBlockingQueue<Integer> queue = new MyBlockingQueue<>(2);
for (int i = 0; i < 10; i++) {
int data = i;
new Thread(new Runnable() {
@Override
public void run() {
try {
queue.enqueue(data);
} catch (InterruptedException e) {
}
}
}).start();
}
for(int i=0;i<10;i++){
new Thread(new Runnable() {
@Override
public void run() {
try {
Integer data = queue.dequeue();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
运行结果:
入队:3
出队:3
入队:4
入队:8
出队:4
入队:1
出队:8
入队:0
出队:1
出队:0
入队:9
出队:9
入队:2
入队:5
出队:2
出队:5
入队:7
入队:6
出队:7
出队:6
五.总结 ↑top
ReentrantLock是可重入的独占锁。比起synchronized功能更加丰富,支持公平锁实现,支持中断响应以及限时等待等等。可以配合一个或多个Condition条件方便的实现等待通知机制。
什么情况下使用ReenTrantLock:
答案是,如果你需要实现ReenTrantLock的三个独有功能时。
参考:
主参考: https://www.cnblogs.com/takumicx/p/9338983.html
辅助参考:
- https://blog.csdn.net/u014730165/article/details/82144848
- https://blog.csdn.net/weixin_40792878/article/details/81369385
详细解释:
- https://www.cnblogs.com/zhimingyang/p/5702752.html
- https://www.jianshu.com/p/c021f144a565
- https://blog.csdn.net/wuyangyang555/article/details/80984428
以上是关于ReentrantLock(重入锁)功能详解和应用演示的主要内容,如果未能解决你的问题,请参考以下文章