锁篇
前言
这一篇主要探究一下关于ReentranLock
的源码和AQS的部分源码,写完这一篇这个系列就暂时告一段落,明天是Redis的原理分析和实战,尽快在字节面试之前把所有的内容再学习一遍。
AQS源码分析
ReentranLock是基于AQS实现的,所以在学习ReentranLock之前,先来研究一下AQS的源码吧。
AQS的全称是AbstractQueuedSynchronizer,从代码上面的注释中我们可以获取几个关键信息:
- AQS的实质就是一个实现同步锁的框架,里面主要包含一个虚拟队列。
- 拥有两种线程模式:独占和共享
- 定义了内部类ConditionObject
成员变量
成员变量主要分为两个部分,一个是有关于AQS的基本成员变量,上面说到AQS维护了一个同步队列,这个队列实际上是由一个双向链表进行维护的,所以会有头节点和尾节点。
//队列头节点
private transient volatile Node head;
//队列尾节点
private transient volatile Node tail;
//状态,state是临界资源,也是锁的描述。表示有多少线程获取了锁。
private volatile int state;
还存在一个unsafe
的变量,unsafe
是一个权限很大的操作类,可以通过类似于反射的方式(实际上反射也是基于unsafe的),获取对象内部各个属性的偏移量。
// 获取Unsafe类的实例,注意这种方式仅限于jdk自己使用,普通用户是无法这样调用的
private static final Unsafe unsafe = Unsafe.getUnsafe();
// 状态变量state的偏移量
private static final long stateOffset;
// 头节点的偏移量
private static final long headOffset;
// 尾节点的偏移量
private static final long tailOffset;
// 等待状态的偏移量(Node的属性)
private static final long waitStatusOffset;
// 下一个节点的偏移量(Node的属性)
private static final long nextOffset;
内部类
从内部类中可以看到AQS主要分为独占模式和共享模式,大部分子类都实现其中一个模式,但是类似于读写锁ReentrantReadAndWriteLock即实现了独占模式也实现了共享模式。
static final class Node {
/** 当前节点处于共享模式的标记 */
static final Node SHARED = new Node();
/** 当前节点出于独占模式的标记 */
static final Node EXCLUSIVE = null;
/** 线程被取消 */
static final int CANCELLED = 1;
/** 释放资源后需要唤醒其他线程*/
static final int SIGNAL = -1;
/** 线程正在等待condition */
static final int CONDITION = -2;
//工作于共享锁状态,需要向后传播,
//比如根据资源是否剩余,唤醒后继节点
static final int PROPAGATE = -3;
//等待状态,值为上面的变量之一
volatile int waitStatus;
//前驱节点
volatile Node prev;
//后继节点
volatile Node next;
//等待锁的线程
volatile Thread thread;
//等待条件的下一个节点
Node nextWaiter;
}
获取锁
acquire
方法主要适用于独占模式的获取锁,首先会调用tryAcquire
方法,这个方法在AQS中并没有实现,直接调用会抛出异常,acquireQueued
是通过自旋方式获取锁,并且判断线程是否需要被挂起。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
//尝试获取锁,如果不成功则先加入队列然后自旋,同时判断是否需要被挂起
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
//没有获取到锁,并且在自旋的时候发现需要挂起就直接中断
selfInterrupt();
}
//没有具体实现,留给子类
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
//中断
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
加入队列
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
//插入队伍尾部
if (pred != null) {
node.prev = pred;
//通过cas插入
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//如果tail为空,说明队列还没有初始化,执行enq()
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;
}
}
}
}
自旋获取锁
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);
}
}
判断是否需要挂起
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
//前驱节点的状态是SIGNAL,说明前驱节点释放资源后会通知自己
//此时当前节点可以安全的park(),因此返回true
return true;
if (ws > 0) {
//前驱节点的状态是CANCLLED,说明前置节点已经放弃获取资源了
//此时一直往前找,直到找到最近的一个处于正常等待状态的节点
//并排在它后面,返回false
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//前驱节点的状态是0或PROPGATE,则利用CAS将前置节点的状态置
//为SIGNAL,让它释放资源后通知自己
//如果前置节点刚释放资源,状态就不是SIGNAL了,这时就会失败
// 返回false
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
private final boolean parkAndCheckInterrupt() {
//使用LockSupport,挂起当前线程
LockSupport.park(this);
return Thread.interrupted();
}
释放锁
依旧是独占模式的释放锁,释放锁的操作比起获取锁来简单一些,如果尝试释放成功就唤醒下一个节点。
public final boolean release(int arg) {
//尝试释放锁
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
//唤醒下一个节点
unparkSuccessor(h);
return true;
}
return false;
}
private void unparkSuccessor(Node node) {
//如果状态为负说明是除CANCEL以外的状态,
//尝试在等待信号时清除。
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
//下一个节点为空或CANCELLED状态
//则从队尾往前找,找到正常状态的节点作为之后的继承人
//也就是下一个能拿到资源的节点
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
//此时找到继承人了,那么就唤醒它
if (s != null)
LockSupport.unpark(s.thread);
}
获取共享锁
public final void acquireShared(int arg) {
//抽象方法交由子类实现
if (tryAcquireShared(arg) < 0)
//如果没获取到锁就加入队列
doAcquireShared(arg);
}
private void doAcquireShared(int arg) {
//以共享模式加入队列
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
//尝试自旋
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
释放共享锁
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
private void doReleaseShared() {
//释放成功,需要唤醒下一个节点
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
ReentrantLock
接口
ReentrantLock没有继承其他类,只实现了一个Lock接口,而Lock接口中声明了一些获取锁、释放锁、通知等方法。
public class ReentrantLock implements Lock, java.io.Serializable
//Lock接口
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
成员变量
可以看到看到ReentrantLock的成员变量非常少,只有一个序列化UID以及一个叫做Sync的对象。在这个对象上面注释了“通过同步器实现所有机制”
private static final long serialVersionUID = 7373984872572414699L;
/** Synchronizer providing all implementation mechanics */
private final Sync sync;
内部类
上面提到的Sync正是ReentrantLock的内部类,而从Sync又衍生出nofairSync和fairSync两个应用于非公平锁和公平锁的任务同步器。
Sync
我们先从Sync开始,Sync是一个抽象类,实现了一部分非公平锁的获取方法以及一些其他的释放锁、获取锁状态的方法。
但是最重要的lock
方法确是一个抽象方法交由子类实现。
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
//获取锁的方式,子类化的原因是允许非公平版本的快速路径
//这里的快速路径的意思是非公平锁会先尝试把自己设置为锁的拥有者
//如果失败再尝试进入队列。
abstract void lock();
//非公平锁的尝试获取锁
//ReentrantLock会根据自身是否公平而在tryAcquire中选择执行不同的方法。
//可以看到是final修饰的,所以实现就在这里了
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//获取队列的状态,前面说过state表示有多少线程持有锁
int c = getState();
//如果没有线程持有,则直接通过CAS获取。
if (c == 0) {
if (compareAndSetState(0, acquires)) {
//把当前线程设置为锁的持有者
setExclusiveOwnerThread(current);
return true;
}
}
//获取失败则先判断自己是否是锁的持有者
//因为ReentrantLock是一个可重入锁
else if (current == getExclusiveOwnerThread()) {
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) {
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;
}
protected final boolean isHeldExclusively() {
//检查当前线程是否是锁的持有者
//不知道为什么上面不调用这个方法而是直接比较
//注释上写了不需要调用这个方法来判断
return getExclusiveOwnerThread() == Thread.currentThread();
}
final ConditionObject newCondition() {
return new ConditionObject();
}
// 调用外部类的方法,主要获取一下锁的状态
final Thread getOwner() {
return getState() == 0 ? null : getExclusiveOwnerThread();
}
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
}
}
NonfairSync
可以看到在这个子类中只实现了一个获取锁的方式。
在lock()
方法中尝试直接获取锁,先通过cas更新,这里可以看到cas(0,1),所以需要本身就0才能更新成功。
如果失败的话才尝试调用acquire方法来获取锁。
acquire会先尝试tryAcquire方法,而tryAcquire方法调用了nonfairTryAcquire
nonfairTryAcquire在上面已经介绍过了,就是尝试cas获取锁,如果失败再看一下自己是否是持有者,如果是则+1,否则失败。
如果这两者都失败,会再尝试进入等待队列,如果也失败就会直接中断。
//非公平锁的子类实现
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
//AQS中的acquire方法,为了直观我写在这边
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
fairSync
公平锁和非公平锁之间的区别就在于tryAcquire,当锁没有被线程所持有的时候,
会直接尝试获取锁,而非先采用CAS更新。
获取锁的时候会判断是否有前驱节点,没有的话就直接获取,有的话就会进入队列。
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
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;
}
//如果当前锁被某个线程持有就直接返回false
//返回false之后会尝试加入队列
return false;
}
}
//判断一下队列中是否有线程等待,或者队列中的下一个线程是否是当前线程。
public final boolean hasQueuedPredecessors() {
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
构造方法和其他方法
构造方法比较简单,默认是一个非公平锁,可以传入fair
参数,为true的话就生成一个公平锁。
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
可以看到其他的方法其实都是调用的sync类中的方法,包括释放锁、获取锁。
在ReentrantLock中不能传入加锁的次数,所以一般尝试获取锁的时候都是获取一次。
public void lock() {
sync.lock();
}
小结
通过上面的源码分析可以看到ReentrantLock是一种可重入锁,每次只能加锁一次。
当采用非公平锁的时候,如果一个线程想要获取锁会先使用CAS更新,这就导致了如果一个线程先释放锁,然后再获取锁,那么很大概率就是这个线程又获得了锁而非等待队列中的线程。这个操作减小了线程切换的开销,但是有可能导致等待队列中的线程饥饿。
而采用公平锁的时候,每次获取锁都会判断当前线程是否是队列中的下一个节点,如果队列为空或者是下一个节点才允许获取锁,这样就保证了公平性。
ReentrantReadWriteLock
ReentrantReadWriteLock表示的是读写锁,之前介绍过AQS分为独占和共享锁两种模式,ReentrantReadWriteLock同时实现了这两个模式,写锁是一种独占锁,而读锁是一种共享锁。
接口
ReentrantReadWriteLock实现了ReadWriteLock接口,这个接口中声明了读锁和写锁两个方法。
public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable
public interface ReadWriteLock {
/**
* Returns the lock used for reading.
*
* @return the lock used for reading
*/
Lock readLock();
/**
* Returns the lock used for writing.
*
* @return the lock used for writing
*/
Lock writeLock();
}
成员变量
相比较ReentrantLock,成员变量多出了两个读锁和写锁的对象,以及一个使用unsafe方法获取的偏移量。
/** Inner class providing readlock */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** Inner class providing writelock */
private final ReentrantReadWriteLock.WriteLock writerLock;
/** Performs all synchronization mechanics */
final Sync sync;
内部类
Sync
和之前类似,读写锁中也维护了一个Sync的同步器类,而公平锁和非公平锁是通过继承这个子类,然后在获取锁的时候执行一些不同的操作。
不过在读写锁中这个类由于要实现独占锁和共享锁两种模式所以代码非常复杂,我们一点一点来看。
成员变量
在之前的可重入锁中是通过AQS中的state成员变量来表示自身同时被多少个线程所持有的。
而读写锁中需要同时维护读锁的持有线程数和写锁的持有线程数,所以将state字段一分为二,前16位表示共享锁的个数,后16位表示独占锁的个数。
对于独占锁来说,表示重入次数是非常简单的,因为同时只能有一个线程持有,所以返回state的后16位就行了,但是对于共享锁,同时可能有多个线程持有,所以维护了一个ThreadLocal对象用于在线程内部保存自己的重入次数。
static final int SHARED_SHIFT = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
/** Returns the number of shared holds represented in count */
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
/** Returns the number of exclusive holds represented in count */
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
//ThreadLocal保存每个自己重入次数
private transient ThreadLocalHoldCounter readHolds;
//缓存最后一个成功获取readLock的线程的重入次数
private transient HoldCounter cachedHoldCounter;
//第一个获取读锁的线程
private transient Thread firstReader = null;
//第一个获取读锁的重入次数
private transient int firstReaderHoldCount;
独占锁的获取和释放
根据代码上面的注释,我们可以看到读写锁中写锁的获取策略是这样的:
- 首先判断一下state是否为0,如果不是0的话说明必定有读锁或者写锁。
- 如果state不是0但是写锁是0(此时必定有读锁),或者当前的独占锁不是请求线程返回false。
- 如果线程数超过了上限则直接抛出异常。
- 当前线程已经持有写锁,返回true。
- 接下来会判断一下读锁是否需要阻塞,然后采用cas更新,如果cas更新失败并且需要阻塞则返回false,否则返回true。
其中writerShouldBlock()
方法是一个抽象方法,交于子类实现。
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
// (Note: if c != 0 and w == 0 then shared count != 0)
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires);
return true;
}
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
释放独占锁则显得比较简单,如果释放锁之后计数为0就把独占锁设置为null。
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
共享锁的获取
相比独占锁,共享锁的原理更加复杂。
先来看锁的获取,如果已经有线程获取了写锁并且不是当前线程则直接返回-1。
接下来就判断读锁是否需要阻塞、读锁是否达到上限、尝试CAS更新,如果都成功则说明加读锁成功,设置一些变量。
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
//有写锁并且不是当前线程
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
int r = sharedCount(c);
//CAS更新成功。
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {
//是第一个读锁
firstReader = current;
firstReaderHoldCount = 1;
//是第一个读锁,并且在进行重入
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
//缓存最近一个读锁,也就是当前线程的重入次数
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
//成功获取到读锁,计数+1
rh.count++;
}
return 1;
}
//如果失败则尝试全力获取读锁
//代码和这边其实差不多,但是多了一个自旋的过程
//一旦发现被加了写锁或者读锁到达上限则直接返回失败
return fullTryAcquireShared(current);
}
共享锁的释放
共享锁的释放就比获取稍微简单一些。
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
//当前线程正好是第一个读线程
if (firstReader == current) {
// 断言读锁必定大于0
//如果是1就把第一个读锁置为null
if (firstReaderHoldCount == 1)
firstReader = null;
else
//否则自减
firstReaderHoldCount--;
} else {
//获取最后一个读锁的重入次数
HoldCounter rh = cachedHoldCounter;
//如果不是最后一个读锁,就从ThreadLocal中获得重入次数
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
for (;;) {
int c = getState();
//读锁状态-1
int nextc = c - SHARED_UNIT;
//尝试更新
if (compareAndSetState(c, nextc))
// Releasing the read lock has no effect on readers,
// but it may allow waiting writers to proceed if
// both read and write locks are now free.
return nextc == 0;
}
}
Sync子类
在非公平锁中,写锁是不会被阻塞的,所以会直接尝试CAS更新然后加入队列。
而读锁需要判断当前队列中的头结点是否是独占锁。
/**
* Nonfair version of Sync
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -8159625535654395037L;
final boolean writerShouldBlock() {
return false; // writers can always barge
}
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
}
而在公平锁中则都需要判断是否有前驱节点。
/**
* Fair version of Sync
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = -2274990926593161451L;
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
}
总结
从上面的总结可以看出Java中锁设计的基本思路。首先都会尝试获取锁,如果获取锁失败再加入到队列中,在队伍中的节点总是会自旋判断自己是否是头结点,如果是的话就尝试获取锁。
在释放锁的时候,都需要唤醒下一个节点,然后由于需要维护同步器中的可重入次数,所以还需要进行CAS更新。
这一篇文章暂时总结到这里,目前只写了如何可重入锁和读写锁,CountDownLatch之类的同步器就之后再更新吧。
参考文章:
AQS源码详细解读
ReentrantLock源码详细解读