之前看过美团的一篇不可不说的Java“锁”事,对java锁的概念做了一次梳理,其实在java类中,ReentrantLock算是一个对锁概念运用的典范,看懂它的源码对锁的理解很有帮助。我也以ReentrantLock为原型,略加改动使之能在分布式环境中运行。
当我们看第一眼ReentrantLock源码,里面有一个Sync对象,它继承AbstractQueuedSynchronizer。
AbstractQueuedSynchronizer是一个抽象的队列式的同步框架,AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock,CountDownLatch
下面代码复写了tryRelease方法,其余的一些方法是自定义方法,我先删除了。其实还有一个tryAcquire方法,当开发复写这两个方法,就可以完成一个锁的编码设计。
是不是感觉有了这个万能的框架后,写一个锁很简单了?AbstractQueuedSynchronizer遵循 模板设计 模式,主骨架已经给你搭好,为了能串联起整个功能,你必须要复写必要的方法,不然直接调用AQS的方法,会抛出UnsupportedOperationException异常。
所以对ReentrantLock源码的研究也是对AQS的研究。
private final Sync sync;
/**
* Base of synchronization control for this lock. Subclassed
* into fair and nonfair versions below. Uses AQS state to
* represent the number of holds on the lock.
*/
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
}
上锁的流程可以直接看AQS的acquire方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
然后根据 && 的短路特性,我们把上面的步骤总结一下:
先尝试获取锁(tryAcquire),发现获取失败,则加入等待队列(addWaiter),并且判断是否需要休眠,是的话则休眠,不是则重试获取锁(acquireQueued)。
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
释放锁的步骤简单:先尝试释放锁,成功的话唤醒head节点的后继节点.
我想根据锁的特性(可重入,公平锁,自旋)来 拆解 讲解ReentrantLock,可以加深对锁的理解。
乐观锁与悲观锁是一种广义上的概念,在ReentrantLock的实现中,我们要从整体和部分两部分来说。
从整体来看,它是一个悲观锁,因为它是一个独占锁,当一个线程持有它并释放它前,其他线程是不能读或写共享资源的。
但从部分来看,ReentrantLock的实现方式有用到乐观锁(CAS)。
在AQS中,它维护了一个state变量(代表共享资源),ReentrantLock利用state变量来维护锁的获取状态,看代码
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//初始状态下,state为0,代表锁还没有线程获取。
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
//当锁获取成功时,state变成1(acquires为1)
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
。。。。。
上图的部分代码可以知道,state初始化为0,表示未锁定状态。线程
调用时,会调用tryAcquire()独占该锁并将state设为acquires值(默认为1)。此后,其他线程再tryAcquire()时就会失败,直到线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。
如何保证只有当state为0时更新才能成功?这就是compareAndSetState方法去做的事,compareAndSetState使用一种比较交换的技术(CAS Compare And Swap)来鉴别线程冲突,一旦检测到冲突的产生,更新会失败,并且线程不会阻塞挂起(跟悲观锁的主要区别),而是返回操作结果,你后面需要重试或者报错都取决于你,它是一个原子操作,所以在多线程中,CAS可以保证同步,这也就是乐观锁的一个经典实现。
CAS的更多内容网上有一堆,大家可以拓展阅读。所以在对一个锁的实现进行定性时,不能陷入一种误区,就是这个锁一定是悲观 or 乐观,需要根据锁的内部实现方式,服务对象来综合判断。
重入锁 意思是 同一个线程 能够 多次获取同一个锁,并且不产生死锁。可能会问什么情况会这个线程多次获取同一个锁?下面是最简单的示例,test方法调用test1方法,两个方法都用synchronized修饰,如果synchronized没有可重入的特性,线程执行到test1的时候会因为没有锁而卡住(test的锁还没有释放)。
synchronized void test() {
test1();
}
synchronized void test1() {
}
ReentrantLock名字本身就表示它是一个可重入锁,继续从代码看看怎么实现的吧。
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;
}
return false;
}
}
这个代码还是取的上面那个代码,只是这次我们得关注重心在 else if代码块中。
当getState()返回>0时,说明这个线程已经持有该锁了,但是还没有释放锁,然后他它会先判断当前线程是不是持有该锁的线程(两个线程不相等当然不能执行操作),这是可重入的前提,必须是同一个线程,
current == getExclusiveOwnerThread()
判断完没毛病后,就是简单的对state变量进行加法操作,你重入了2次,state就为2,重入3次就是3。可以看到这部分拿锁操作连CAS都没用,效率会非常快。
在解锁方面,大家也应该想到了,就是对state-1,直到state为0
//releases 默认为1
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
//当然也会判断一下当前锁的持有线程是不是跟 当前执行的线程一致
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//如果state减为0,则释放锁,否则只是更新一下state变量,重入了几次,就要解锁几次。
if (c == 0) {
free = true;
//锁持有线程清空
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
可重入锁解决了实际使用可能的死锁问题,而且性能消耗比线程第一次获取锁小很多.到目前为止,我们已经看完了ReentrantLock的门面设计,说前面的都是门面,因为ReentrantLock还有一个重要的组成部分还没介绍,就是先进先出的队列,用于暂存被睡眠的线程,我们由公平锁来引出它。
让我们回到获取锁的流程,tryAcquire方法如果失败了,就会被丢进一个等待队列,先从addWaiter开始。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
addWaiter 和 enq方法都被我摘出来。当一个节点获取锁失败时,会被放入队列中,队列本身的插入逻辑与一般维护队列的代码没什么区别,但怎么保证线程安全性?还是利用了CAS来保证。
在enq中,如果这个队列本身不存在,会进行一个循环(enq),先建一个头节点,这个头节点没有绑定线程信息,然后需要加入队列的节点Node会放在尾节点。
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;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
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结束以后,就轮到acquireQueued。这里的逻辑有点绕,刚进去就是一个死循环
当前线程node称为 节点A
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)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
经过上面的循环,节点A 有两个归宿,一个是拿锁成功返回,一个是被挂起,而挂起后怎么被 唤醒再继续 上面的步骤<1>的,玄机在解锁的代码。
假设tryRelease返回为true
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) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
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);
}
在唤醒和睡眠的代码里,逻辑比较绕的,就是对waitStatus状态的变更,而总结一句话就是:
唤醒前,总会把 Head头节点 设为 0(初始态),并且唤醒节点成功获取锁之后会取代头节点位置(acquireQueued setHead(node))
睡眠前,总会把前置节点设为 -1 (请求释放(Node.SIGNAL) )
介绍完AQS对队列的使用,我们可以看到对公平锁和非公平锁的使用
公平锁: 按照字面意思理解很简单,下一个处理的线程一定是按照队列排序的,即使是新来的线程,也必须先进队列再操作。
FairSync是ReentrantLock的公平锁实现类,里面的tryAcquire方法跟NonfairSync(非公平锁)唯一不同的就是多了一个hasQueuedPredecessors判断
hasQueuedPredecessors的意思就是每个新来的线程必要要先判断 队列里是不是已经有等待的线程,如果自己不是队列里第一个线程,就不允许尝试获取锁。
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;
}
}
。。。。。
}
非公平锁: 首先要更正一个误区,即使是非公平锁,也大致保持着公平的特性,如果没有新来的线程,对锁的获取也是先来后到的原则(队列来保证),只是如果有新来的线程,就可以插队,而插队不插队的判断逻辑就是有没有使用hasQueuedPredecessors方法(NonfairSync实现类是没有使用hasQueuedPredecessors方法)