java.util.concurrent.locks包下,包含了多种锁,ReentrantLock独占锁、ReentrantReadWriteLock读写锁等,还有java.util.concurrent下的Semaphore、CountDownLatch都是基于AQS实现的。
AQS是一个抽象类,但事实上它并不包含任何抽象方法。AQS将一些需要子类覆写的方法都设计成protect方法,将其默认实现为抛出UnsupportedOperationException异常。如果子类使用到这些方法,但是没有覆写,则会抛出异常;如果子类没有使用到这些方法,则不需要做任何操作。
可重写方法:
final方法:
根据实现方式的不同,可以分为两种:独占锁和共享锁
使用方法:
推荐作为静态内部类来继承AQS。例如ReentrantLock作为外部类实现Lock接口,静态抽象内部类Sync继承AQS。重写Lock接口方法时,是直接调用Sync类的实现类公平锁NonfairSync或非公平锁FairSync内的方法来完成相应逻辑。
state:
private volatile int state;
//volatile,state为0表示锁没有被占用,可以把state变量当做是当前持有该锁的线程数量。
队列:
一个FIFO的双向链表,这种结构的特点是每个数据结构都有两个指针,分别指向直接的后继节点和直接前驱节点。所以双向链表可以从任意一个节点开始很方便的访问前驱和后继。
每个Node其实是由线程封装,当线程争抢锁失败后会封装成Node加入到队列中去。
Node主要属性:
// 节点所代表的线程
volatile Thread thread;
// 双向链表,每个节点需要保存自己的前驱节点和后继节点的引用
volatile Node prev;
volatile Node next;
// 线程所处的等待锁的状态,初始化时,该值为0
volatile int waitStatus;
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
// 该属性用于条件队列或者共享锁
Node nextWaiter;
队列的头节点和尾节点
// 头节点,不代表任何线程,是一个哑结点
private transient volatile Node head;
// 尾节点,每一个请求锁的线程会加到队尾
private transient volatile Node tail;
CAS(Compare and Swap)操作:
JAVA使用 锁和循环CAS来实现原子操作,原子操作意为”不可被中断的一个或一系列操作”,保证只有一个线程操作数据。
悲观锁与乐观锁:
CAS的思想很简单:三个参数,一个当前内存值V、旧的预期值A、即将更新的值B,当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false。循环CAS实现的基本思路就是循环进行CAS操作直到成功为止。
Java中的CAS功能是通过Unsafe类来实现的。Java并发包中java.util.concurrent 大量使用了这种操作,来保证线程安全。compareAndSetState(int, int)方法就是通过Unsafe实现的,而setState就是线程不安全的。
除了try*()方法外,AQS自身实现了诸多的如acquire和doAcquire()等方法,他们之间的区别在于try*()方法代表一次尝试性的获取锁操作,如果获取到了就拿到了锁,否则直接返回。而AQS自身实现的acquire和doAcquire()等方法如果获取不到锁会能够进入同步/等待队列中阻塞等待进行锁的争夺,直到拿到了锁返回。对于共享锁,try*()也会进行自旋获取,因为共享锁可以被多个线程持有。
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire是子类获取锁应该实现的方法,一般在里面判断state值,state==0时就可以获取到锁,通过下面方法获取锁,然后返回true,否则返回false,公平锁的话会再判断自己有没有前驱节点等。
setExclusiveOwnerThread(current); // 将当前线程设置为占用锁的线程
当tryAcquire获取锁失败时,&&前面为true,才会调用addWaiter方法,将当前线程包装成Node,加到等待锁的队列中去, 因为是FIFO队列, 所以加在队尾。
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode); //将当前线程包装成Node
// 这里我们用注释的形式把Node的构造函数贴出来
// 因为传入的mode值为Node.EXCLUSIVE,所以节点的nextWaiter属性被设为null
/*
static final Node EXCLUSIVE = null;
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
*/
Node pred = tail;
// 如果队列不为空, 则用CAS方式将当前节点设为尾节点
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node); //将节点插入队列
return node;
}
将一个节点node添加到sync queue末尾的三步:
添加有可能失败,原因可能是以下两种之一:
失败的时候会调用一个enq(node)方法,在该方法中, 出现第一种情况时,该方法也负责在队列为空时, 初始化队列,然后使用了死循环, 即以自旋方式将节点插入队列,如果失败则不停的尝试, 直到成功为止,运用到了乐观锁的原理。
private Node enq(final Node node) {
for (;;) {
Node t = tail;
// 如果是空队列, 首先进行初始化
// 这里也可以看出, 队列不是在构造的时候初始化的, 而是延迟到需要用的时候再初始化, 以提升性能
if (t == null) {
// 注意,初始化时使用new Node()方法新建了一个dummy节点
if (compareAndSetHead(new Node()))
tail = head; // 这里仅仅是将尾节点指向dummy节点,并没有返回
} else {
// 到这里说明队列已经不是空的了, 这个时候再继续尝试将节点加到队尾
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
enq(node)方法运行可能会造成尾分叉既多个尾节点的现象,因为node.prev = t;可能被多个线程运行,后面if语句则是CAS操作保证了单线程运行。不过只是一种暂时的现象,因为线程不断循环保证入队。
enq(node)方法后返回到acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法。
该方法中将再次尝试去获取锁,因为如果当前节点的前驱节点就是HEAD节点,则可以再尝试获取锁。
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);
}
}
setHead方法将该节点设置成头节点,上一个头节点就被顶掉。以此来达成节点出队列的效果。
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
如果获取不到锁调用shouldParkAfterFailedAcquire,该方法用于决定在获取锁失败后, 是否将线程挂起,决定的依据就是前驱节点的waitStatus值。在独占锁的获取操作中,我们只用到了其中的两个——CANCELLED和SIGNAL。每一个节点最开始的时候waitStatus的值都被初始化为0。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus; // 获得前驱节点的ws
if (ws == Node.SIGNAL)
// 前驱节点的状态已经是SIGNAL了,说明闹钟已经设了,可以直接睡了
return true;
if (ws > 0) {
// 当前节点的 ws > 0, 则为 Node.CANCELLED 说明前驱节点已经取消了等待锁(由于超时或者中断等原因)
// 既然前驱节点不等了, 那就继续往前找, 直到找到一个还在等待锁的节点
// 然后我们跨过这些不等待锁的节点, 直接排在等待锁的节点的后面
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 前驱节点的状态既不是SIGNAL,也不是CANCELLED
// 用CAS设置前驱节点的ws为 Node.SIGNAL,给自己定一个闹钟
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
返回false时会进行循环,就是将那些CANCELLED的节点移出队列,然后再循环一次,再尝试获取锁,因为自己有可能已经到头节点后面了,如果不是则自己排到waitStatus值为SIGNAL的前节点后面,此时shouldParkAfterFailedAcquire返回true。将调用parkAndCheckInterrupt()方法。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); // 线程被挂起,停在这里不再往下执行了
return Thread.interrupted();
}
调用了LockSupport类的park方法。
LockSupport工具类主要用来挂起park(Thread)和唤醒unpark(Thread)线程,底层实现也是使用的Unsafe类。若其他线程调用了阻塞线程的interrupt()方法,阻塞线程也会返回,即阻塞的线程是响应中断的,而且不会抛出InterruptedException异常。LockSupport并不需要获取对象的监视器,而是给线程一个“许可”(permit),unpark可以先于park调用,unpark一个并没有被park的线程时,该线程在下一次调用park方法时就不会被挂起。
所以最后return Thread.interrupted();是因为不能保证他不是被中断的,所以返回Thread的中断状态。
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
tryRelease(arg)
该方法由继承AQS的子类实现, 为释放锁的具体逻辑。一般是将state设置为0,setExclusiveOwnerThread(null);再将当前线程设置为null。
unparkSuccessor(h) 唤醒h的后继线程
当有头节点且头节点的waitStatus不等于0的时候则唤醒后继线程,因为waitStatus初始值为0,当队列进入新节点时,头节点会被设置为SIGNAL。
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
// 如果head节点的ws比0小, 则直接将它设为0
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 此时从尾节点开始向前找起, 直到找到距离head节点最近的ws<=0的节点
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; // 没有return, 继续向前找。
}
// 如果找到了还在等待锁的节点,则唤醒它
if (s != null)
LockSupport.unpark(s.thread);
}
从尾部开始遍历,因为新节点接入的时候是先node.prev = t,队列可能只执行到这步,后面还没执行,所以从尾向前遍历。
找到头节点的下一个不是CANCELLED的节点并唤醒,unpark方法对应前面添加节点的park方法,所以回到前面。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); // 线程被挂起,停在这里不再往下执行了
return Thread.interrupted();
}
所以当线程获取不到锁,会被park一直阻塞状态,直到被interrupt或者有锁的线程释放锁时,才会获得锁。获得锁后返回中断状态
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);
}
}
如果是中断唤醒的返回true,再设置interrupted = true,因为Thread.interrupted()调用后中断状态会被重新设回false。继续循环,如果自己是在头节点下一位,就可以获取锁了,否则又要挂起。
共享锁的acquireShared方法对应独占锁acquire
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
doAcquireShared对应了独占锁的acquireQueued,
private void doAcquireShared(int arg) {
··························································
final Node node = addWaiter(Node.SHARED); //代表共享模式
··························································
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
//与独占锁的acquireQueued的区别主要就是中间这段代码
··························································
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);
}
}
重点在setHeadAndPropagate方法
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
//if里面包含了两个头节点,一个新一个老,多线程下两者可能不一样,两种情况。
//1.propagate > 0 表示调用方指明了后继节点需要被唤醒
//2.头节点后面的节点需要被唤醒(waitStatus<0),不论是老的头结点还是新的头结点
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
//如果当前节点的后继节点是共享类型或者没有后继节点,则进行唤醒
//这里可以理解为除非明确指明不需要唤醒(后继等待节点是独占类型),否则都要唤醒
if (s == null || s.isShared())
doReleaseShared();
}
}
在setHeadAndPropagate方法里面,将获取锁的节点设置为头节点,然后再去doReleaseShared,doReleaseShared对应了独占锁的unparkSuccessor,作用是唤醒下一个线程,所以在共享锁的releaseShared方法(对应独占锁release),就是释放锁方法里,也主要是用doReleaseShared来释放锁。
下面到doReleaseShared方法
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) { //至少有头尾两个节点
int ws = h.waitStatus;
if (ws == Node.SIGNAL) { //ws为SIGNAL的时候才去唤醒下一个节点
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) //将头节点的SIGNAL改为0,CAS操作
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;
}
}
doReleaseShared方法里,我们暂且把现在持有锁的线程成为节点A,下一个节点为B。
首先判断 if (ws == Node.SIGNAL),因为我们每次插入节点都会默认0,并且把前节点设成SIGNAL,所以当条件成立时,声明A节点后面已经有B了。 到下一层,if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)),意思是把节点A的SIGNAL改为0。
为什么需要CAS操作呢?
CAS保证了后面单线程唤醒后继线程的操作,在上面谈到的doReleaseShared这个方法,在获取锁和释放锁的时候都会调用,防止重复唤醒。
接下来是 else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) 什么时候满足这句呢?
ws为0,每当一个节点进同步队列都会把前面节点设置为SIGNAL,自己初始为0,所以满足ws==0的条件就是节点A是队列最后一个且后面还没有节点B入列的情况。
满足了ws==0,运行下面这句。
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE),CAS操作失败返回true,将该新节点A的0设置APROPAGATE不成功。不成功就意味着新节点A的0已经被改了,被改意味着新节点A后面已经进入了节点B,设置前节点为SIGNAL的操作是线程在获取不到锁之后,阻塞之前,忘记的可以回顾一下前面的内容。
所以else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) 这个条件成立分为了两个阶段,既有尾节点又加入新节点的这个瞬间可能会满足。
那么满足为什么要continue呢?
因为节点B线程在获取不到锁之后,阻塞之前,所以此时A还没释放锁,A仍是头节点,h==head条件成立,执行break跳出循环,不会去唤醒B了,这不符合共享锁的机制。所以应该continue继续循环,去唤醒B节点,而不是等A运行完释放锁的时候才去调用。
h == head如果不成立,说明A唤醒完B,B已经调用了setHead这个方法了,这个时候再去循环看看B节点后面有没有节点。