ReentrantLock上锁过程
当面试官问执行了ReentrantLock锁是怎么上锁时,可以这样回答
1、首先创建了一个ReentrantLock锁时默认是使用的非公平锁,当执行lock的上锁方法时就会调用ReentrantLock内部的非公平锁的实现对象的lock方法。这个非公平锁对象继承了AQS(AbstractQueuedSynchronizer)这个抽象类,这个抽象类中包含了一个锁对象可能会用到的大多数方法(后面有时间再跟更新)
ReentrantLock默认以非公平锁为实现方式,当这样的对象执行lock方法时
ReentrantLock lock = new ReentrantLock();
lock方法其实是ReentrantLock 对象内部的sync对象的lock方法
lock.lock();
这里执行了sync的lock方法,此时的sync对象为NonfairSync
public void lock() {
sync.lock();
}
NonfairSync的结构如下
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
这里实现了上锁的方法,下面的tryAcquire也是用来上锁,由acquire调用
上面的sync.lock调用的就是这个方法
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
上面的lock方法第一次cas上锁失败后开始执行acquire方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
这里进来先执行tryAcquire方法,就是上面的非公平锁实现NonfairSync 中的方法,进入到这个方法调用的nonfairTryAcquire方法中看看
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
这个getState是父类中的父类AQS中的方法,用来获取当前锁标识state变量的值,0 为没有被线程持有 大于0标识被某个线程持有,数值为重入次数
int c = getState();
若没有线程持有,尝试加锁,这里已经是第二次尝试加锁了
为什么要重新尝试一次,是因为有可能在第一次上锁失败后持有锁的线程就把锁释放了。
if (c == 0) {
这里的逻辑和NonfairSync中的lock方法逻辑相同,若加锁成功把exclusiveOwnerThread变量设为当前获取锁的线程,这个变量是AQS中的,被Sync继承
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
若上锁失败就检查当前持有锁的线程是不是自己,若是自己那就是重入锁,给state变量加上1,并返回上锁成功,否则返回上锁失败
else if (current == getExclusiveOwnerThread()) {
c是state变量值,acquires是调用方法传的参数,值为1
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
setstate方法如下
protected final void setState(int newState) {
state = newState;
}
此时回到acquire方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
若tryAcquire方法返回false,即上锁失败,后面就需要把当前线程封装为一个Node节点添加到AQS中的同步队列中去
Node节点的结构如下,省去了构造方法
static final class Node {
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
这四个值是下面的waitState的四个枚举值,用于标记这个封装为Node节点的线程的状态(被中断,处于同步队列中,处于等待队列中)
static final int CANCELLED = 1;线程被中断,若在同步队列中遇到这样的节点需要丢弃
static final int SIGNAL = -1;等待被唤醒,加入到同步队列中的初始转态,也是正常状态
static final int CONDITION = -2;处于条件等待转态,位于等待队列中的节点状态
static final int PROPAGATE = -3;
volatile int waitStatus;线程状态,就是上面四种值中的一个
volatile Node prev;指向同步队列的前一个节点
volatile Node next;指向同步队列的后一个节点
volatile Thread thread;Node节点封装的线程
Node nextWaiter;指向等待队列的下一个节点
是否为共享锁
final boolean isShared() {
return nextWaiter == SHARED;
}
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
}
接着看acquireQueued(addWaiter(Node.EXCLUSIVE), arg)这个方法
首先进入到addWaiter(Node.EXCLUSIVE)中
这里的方法已经是在AQS中了
private Node addWaiter(Node mode) {
把当前线程封装到Node节点中
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
这里判断如果同步队列的最后一个节点不为空,就把node节点添加到同步对列的尾部,这里也是cas加入的
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
到这里说明同步队列为空或者cas添加到队尾失败
enq(node);
return node;
}
addWaiter()方法的目的是把节点添加到队尾。
看看enq(node)方法
private Node enq(final Node node) {
这里是个死循环,不停的cas加入队尾直至成功入队
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;
}
}
}
}
enq(node)方法的作用就是在第一次加入队尾失败后,循环尝试着把节点加入到同步队列的尾部,当队列为空或者第一次加入队尾失败时会执行(有多线程竞争的情况第一次加入可能会失败)。
再次回到acquire方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
此时addWaiter(Node.EXCLUSIVE)方法也已经完成,看看acquireQueued()方法
这个也是AQS中提供好的方法
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
这里获取新加入节点的前一个节点
final Node p = node.predecessor();
如果前面的节点是头结点,就再次尝试获取一下锁
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);
}
}
若这个方法返回true说明node的前驱节点是一个SIGNAL状态(等待唤醒状态)的节点,需要把node节点中的线程挂起(这里就是把新加入的节点挂起)
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
如果前面的节点是处于等待获取锁状态就返回true
if (ws == Node.SIGNAL)
return true;
判断前一个节点是否是已终止状态,如果是就往前找,找到为SIGNAL状态的节点
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
把当前节点的前继节点置换为一个状态为SIGNAL的节点
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
把当前线程挂起并返回线程中断状态
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
这个方法做的内容就是从同步队列的最后一个节点开始往前遍历,把已经取消的线程节点从队列中删除,然后把新加入同步对列中的线程挂起。
至此上锁过程就结束了
总结一下这个过程:
1、当执行了ReentrantLock.lock()后先使用unsafe的cas给AQS中的变量state置换为1(在ReentrantLock中是Sync对象继承的AQS),若上锁失败就进入到acquire方法中。
2、进入到这个方法后执行tryAcquire(arg)方法,先再次尝试获取锁(进行cas操作),若失败就判断当前获取锁的线程是不是自己,如果是自己就对state加一并返回true,如果不是就返回false。
3、然后执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)就把自己封装为一个Node对象进行加入同步队列的操作。
4、addWaiter(Node.EXCLUSIVE)这个方法会创建一个Node节点把当前线程存进去,并添加到同步队列的队尾(同样cas添加)。
5、acquireQueued()这个方法是进行同步对列的整理,沿着新加入的节点往前面找,如果前面的节点转态是已取消的(waitState值为1)就跳过前面节点往前找,直到找到状态为SIGNAL或者为CONDITION 状态的节点,把新节点的前继节点设置为这个节点,这个操作相当于删除了新节点前面的被取消的节点。