Java中的锁 重入锁ReentrantLock
简介
重入锁ReentrantLock指的是支持同一个线程对资源的重复加锁。ReentrantLock中有公平锁和非公平锁的两种实现。
synchronized
synchronized关键字支持隐式的重入;当一个线程获取到锁时,是支持这个线程多次获取这个锁的,不会出现自己阻塞自己的情况,并且我们开发过程中对于synchronized关键字也不需要关心锁的释放。举个递归的例子我们来看synchronized关键字对锁的重入。
代码示例
package com.lizba.p6;
/**
* <p>
* synchronized锁重入测试
* </p>
*
* @Author: Liziba
* @Date: 2021/6/21 21:45
*/
public class SynchronizedTest {
public static void main(String[] args) {
int sum = cal(0);
System.out.println(sum);
}
/**
* 简单递归重入,递归十次
* @param i
* @return
*/
private static synchronized int cal(int i) {
if (i < 10) {
return cal(++i);
}
return i;
}
}
输出结果为10,在这个过程中main线程重复进入synchronized关键字修饰的cal(int i)方法。因此也证明了上面的说法正确。
ReentrantLock
ReentrantLock基于AQS和Lock来实现的,那如果是我们ReentrantLock要实现可重入,需要解决和实现如下两个问题:
- 同一个线程多次获取锁,则需要判断当前来获取锁的线程和占有锁的线程是否为同一个线程
- 多次获取锁,则需要多次释放这个锁,可以通过一个计数器累加和自减来记录锁的重复获取与释放
ReentrantLock中有两种重入锁的实现,分别是:
- NonfairSync-非公平锁
- FairSync-公平锁
公平锁和非公平锁的本质区别就在于,获取锁的顺序是否符合FIFO,对于公平锁来说先加入同步队列等待的线程,必将会先获取到同步状态(锁),对于非公平锁来说,获取到锁的顺序不确定。
简单使用
在进行源码分析之前,先来看看ReentrantLock是如何使用的,ReentrantLock的使用非常简单,示例代码如下:
package com.lizba.p6;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* <p>
* ReentrantLock使用示例代码
* </p>
*
* @Author: Liziba
* @Date: 2021/6/21 22:09
*/
public class ReentrantLockDemo {
/** 初始化一个非公平锁,ReentrantLock的默认实现是非公平锁 */
private static Lock lock = new ReentrantLock();
public static void main(String[] args) {
new Thread(() -> testReentrantLock(), "Thread A").start();
new Thread(() -> testReentrantLock(), "Thread B").start();
}
/**
* 假设为获取锁执行的相关业务逻辑方法
*/
private static void testReentrantLock() {
// 获取锁要在try值外,如果获取锁过程中异常,不会无故释放锁
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + ":获取了锁");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放锁在finally代码块中
lock.unlock();
System.out.println(Thread.currentThread().getName() + ":释放了锁");
}
}
}
如上简单使用的案例,Thread A和Thread B输出的结果如下(这里是非公平锁不要被现在的顺序迷惑):
Sync—ReentrantLock组合的自定义同步器抽象
/**
* ReentrantLock内部类Sync,也是其内部组合实现的自定义同步器的抽象
*/
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
/**
* 定义获取锁的抽象方法,由NonfairSync和FairSync去实现各自获取锁的方式
*/
abstract void lock();
/**
* NonfairSync中tryAcquire调用的方法
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// 如果当前共享状态未被其他线程占用
if (c == 0) {
// 尝试通过CAS占有当前共享状态
if (compareAndSetState(0, acquires)) {
// 设置共享状态持有线程为当前线程
setExclusiveOwnerThread(current);
return true;
}
}
// 如果共享状态已被占用,则判断当前占用共享状态的线程是否就是当前线程
else if (current == getExclusiveOwnerThread()) {
// 如果是则自增获取次数,设值state
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
/**
* 释放共享状态
*/
protected final boolean tryRelease(int releases) {
// 计算减少后的值state
int c = getState() - releases;
// 判断当前线程和持有共享状态的线程是否是同一个线程,不是则抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 如果state递减后的值为0了,表示线程释放完共享状态,需要情况持有共享状态的线程变量
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
// 设置state
setState(c);
return free;
}
/**
* 判断占用共享状态的线程是否是当前线程
*/
protected final boolean isHeldExclusively() {
return getExclusiveOwnerThread() == Thread.currentThread();
}
/**
* 如果state不为0,则获取共享状态的持有线程,否则返回null
*/
final Thread getOwner() {
return getState() == 0 ? null : getExclusiveOwnerThread();
}
/**
* 如果当前线程持有共享状态,则返回state,否则返回0
*/
final int getHoldCount() {
return isHeldExclusively() ? getState() : 0;
}
/**
* 判断当前共享状态是否被持有
*/
final boolean isLocked() {
return getState() != 0;
}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
setState(0); // reset to unlocked state
}
final ConditionObject newCondition() {
return new ConditionObject();
}
}
NonfairSync源码分析
/**
* 非公平锁的代码实现
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
*
*/
final void lock() {
// CAS设置共享状态,返回true表示成功获取共享状态
if (compareAndSetState(0, 1))
// 设置当前线程为共享状态的持有线程
setExclusiveOwnerThread(Thread.currentThread());
else
// 否则调用AQS中的acquire(int arg)尝试获取同步状态,失败则加入等待队列,自旋获取共享状态
acquire(1);
}
/**
* 该方法在调用Sync中定义的nonfairTryAcquire方法,上面详细讲述了
* 主要是做线程重入判断,并对state共享状态值的增加(当获取同步状态的线程是持有同步状态的线程也就是所说的重入)
*/
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
NonfairSync的tryRelease调用的是Sync中的tryRelease,上面在Sync源码中详细介绍了。假设线程A对共享状态tryAcquire(1)了十次,那么线程A在调用tryRelease(1)的前9次,state的值依次递减的同时一定会返回false,只有第十次也就是最后一调用tryRelease(1),同步状态才会真正的释放,方法返回true,持有共享状态的线程置为null。
FairSync源码分析
/**
* 公平锁的代码实现
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
/**
* 调用AQS中的acquire(int arg)尝试获取同步状态,失败则加入等待队列,自旋获取共享状态
*/
final void lock() {
acquire(1);
}
/**
* 公平锁和非公平锁的主要区别在于此方法
*/
protected final boolean tryAcquire(int acquires) {
// 获取到当前线程
final Thread current = Thread.currentThread();
// 获取当前同步状态
int c = getState();
// 如果同步状态为0,则说明当前同步状态已完全释放
if (c == 0) {
// 1、hasQueuedPredecessors判断当前节点是否存在前驱节点
// 2、如果不存在则CAS设置state的值
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;
}
}
FairSync能够顺序的获取共享状态,也就是保证加入同步队列的顺序和获取到同步状态的顺序一致,依靠的是hasQueuedPredecessors()这个判断当前节点是否存在前驱节点的判断。
NonfairSync和FairSync区别示例代码
package com.lizba.p6;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
/**
* <p>
* 公平和非公平锁测试
* </p>
*
* @Author: Liziba
* @Date: 2021/6/21 23:33
*/
public class FairAndUnfairTest {
/** 定义公平锁 */
private static Lock fairLock = new ReentrantLockCustomize(true);
/** 定义非公平锁 */
private static Lock unfairLock = new ReentrantLockCustomize(false);
/**
* 测试公平锁和非公平锁
* @param lock
*/
private static void testFairAndUnfairLock(Lock lock) {
for (int i = 1; i <= 5; i++) {
new Job(lock, ""+i).start();
}
}
/**
* 定义线程实现,打印当前线程和等待队列中的线程
*/
private static class Job extends Thread {
private Lock lock;
public Job(Lock lock,String name) {
this.lock = lock;
setName(name);
}
@Override
public void run() {
// 通过两次输出,来判断是否与队列中一致
for (int i = 0; i < 2; i++) {
lock.lock();
try {
System.out.println("获取锁的线程:" + Thread.currentThread().getName());
System.out.println("同步队列中的线程:" + ((ReentrantLockCustomize)lock).getQueuedThreads().stream().map(t -> t.getName()).collect(Collectors.joining(",")));
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
/**
* 自定义可重入锁,主要新增getQueuedThreads()方法,用于获取等待队列中的线程
*/
private static class ReentrantLockCustomize extends ReentrantLock {
public ReentrantLockCustomize(boolean fair) {
super(fair);
}
/**
* 返回正在等待获取锁的线程列表,获取的实现列表逆序输出,反转后则为FIFO队列的原本顺序
*
* @return 等待队列中的线程顺序集合
*/
public Collection<Thread> getQueuedThreads() {
List<Thread> ts = new ArrayList<>(super.getQueuedThreads());
Collections.reverse(ts);
return ts;
}
}
}
测试公平锁
// 测试公平锁
testFairAndUnfairLock(fairLock);
查看输出
同步队列中等待的线程的顺序为2、3、4、5此时输出的结果为1、2、3、4、5和 1、2、3、4、5,按照同步队列中等待的顺序顺序输出,先进入同步队列的先获取到锁。
测试非公平锁
// 测试非公平锁
testFairAndUnfairLock(unfairLock);
查看输出
同步队列中等待的线程顺序为2、4、5、3当时线程1却连续获取了两次锁,因此非公平锁是不能保证获取锁的顺序的。
存在问题:
非公平锁很明显存在线程“饥饿”问题,也就是一个线程获取到锁后会继续的再次获取到锁的可能性比较大,导致其他线程等待时间较长,那么为何ReentrantLock还有继续设置其为默认实现呢?这个主要原因是,公平锁会带来大量的线程切换的开销,而非公平锁虽然可能会导致线程“饥饿”问题,但是其吞吐量是远远大于公平锁的,相比之下非公平锁优势更大。
- 点赞
- 收藏
- 关注作者
评论(0)