文章目录
1. 简介
ReentrantLock,可重入锁,指的是同一个线程,外层函数获得锁之后,内层递归函数仍然能获取该锁的代码;同一个线程,在外层函数获得锁之后,在进入内层方法时,会自动获取锁;
使用demo详见另外一篇博文《java锁类型详解》
同时ReentrantLock内部也实现了公平与非公平两种模式。
本文着重讲解ReentrantLock的源码。看看如何实现可重入,公平与非公平。
先上个使用demo,来个直观感受:
//声明一个可重入锁
Lock lock = new ReentrantLock();
public void get() {
lock.lock();
try {
System.out.println(Thread.currentThread().getId() + "\t invoked set()");
//调用下一个加锁的方法
set();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void set() {
lock.lock();
try {
System.out.println(Thread.currentThread().getId() + "\t ######invoked set()");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
线程调用get()方法,拿到锁后,再调用加了锁的set()方法, 可以直接获取到锁。
可以看到,主要方法就是lock.lock();和unlock;
public void lock() {
sync.lock();
}
lock()是sync内部类的抽象方法,是不是感觉有点混乱,下面我们先看看ReentrantLock代码结构。
2.代码结构
先看代码结构:
ReentrantLock内部包含3个静态内部类:
- Sync :同步类,继承自AQS(AbstractQueuedSynchronizer,此类比较重要,但我们不单独讲解,我们结合具体的实现类中的方法说明)
- NonfairSync :非公平锁
- FairSync :公平锁
内部类继承关系如上图。
3.构造方法
如果想使用ReentrantLock,必然先创建对象,我们看下2个简单构造方法:
//无参数构造方法,直接new了一个非公平锁
//因此ReentrantLock默认是非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
//也可以指定锁类型
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
所以,再我们new ReentrantLock();的时候,默认使用的是非公平锁,lock()方法也是非公平锁的lock。继续往下看。
4.非公平锁NonfairSync
既然默认是非公平锁,我们就先从NonfairSync开始吧。
此内部类总共就俩方法:
4.1 lock()方法
我们先看常用的lock()方法:
final void lock() {
//compareAndSetState是AQS中的,原理就是乐观锁cas
//作用就是尝试获取锁,获取到,就在内存的某个位置标记1
if (compareAndSetState(0, 1))
//如果获取到了锁,就标记此线程独占了此锁
setExclusiveOwnerThread(Thread.currentThread());
else
//直接没有获取到锁,则走公平锁的的拿锁逻辑,内部需要排队
acquire(1);
}
说明:当new了一个锁lock后,这个lock往往是全局变量,供多个线程使用:Lock lock = new ReentrantLock();
1.当一个线程thread调用lock方法时,因为是非公平锁,所以会直接尝试获取(抢)锁,成功后标记线程占用此锁,
2.如果失败,则调用acquire(1);进行排队;
4.2 acquire()
下面看看acquire(1)的实现:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&//再次尝试抢锁
//加入队列排队
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
//中断当前线程
selfInterrupt();
}
此方法是AQS中的方法,其中if条件比较复杂,分开说:
- tryAcquire(arg) :再次尝试抢锁
- addWaiter(Node.EXCLUSIVE) :创建节点,加入队列
- acquireQueued() :再次判断是否真的需要排队
4.2.1 tryAcquire(arg)
重点看tryAcquire(arg),这是在公平锁与非公平锁中自己实现的,并且是各自单独实现的:
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
//父类Sync中实现
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//此锁被某个线程拥有的次数(一个线程可能会多次获取此锁),开始肯定是0
int c = getState();
if (c == 0) {
//compareAndSetState(0, acquires):再一次直接抢锁,之前在lock()开始的时候进行过此逻辑
//抢到了锁,则返回ture
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//如果状态不是0(说明可能是此线程之前在别的方法中可能已经拿到了此锁)
//进入此if,进一步判断是不是当前线程,返回ture,这也是可重入锁的含义:此线程别的A方法已经拿到锁,那么方法B可直接获取到锁
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
//int类型值溢出,跑异常
throw new Error("Maximum lock count exceeded");
//更新此锁的state值
setState(nextc);
return true;
}
//如果尝试获取锁失败,返回false
return false;
}
步骤总结:
1.先通过state字段,看看这个锁是否被人持有,如果没有人持有,不管有没有人排队,就直接尝试枪锁;
2.如果是被人持有,则看看持有人是不是自己,如果是自己,就直接拿到此锁,这也是可重入核心原理实现;
4.2.2 addWaiter
if (!tryAcquire(arg) &&
//加入队列排队
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)){}
继续看addWaiter:
//加入等待队列中,创建一个新的节点,将此节点放置于队列尾部,并将此节点返回
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
//拿到当前队列中的尾部节点
Node pred = tail;
if (pred != null) {
//如果尾部节点不是null,那么将新节点指向尾部节点
node.prev = pred;
//cas,重新将新节点设置为尾部节点
if (compareAndSetTail(pred, node)) {
//尾部的前一个节点指向此尾部节点
pred.next = node;
return node;
}
}
//如果尾部节点是null,进入死循环,初始化头部和尾部节点,并把当前节点连接到tail后边
enq(node);
return node;
}
//创建头部节点,并让尾部==头部
//将新节点链接到尾部
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
//初始化头部节点和尾部节点
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
步骤总结:
addWaiter的主要作用就是new一个当前线程的节点,并拼接到tail节点后边,同时让tail也指向此节点;最后返回此节点。
但是当tail为空时,要先初始化tail。
4.2.3 acquireQueued()
if (!tryAcquire(arg) &&
//加入队列排队
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)){}
继续看:
//是否真的需要排队
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
//如果当前线程节点是head,说明没有人排队了,直接尝试获取锁
//如果成功了,就不加阻塞队列等待
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//走到这,说明仍然没获取到锁,仍然可能需要排队
//那么就继续判断它前边那个人的状态
if (shouldParkAfterFailedAcquire(p, node) &&
//进行线程中断操作
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
步骤总结:
此方法的主要作用是,判断当前线程是否真的需要排队;因为addWaiter已经将当前线程的node排到了队列尾部;
但是,如果它的前一个节点就是head节点,那也是不需要排队的,因为它已经算是在最前边了;
如果前边不是头节点,那么也不一定需要排队等待,因为前边的人可能正好处理完了,要把锁交给它了;最后如果这些条件都不符合,那么就中断等待别人唤醒。
5.公平锁FairSync
5.1 lock()方法
final void lock() {
acquire(1);
}
公平锁的lock和非公平很相似,只是少了抢锁的步骤,可以对比4.1节
5.2 tryAcquire()方法
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//此锁被若干线程拥有的次数,开始肯定是0
int c = getState();
if (c == 0) {
//hasQueuedPredecessors():队列中是否有别的线程在此线程前边排队?没有则继续判断
//compareAndSetState(0, acquires):再一次直接抢锁,之前在lock()开始的时候进行过此逻辑
//上边这俩条件都满足,说明抢到了锁,返回ture
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//如果状态不是0(说明此线程之前在别的方法中可能已经拿到了此锁)
//进入此if,就会返回ture,这也是可重入锁的含义:此线程别的A方法已经拿到锁,那么方法B可直接获取到锁
else if (current == getExclusiveOwnerThread()) {
//有线程拿到此锁,则计数+1
int nextc = c + acquires;
if (nextc < 0)
//int类型值溢出,跑异常
throw new Error("Maximum lock count exceeded");
//更新此锁的state值
setState(nextc);
return true;
}
//如果尝试获取锁失败,返回false
return false;
}
步骤总结:
1.先通过state字段,看看这个锁是否被人持有,如果没有人持有,又没有人在前面排队,那么就直接尝试枪锁;
2.如果是被人持有,则看看持有人是不是自己,如果是自己,就直接拿到此锁,这也是可重入的核心实现原理;
公平锁的tryAcquire()与非公平锁基本一致,只有一行代码不同:hasQueuedPredecessors()
公平锁获取锁的判断条件中多了hasQueuedPredecessors():判断队列中是否有别的线程在此线程前边排队
//判断是是否有人再前边排队
public final boolean hasQueuedPredecessors() {
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
//头节点不等于尾部节点
//头节点的下一个节点,不能是null,也不能是当前线程,表示前面有别人在排队,这也是公平锁的核心理念:有别人在前边,我不能插队
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
6.unlock()方法
加完锁,最后都是要释放锁的。
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {//释放锁,设置state=state-1
Node h = head;
if (h != null && h.waitStatus != 0)
//通知后续节点拿锁
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
//将state状态-1
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//state是一个线程加锁的次数,要想释放,必须减到0;比如加了两次锁,就必须有两次释放锁的动作
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
步骤总结:
释放锁,就是将state(一个线程加锁的次数)减到0,然后通知队列下一个线程节点。
到这里,ReentrantLock的核心方法和原理,基本理清楚了,其他方法细节,后续再补充吧。
7.lockInterruptibly()方法
lockInterruptibly()比较特殊,他与lock()方法的区别是:
lock优先考虑获取锁,获取到锁之后,才会响应中断;
lockInterruptibly()优先响应中断,也就是即使没获取到锁,进入了等待状态,依然可以响应thread.interrupt()中断方法,停止等待。