一、前言
在并发场景下,多个线程同时对某个变量进行写入,可能导致某个线程在写入变量前读取到的是过期的值(不是其他线程已经写入的数据),为了解决此类数据安全问题,java通过锁来控制多个线程访问同一变量的顺序,从而解决该问题。锁有多种分类,比如悲观锁/乐观锁,共享锁/独占锁,公平锁/非公平锁等。
1. 悲观锁/乐观锁
悲观锁:假设最坏的情况,认为每次拿数据时都会被其他线程修改,所以在拿数据时会先获取锁,其他线程因为获取不到锁一直阻塞到获取锁的线程释放锁。synchronized就是一种悲观锁。
乐观锁:总是假设最乐观的情况,认为每次拿数据是其他线程不会修改该数据,因此不用假设,只有在更新数据时需要判断其他线程有没有在这期间修改过该数据,可以通过数据的版本号来实现。java中乐观锁的实现方式是CAS,也是AQS(AbstractQueuedSynchronizer)的基础.
2. 共享锁/独占锁
共享锁:锁可以被多个线程同时持有,Semaphore、CountdownLatch、ReentrantReadWriteLock中的readerLock都是共享锁。
独占锁:锁同时只能被一个线程持有,比如ReentrantLock、ReentrantReadWriteLock中的writerLock。
3. 公平锁/非公平锁
公平/非公平是针对于当多个线程同时竞争一把锁时,应该按照那种机制给线程派锁。公平锁是按照线程申请锁的顺序来获取锁。对于非公平锁来说,获取锁的顺序与申请锁的顺序无关,可能导致后申请锁的线程比先申请锁的线程早获取锁。
二、AQS简介
AQS是java众多锁的基础,ReentrantLock、Semaphore、CountdownLatch、ReentrantReadWriteLock的实现都依赖AQS。AQS是一种乐观锁,底层大量采用了CAS操作,当多个线程竞争锁时,通过自旋的方式重试。
AQS内部支持共享锁、独占锁,如果要使用AQS,子类需要分别实现不同的方法。
如上表所示,AQS对于这些方法的默认实现是抛出UnsupportedOperationException,子类需要覆写。
AQS的内部核心是通过FIFO(先入先出队列)实现线程获取锁时的排队工作,全局共享的状态变量(state)标识锁的持有情况,CAS+自旋实现乐观锁。
AQS中的队列是一个双向链表,元素是其内部类Node:
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;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
/** 节点的等待状态 */
volatile int waitStatus;
/** 当前节点的下一个节点 */
volatile Node prev;
/**
当前节点的下一个节点
*/
volatile Node next;
/**
*节点内部的线程
*/
volatile Thread thread;
Node nextWaiter;
/**
* Returns true if node is waiting in shared mode.
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
}Node中的waitStatus标识当前节点所处的状态,一共有4中状态:
- CANCELLED:当线程由于等待超时或者中断,取消竞争锁时,节点的状态置为该值;
- SIGNAL:与线程的挂起/线程阻塞有关。如果一个节点竞争锁失败时需要挂起,为了确保当锁资源被释放时其能被正常唤醒,要确保其pre节点的waitStatus为SIGNAL;
- CONDITION:标识当前节点处于条件队列下;
- PROPAGATE:用于确保释放锁操作操作的传递。
AQS中通过两个成员变量head、tail管理等待队列,其中head是dummy节点,内部的thread为null,这样做的原因是为了确保除了head节点之外,队列中的节点都能以相同的方式处理。
/**
头结点,实际上该节点是dummy节点,其内部的thread为null
*/
private transient volatile Node head;
/**
尾结点
*/
private transient volatile Node tail;
/** * 同步状态,共享锁下表示持有锁的线程数量,独占锁下表示重入锁的重入次数. */
private volatile int state;
下图是AQS独占锁中队列的示意图,每个节点的nextWaiter为null。
三、锁的获取操作
以ReentrantLock的使用为例,通过调用lock()获取锁,lock()实际上会调用AQS的acquire(),下面我们以acquire()作为入口,分析独占锁的获取过程。
AQS:
public final void acquire(int arg) {
//tryAcquire()由子类实现,尝试获取独占锁。查询state是否允许在独占锁模式下获取,如果允许则表示能获取锁
//addWaiter(),在给定模式(独占锁)下,为当前线程创建节点,并入队列
//acquireQueued(),对于已经在队列中的线程,在独占锁模式、不中断的条件下获取锁。
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}ReentrantLock,公平锁tryAcquire实现:
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//c=0表示,当前没有其他线程持有锁
if (c == 0) {
//是否有其他线程比当前线程提前尝试获取锁(公平锁的体现)
//cas将state->1
//设置独占线程
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;
}AQS.addWaiter(Node mode):
private Node addWaiter(Node mode) {
//创建代表当前线程的节点,并设置模式(独占/共享)
Node node = new Node(Thread.currentThread(), mode);
// 如果尾结点已经存在,则直接在尾结点后拼接当前节点并返回
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//如果尾结点为null,或者compareAndSetTail执行失败(当前线程修改尾结点的同时,其他线程修改了尾结点,导致cas失败)
enq(node);
return node;
}private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) {
//如果tail为null,说明head也为null,初始化head,tail。
if (compareAndSetHead(new Node()))
tail = head;
} else {
//如果此时tail不为null,在tail后插入当前节点
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}经过AQS.addWaiter(),线程已经进入FIFO队列中排队,下一步是调用acquireQueued()尝试获取锁。
AQS.acquireQueued(final Node node, int 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)) {
//获取到锁,重新设置head.
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//判断当前线程在获取锁失败后是否能挂起,挂起后检查中断状态(调用Thread.interrupted())
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
//在reentranLock.lock()中,cancelAcquire不会得到执行,线程不会因为竞争锁失败而取消获取,而是一直循环/挂起。
if (failed)
cancelAcquire(node);
}
}通过以上代码,我们发现对于acquireQueued()来说,线程会一直尝试获取锁,不会因为中断而退出锁的竞争。对于ReentrantLock,要想能通过中断控制线程放弃获取锁,可以选择lockInterruptibly(),该方法可以通过响应中断的方式放弃对锁的竞争。
AQS.shouldParkAfterFailedAcquire(Node pred, Node node):该方法主要用于在线程竞争锁失败后,判断当前线程是否应该挂起。在文章中对线程waitStatus的描述中,一个线程在竞争锁失败后,如果需要挂起,首先要确保前置节点的waitStatus为SIGNAL,这样前置节点在获取锁成功后,才知道需要唤起后继节点。
//返回值为true表示当前线程能挂起,否则不能挂起。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
//如果waitStatus是SIGNAL,说明线程可以安全挂起。
if (ws == Node.SIGNAL)
return true;
//如果前置节点的waitStatus>0,说明前置节点已经取消锁的竞争,应该从队列中移除前置节点中已经取消锁竞争的节点。
if (ws > 0) {
//通过循环向前遍历队列,直到节点的waitStatus<0。
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
*如果waitStatus=0或者PROPAGATE,应该将前置节点的waitStatus置为SIGNAL,下次调用该方法时才能选择挂起当前线程。
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}AQS.parkAndCheckInterrupt():挂起当前线程,等待被唤醒。
private final boolean parkAndCheckInterrupt() {
//线程挂起,不再往下继续执行;只有当线程被唤醒或者调用了Thread.interrupt(),才继续重复上述获取锁的操作。
LockSupport.park(this);
return Thread.interrupted();
}当竞争锁失败后,可以通过响应中断的方式退出锁的竞争。
AQS.cancelAcquire(Node node):
//通过响应中断的方式,当前线程退出锁的获取。
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
node.thread = null;
//跳过前置节点中已经取消锁竞争的节点。
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// predNext is the apparent node to unsplice. CASes below will
// fail if not, in which case, we lost race vs another cancel
// or signal, so no further action is necessary.
Node predNext = pred.next;
// Can use unconditional write instead of CAS here.
// After this atomic step, other Nodes can skip past us.
// Before, we are free of interference from other threads.
//设置节点的waitStatus为CANCELLED
node.waitStatus = Node.CANCELLED;
// 如果当前节点是tail,通过CAS将tail设置为pred,这一步可能失败,但是失败了也无挂紧要,因为节点的waitStatus已经
//被置为CANCELLED,其他线程也可以将当前节点从队列中剥离。
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
//由于当前节点要从等待队列中剥离,需要节点其后继节点的唤醒问题,要么被当前线程唤醒,要么没前置节点所在的线程唤醒。
int ws;
//如果pred不是head,而且pred.thread不为null,而且pred.waitStatus=SIGNAL,或者pred.status<0(为PROPAGATE)并将pred.waitStatus置为SIGNAL
//说明后继节点的唤醒可以由前置节点所在的线程解决,否则只能由当前线程唤醒。
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
//唤醒当前节点的后继节点。
unparkSuccessor(node);
}
node.next = node; // help GC
}
}AQS.unparkSuccessor:唤醒
private void unparkSuccessor(Node node) {
/*
* 清除节点的waitStatus,因为当前节点已经离队。
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
//一般来说唤醒的是node的next节点,但是如果next节点是null或者next节点已经取消,则需要找到离node最近的非CANCELLED的节点
//采用的方式是从尾部开始遍历,知道找到离node最近的非CANCELLED节点。
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);
}四、锁的释放操作
以ReentranLock为例,线程获取锁成功后,完成相关业务操作后,需要调用unlock()释放锁,unlock()通过调用AQS.release()完成锁的释放,释放锁后,需要唤醒队列中排在首位而且waitStatus != CANCELLED的节点。
//释放锁
public final boolean release(int arg) {
//释放锁,通过CAS操作对state进行修改。
if (tryRelease(arg)) {
Node h = head;
//如果head不为null,而且head节点的状态不可能为CANCELLED,head.waitStatus为SIGNAL/PROPAGATE时,
//说明队列中有正在等待获取锁的节点,需要唤醒后继节点。
if (h != null && h.waitStatus != 0)
//唤醒后续节点
unparkSuccessor(h);
return true;
}
return false;
}ReentranLock.Sync.tryRelease(int arg):
//释放锁
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//如果c=0,说明当前线程完全释放锁,否则只是减少锁的重入次数。
if (c == 0) {
free = true;
//将独占线程置为null
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}至此,独占锁的获取与释放代码分析完毕。AQS中,最重要的是stat(同步状态),FIFO队列(线程竞争锁的等待队列),这两个值的变化是整个AQS的核心。