JUC:java.util.concurrent
1.AQS
AQS全称AbstractQueuedSynchronizer,即抽象队列同步器,是阻塞式锁和同步器工具的框架。
其实AQS的本质就是JUC并发包下的一个基类,ReentrantLock、Semaphore、CyclicBarrier、CountDownLatch等并发类都是基于AQS实现的。具体做法是继承AQS并实现其模板方法,从而达到同步状态的管理。
AQS的核心思想是:如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效工作线程,并将共享资源设置为锁定状态;如果被请求的共享资源被占用,就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制在AQS里是用CLH队列实现的。AQS内部维护了一个等待队列(CLH队列)进行排队,将暂时获取不到锁的线程加入到队列中,并用一个volatile类型的变量state表示锁状态,然后子类用CAS的方式去操作。
AQS可以实现独占锁和共享锁。ReentrantLock是独占锁。ReentrantReadWriteLock是独占锁和共享锁。CountDownLatch是共享锁。
2.AQS源码分析
①AQS结构
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
protected AbstractQueuedSynchronizer() { }
private transient volatile Node head;//头结点
private transient volatile Node tail;//尾结点
private volatile int state; //同步状态(state为0表示无锁; state>0时有锁)
protected final int getState() { //获取锁状态
return state;
}
protected final void setState(int newState) {
state = newState; //设置锁状态
}
}
AQS内部包含了一个队列和一个volatile int类型的state变量。
head、tail和state都是volatile类型变量,volatile可以保证多线程的内存可见性。
同步队列的基本结构:
同步队列的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;//等待队列中的后继节点
final boolean isShared() { //当前节点是否处于共享模式等待
return nextWaiter == SHARED;
}
final Node predecessor() { //获取前驱节点,如果为空则抛出空指针异常
Node p = prev;
if (p == null) {
throw new NullPointerException();
} else {
return p;
}
}
Node() { }
Node(Thread thread, Node node) { //addWaiter会调用此构造函数
this.nextWaiter = node;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { //Condition会用到此构造函数
this.waitStatus = waitStatus;
this.thread = thread;
}
}
Node节点是对每一个访问同步代码的线程的封装,包含了需要同步的线程本身以及线程的状态,如是否被阻塞,是否等待唤醒,是否已经取消等。每个Node节点内部关联其前驱节点prev和后继节点next,这样方便线程释放锁后快速唤醒下一个在等待的线程。
SHARED和EXCLUSIVE常量分别代表共享模式和独占模式。共享模式是一个锁允许多条线程同时操作,如信号量Semaphore采用的就是基于AQS的共享模式实现的。独占模式是同一个时间段只能有一个线程对共享资源进行操作,多余的请求线程需要排队等待,如ReentrantLock。
从Node结构中prev和next节点可知,node是一个双向链表,waitStatus存储了当前线程的状态信息:
CANCELLED (1) :当前线程因为超时或者中断被取消。这是一个终结态,也就是状态到此为止。
SIGNAL (-1) :当前线程的后继线程被阻塞或即将被阻塞,当前线程释放锁或者取消后需要唤醒后继线程。这个状态一般都是后继线程来设置前驱节点的。
CONDITION (-2):当前线程在condition队列中。
PROPAGATE (-3) :用于将唤醒后继线程传递下去,这个状态的引入是为了完善和增强共享锁的唤醒机制。在一个节点成为头节点之前,是不会跃迁为此状态的。
0 表示无状态。
AQS使用了模板方法模式。自定义同步器时需要继承AbstractQueuedSynchronizer并重写下面几个AQS提供的钩子方法:
boolean tryAcquire(int arg) 独占方式,尝试获取独占锁。成功返回true,失败返回false。
boolean tryRelease(int arg) 独占方式,尝试释放独占锁。成功返回true,失败返回false。
int tryAcquireShared(int arg) 共享方式,尝试获取共享锁。负数表示失败;0表示成功,但没有剩余可用锁;正数表示成功,且有剩余锁。
boolean tryReleaseShared(int arg) 共享方式,尝试释放共享锁。成功返回true,失败返回false。
boolean isHeldExclusively() 当前线程是否正在独占锁。只有用到condition才需要实现它。
②acquire()-独占模式请求获取锁
public final void acquire(int arg) {
if(!tryAcquire(arg) && acquireQueued( addWaiter(Node.EXCLUSIVE),arg))
selfInterrupt();
}
1)首先调用tryAcquire()方法尝试获取共享资源,如果成功则返回true。
tryAcquire()是模板方法,由子类实现具体操作。
2)如果tryAcquire()失败,则调用addWaiter()方法把当前线程封装成一个独占模式的Node节点,并使用CAS操作把该节点追加到等待队列的尾部。
addWaiter()方法作用是:判断等待队列是否为空,如果是空则初始化之后再将自己插入,否则直接将自己插入队尾。
private Node addWaiter(Node mode) {
Node node = new Node(mode);//将当前线程封装成一个独占模式的节点
for (;;) { //自旋,只有compareAndSetTail( oldTail, node)返回true,即当前线程成功插入队尾以后才会return结束for循环,否则就一直在这里自旋尝试插入队尾
Node oldTail = tail;
if (oldTail != null) {
node.setPrevRelaxed(oldTail); //将当前线程节点的前驱节点设置为刚才的尾节点
if (compareAndSetTail(oldTail, node)) { //用CAS方式将当前线程节点设置为队列的尾节点
oldTail.next = node; //刚才尾节点的后继节点设置为当前线程节点
return node; //将当前线程节点返回
}
} else { //尾节点为空,说明队列为空,需要先初始化队列
initializeSyncQueue();
}
}
}
Node node = new Node(mode);表示用当前线程创建一个EXCLUSIVE的Node:
Node(Node nextWaiter) {
this.nextWaiter = nextWaiter;
THREAD.set(this, Thread.currentThread());
}
当AQS中队列为空时会调用initializeSyncQueue()方法初始化头节点和尾节点,然后再将当前线程节点放到队列尾部:
private final void initializeSyncQueue(){
Node h;
if(HEAD,.compareAndSet(this, null, (h=new Node()))) //初始化一个Node作为头节点,这个头节点没有任何意义(没有thread),不属于排队线程
tail = h;
}
addWaiter()方法执行完成后,会返回当前线程的节点,然后执行acquireQueued()方法(注意在执行该方法之前已经将当前线程节点加入AQS的队列中)。
3)当前线程的节点加入队列尾部后,该节点的线程进入自旋状态。自旋时,判断自旋节点的前驱节点是不是头节点,如果前驱节点是头节点,则该节点再次尝试获取,如果仍未获取到,则把自旋节点的线程挂起,等待前驱节点唤醒;如果此时尝试获取成功,则将该节点设置为头节点。
acquireQueued()方法的作用:判断当前线程结点的前驱结点是否为头结点,如果前驱节点是头结点就尝试再次获取锁,否则就直接调用park()睡眠。如果不能获取锁就一直睡眠停留在这里,否则就会返回执行用户编写的代码。
final boolean acquireQueued(final Node node, int arg) {
try {
boolean interrupted = false;
for (;;) { //自旋开始,如果前驱节点是head即当前节点是排队的第一个,这时候如果当前节点成功获取锁就return结束for循环,否则需要确保前驱节点的状态为SIGNAL然后将当前节点park。如果获取锁失败或者修改前驱节点为SIGNAL时失败,就会继续下一次循环再次尝试
final Node p = node.predecessor();//获取当前节点的前驱节点
if (p == head && tryAcquire(arg)) { //如前驱节点是head则再次尝试加锁,如果此时加锁成功就将当前节点设置为新的头节点
setHead(node);
p.next = null; // 将之前的头节点置为null,便于GC回收
return interrupted;//整个方法里只有这里有return,也就是只有这里会结束该方法
}
//前驱节点不是头节点,或者前驱节点是头节点但是再次尝试获取锁失败,此时需要将当前线程挂起,但是挂起时要保证该节点的前驱节点是有效节点,而且要保证前驱节点的状态是SIGNAL(只有前一个节点的状态是SIGNAL才会唤醒下一个节点)。shouldParkAfterFailedAcquire方法返回true代表前驱节点有效,此时调用parkAndCheckInterrupt方法将当前线程挂起(上一个节点的等待状态是-1时,shouldParkAfterFailedAcquire才会返回true,返回true后调用parkAndCheckInterrupt方法将线程阻塞,等待唤醒获取锁)
if (shouldParkAfterFailedAcquire(p, node))
interrupted |= parkAndCheckInterrupt();
}
} catch(Throwable t) {
cancelAcquire(node);
throw t;
}
}
看一下setHead()方法:
private void setHead(Node node) {
head = node;
node.thread = null; //头节点不需要thread属性,队列中的一个节点获取了锁以后,就不需要再排队了,因此将它的thread置为null,并将它设置为新的头节点
node.prev = null;
}
节点获取到锁资源后无需再排队,此时将该节点变成头节点,因为头节点就是没有意义的,即不需要排队。但是队列中该节点不删掉,作为头节点使用。
再看shouldParkAfterFailedAcquire()方法:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if(ws == Node.SIGNAL) //如果前驱节点的等待状态为SIGNAL,那么以后可以正常唤醒该线程,那就没问题
return true;//前驱节点的等待状态是-1才会返回true
if(ws > 0) { //前驱节点为CANCELLED状态,即已经失效。需要将CANCELLED节点舍弃点,找到它之前的有效节点作为前驱节点
do {
node.prev = pred = pred.prev;//将当前节点的prev指针指向前驱节点的前驱节点
} while (pred.waitStatus > 0); //找到队列中排在当前节点之前并且等待状态不为CANCELLED的节点
pred.next = node;//将找到的有效的前一个节点的next指向当前节点
} else { //等待状态不是SIGNAL或CANCELLED此时将前一个有效节点的状态改为SIGNAL
pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
}
return false;
}
最后是parkAndCheckInterrupt()方法:
private final boolean parkAndCheckInterrupt(){
LockSupport.park();
return Thread.interrupted();
}
将当前线程挂起。
当acquireQueued()方法出现代码异常时,会调用cancelAcquire()方法:
private void cancelAcquire(Node node) { //参数node为当前节点
if(node == null) return;
node.thread = null; //将当前节点的线程置为空,即竞争锁资源跟我没关系了
Node pred = node.pred;
//如果当前节点的前驱节点为CANCELLED状态,则跳过该前驱节点。通过while循环找到当前节点之前的最近的有效节点
while(pred.waitStatus > 0)
node.prev = pred = pred.prev;
//将找到的最近的有效节点声明为predNext
Node predNext = pred.next;
node.waitStatus = Node.CANCELLED;//将当前节点置为失效节点
if(node == tail && compareAndSetTail(node, pred)) { //如果当前节点为尾节点,(由于当前节点为失效节点)则将刚刚找到的离当前节点最近的有效节点置为尾节点(这样就把当前节点以及它之前所有失效的节点都跳过了)
pred.compareAndSetNext(predNext, null);//用CAS的方式将尾节点的next置为null(这样就与失效节点都断开了链接)
} else {
int ws;
if(pred != head && ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && pred.compareAndSetWaitStatus(ws, Node.SIGNAL))) && pred.Thread != null) { //当前节点的前驱节点不是头节点,并且前驱节点有效,此时需要把前驱节点的next到指向自己的next(自己已经为CANCELLED状态,需要跳过自己)
Node next = node.next; //自己的next节点
if(next != null && next.waitStatus <=0)
pred.compareAndSetNext(predNext, next);//用CAS方式把当前节点的next赋值给前驱节点的next(跳过自己)
} else { //如果当前节点是头节点,而当前节点已经是CANCELLED状态,此时需要唤醒它的后继节点
unparkSuccessor(node);//唤醒当前节点的后继节点
}
node.next = node;//help GC
}
}
③release()-独占模式请求释放锁
public final boolean release(int arg) {
if(tryRelease(arg)) { //模板方法
Node h = head; //头节点
if(h != null && h.waitStatus != 0) //如果队列非空且头结点的状态标识为SIGNAL
unparkSuccessor(h); //唤醒头结点的后继结点,即排队的第一个节点
return true; //释放锁成功
}
return false; //释放锁失败(该锁被重入了,释放次数<加锁次数)
}
1)调用tryRelease()释放共享资源,即state=0。
tryRelease()是模板方法。
2)释放共享资源后唤醒没有被中断的后驱节点的线程。
主要是unparkSuccessor()方法:
private void unparkSuccessor(Node node) {
int ws = node.waitStatus; //获取等待状态,此时可能为SIGNAL或CANCELLED。如果为CANCELLED意味着node已经被唤醒但发现自己被中断需要继续往后唤醒新的结点
//如果当前结点状态标识为SIGNAL
if (ws < 0) //不是CANCELLED状态
node.compareAndSetWaitStatus(ws, 0); //使用CAS方式修改当前结点状态标识为0,即初始状态
Node s = node.next; //获取后继结点
if (s == null || s.waitStatus > 0) { //如果队列中没有等待的线程或当前节点的后继节点的等待状态为CANCELLED,则从后往前找到最后一个(也就是从前往后第一个)等待状态不是CANCELLED的结点,且不能是当前结点
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null) //如果找到了最靠前的等待状态为SIGNAL的节点,即队列中有等待被唤醒的线程,就调用unpark方法唤醒该节点的线程
LockSupport.unpark(s.thread); //唤醒第一个等待的线程(被阻塞的线程会在parkAndCheckInterrupt()中继续向下执行)
}
④acquireShared()-共享模式请求获取锁
public final void acquireShared(int arg) {
if(tryAcquireShared(arg) <0)
doAcquireShared(arg);
}
tryAcquireShared(arg)是模板方法,需要子类具体实现,其返回值小于0说明获取共享锁失败(相当于独占锁tryAcquire返回false),然后调用doAcquireShared()将当前线程加入等待队列。
private void doAcquireShared(int arg) {
final Node node=addWaiter(Node.SHARED);
boolean interrupted = false;
try {
for(;;) {
final Node p = node.predecessor();
if(p == head) {
int r = tryAcquireShared(arg);
if(r > 0) {
setHeadAndPropagate(node,r);
p.next = null;
return;
}
}
if(shouldParkAfterFailedAcquire(p,node)
interrupted |= parkAndCheckInterrupt();
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
} finally {
if(interrupted)
selfInterrupt();
}
}
和独占锁的acquire()方法很类似,区别是Node节点为SHARED,获取锁方法tryAcquireShared小于0时表示无法获取锁。
⑤releaseShared()-释放共享锁
public final boolean releaseShared(int arg) {
if(tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
tryReleaseShared()是模板方法,由子类具体实现,其返回值为true表示释放成功。
private void doReleaseShared(){
for(;;) {
Node h = head;
if(h != null && h != tail) {
int ws = h.waitStatus;
if(ws == Node.SIGNAL) {
if(!h.compareAndSetWaitStatus( Node.SIGNAL, 0))
continue;
unparkSuccessor(h);
} else if(ws == 0 && !h.compareAndSe tWaitStatus(0, Node.PROPAGATE))
continue;
}
if(h == head) break;
}
}
3.synchronized同步锁
synchronized是同步锁,也称互斥锁、内部锁,用于多线程同步。
当多个线程同时访问被synchronized修饰的方法时,同一时间有且仅有一个线程可以访问该方法,当一个线程在访问时,其它线程只能等待,这个线程访问完毕后,下一个线程才可以访问。
每个对象只有一个锁,谁能够拿到这个锁谁就有访问权限。在多线程运行过程中,线程会去抢对象的监视器monitor,这个监视器是对象独有的(其实就相当于一把钥匙),抢到了就能获得当前代码块的执行权,其他没有抢到的线程就会进入队列SynchronizedQueue中等待,等待当前线程执行完后释放锁。当前线程执行完毕后会通知出队,然后继续重复当前过程。从jvm的角度来看monitorenter和monitorexit指令代表着代码的执行与结束 。
synchronized锁的特点:
①监视器锁:使用synchronized实现的线程同步是通过监视器monitor实现的,所以内部锁也叫监视器锁。
②自动获取/释放:线程对同步代码块的锁的申请和释放由JVM内部实施,线程在进入同步代码块前会自动获取锁,并在退出同步代码块时自动释放锁,这也是同步代码块被称为内部锁的原因。(异常时也会自动释放)
③锁定方法/类/对象:synchronized关键字可以用来修饰方法,锁住特定类和特定对象。
④临界区:同步代码块就是内部锁的临界区,线程在执行临界区代码前必须持有该临界区的内部锁。
⑤锁句柄:内部锁锁的对象就叫锁句柄(就是刚才代码里面的this或者lock对象),锁句柄通常会用 private和final关键字进行修饰。因为锁句柄变量一旦改变,会导致执行同一个同步代码块的多个线程实际上用的是不同的锁。
⑥不会泄漏:内部锁不会导致锁泄漏,因为javac 编译器把同步代码块编译为字节码时,对临界区中可能抛出的异常做了特殊处理,这样临界区的代码出了异常也不会妨碍锁的释放。
⑦非公平锁:内部锁是使用的是非公平策略,是非公平锁,也就是不会增加上下文切换开销。
synchronized分为类锁和对象锁,即可以锁类也可以锁对象。在多线程访问时,这两个锁有很大的区别,对象锁是用于对象实例方法或者一个对象实例上的;类锁是用于类的静态方法或者一个类的class对象上的。类的对象实例可以有很多个,但是每个类只有一个Class对象。注意:不同对象实例的对象锁是互不干扰的;每个类只有一个类锁;类锁和对象锁互相不干扰。
类锁的作用是防止多个线程同时访问添加了synchronized锁的代码块,而对象锁是防止其他线程访问同一个对象中synchronized代码块或者函数。
①对象锁
对象锁创建有两种方法:
//同步方法,对象锁
public synchronized void syncMethod() {
……
}
//同步代码块,this对象锁
public void syncThis() {
synchronized (this) {
……
}
}
②类锁
类锁创建也有两种方法:
//同步class对象,类锁
public void syncClassMethod() {
synchronized (SynchronizedDemo.class) {
……
}
}
//同步静态方法,类锁
public static synchronized void syncStaticMethod(){
……
}
③类锁和对象锁比较
根据类锁和对象锁的概念,通过例子验证它们的区别。
首先有三个方法:
public class SynchronizedDemo {
private int ticket = 10;
//同步方法,对象锁
public synchronized void syncMethod() {
for (int i = 0; i < 1000; i++) {
ticket--;
System.out.println( Thread.currentThread().getName() + "剩余的票数:" + ticket);
}
}
//同步块,对象锁
public void syncThis() {
synchronized (this) {
for (int i = 0; i < 1000; i++) {
ticket--;
System.out.println( Thread.currentThread().getName() + "剩余的票数:" + ticket);
}
}
}
//同步Class对象,类锁
public void syncClassMethod() {
synchronized (SynchronizedDemo.class) {
for (int i = 0; i < 50; i++) {
ticket--;
System.out.println( Thread.currentThread().getName() + "剩余的票数:" + ticket);
}
}
}
}
(1)验证一:同一个对象实例,使用两个线程调用不同的对象锁方法(可实现同步)
final SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
//线程一
new Thread() {
@Override
public void run() {
synchronizedDemo.syncMethod();
}
}.start();
//线程二
new Thread() {
@Override
public void run() {
synchronizedDemo.syncThis();
}
}.start();
由于使用的是同一个对象的对象锁,所以执行出来的结果是同步的(即先运行线程一,等线程一运行完后运行线程二,ticket有序的减少)。
(2)验证二:不同的对象实例,使用两个线程调用同一个对象锁方法(不可实现同步)
final SynchronizedDemo synchronizedDemo1 = new SynchronizedDemo();
final SynchronizedDemo synchronizedDemo2 = new SynchronizedDemo();
//线程一
new Thread() {
@Override
public void run() {
synchronizedDemo1.syncMethod();
}
}.start();
//线程二
new Thread() {
@Override
public void run() {
synchronizedDemo2.syncMethod();
}
}.start();
由于是两个不同的对象,它们的对象锁就是不同的,其结果是两个线程互相抢占资源的运行,即ticket偶尔会无序的减少,不能实现同步。
(3)验证三:同一个对象,使用两个线程,一个线程调用对象锁方法、一个线程调用类锁方法(不可实现同步)
final SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
//线程一
new Thread() {
@Override
public void run() {
synchronizedDemo.syncMethod();
}
}.start();
//线程二
new Thread() {
@Override
public void run() {
synchronizedDemo.syncClassMethod();
}
}.start();
由于对象锁和类锁互不干扰,所以也是线程不安全的,不能实现同步。
因此得出结论:1、不同对象实例的对象锁互不干扰;2、每个类只有一个类锁。3、类锁和对象锁互相不干扰。
synchronized使用注意:
①synchronized锁定的是对象,而不是代码。
②synchronized(Object)中Object不能用String常量或Integer、Long等基本数据类型。
③作为锁的Object最好是final修饰,防止该Object发生变化导致锁失效。
④synchronized具有可重入性,是可重入性锁。比如一个类有两个synchronized方法,在m1方法中需要调用m2方法,如果synchronized不具有可重入性,那这里就没办法调用m2了。
⑤程序在执行过程中,如果出现异常,默认情况下锁会被释放。所以在开发过程中,有异常要多加小心,不然可能会发生不同步的情况。比如在一个app中,多个线程共同访问同一个资源,这时如果异常处理不合适,在第一个线程中抛出异常,其他线程就会进入同步代码区,有可能会访问到异常产生的数据。因此要非常小心的处理同步业务逻辑中的异常。
ReentrantLock和synchronized的区别:
①ReentrantLock的加锁和解锁操作都需要手动完成。
②当异常发生时synchronized会自动释放锁,而ReentrantLock不会自动释放锁。因此需要将unlock()操作放在finally代码块中,保证任何时候锁都能够被正常释放掉。
③默认情况下synchronized和ReentrantLock都是非公平锁。但是ReentrantLock可以通过传入true来创建一个公平锁。
4.ReentrantLock可重入锁(同步锁、互斥锁)
在jdk1.6之前,synchronized被称为重量级锁。为了提高性能,出现了ReentrantLock锁。后来改进了synchronized,使得synchronized的执行效率和ReentrantLock差不多,甚至更好,但是由于ReentrantLock可以直接代码操作加锁/解锁、可中断获取锁等特性,因此使用的比较多。
ReentrantLock是java实现的公平锁/非公平锁,也是可重入锁,它是基于AQS实现的。
1)首先看一下ReentrantLock用法:
class X {
private final ReentrantLock lock = new ReentrantLock(true);
public void xx(){
lock.lock();
try{
//do something
} finally{
lock.unlock();
}
}
}
ReentrantLock有一个抽象内部类Sync继承了AbstractQueuedSynchronizer,NonfairSync类和FairSync类均继承自Sync。ReentrantLock的默认构造函数中实例化了NonfairSync类和FairSync类。对于外部而言,只关注lock与unlock方法,但实际上内部调用了AbstractQueuedSynchronizer的方法。
2)下面看源码实现
(1)构造方法
public ReentrantLock() {
sync = new NonfairSync(); //默认为非公平锁
}
public ReentrantLock(boolean fair) {
sync = fair? new FairSync(): new NonfairSync();
}
(2)ReentrantLock加锁
ReentrantLock加锁会调用lock()方法:
public void lock() {
sync.acquire(1); //调用AQS的acquire()方法
}
AQS的acquire()方法:
public final void acquire(int arg) {
if(!tryAcquire(arg) && acquireQueued( addWaiter(Node.EXCLUSIVE),arg))
selfInterrupt();
}
其中tryAcquire()是模板方法,在ReentrantLock中会根据公平锁还是非公平锁有不同的实现。
①ReentrantLock中公平锁的tryAcquire()方法
static final class FairSync extends Sync {
protected final boolean tryAcquire(int acquires){
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { //锁空闲
//判断自己是否需要排队
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { //不需要排队,并且用CAS获取到锁
setExclusiveOwnerThread(current); //设置自己为持有锁的线程
return true;//加锁成功
}
} else if (current == getExclusiveOwnerThread()) { //锁不空闲,并且自己持锁,则锁重入
int nextc = c + acquires;
if (nextc < 0) //正整数的最大值加1会变成负数1(最高位的符号位变了)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;//锁重入成功
}
return false;//加锁失败
}
}
公平锁的tryAcquire()方法首先判断锁是否空闲,如果锁空闲则用hasQueuePredecessors()判断自己是否需要排队,如果不需要排队就用CAS操作尝试获取锁,获取锁成功则将持有锁的线程设置为自己。如果锁非空闲,则看是否为自己持有锁,若为自己持有则重入,否则返回false表示尝试获取锁失败,并将自己加入到等待队列。
//判断自己是否需要排队
public final boolean hasQueuedPredecessors(){
Node h, s;
if ((h = head) != null) { //head不为空
if ((s = h.next) == null || s.waitStatus > 0) { //头节点的后继节点线程已取消
s = null;
for (Node p = tail; p != h && p != null; p = p.prev) { //从后往前获取未取消的节点线程,并将最靠前的未取消的节点赋值给s
if (p.waitStatus <= 0) //等待状态是未取消的
s = p;
}
}
if (s != null && s.thread != Thread.currentThread()) //队列中有等待着的线程且不是自己
return true; //说明需要排队
}
//如果head为null,则不需要等待,因为没有等待队列
return false;
}
②ReentrantLock中非公平锁的tryAcquire()方法
static final class NonfairSync extends Sync {
protected final boolean tryAcquire(int acquires){
return nonfairTryAcquire(acquires);
}
}
nonfairTryAcquire()方法的实现在ReentrantLock的Sync内部类里:
abstract static class Sync extends AbstractQueuedSynchronizer {
final boolean nonfairTryAcquire(int acquires){
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) { //非公平锁只要判断锁是空闲的,就直接尝试CAS获取锁,而不会判断自己是否需要排队
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; //加锁失败
}
}
比较会发现,公平锁和非公平锁加锁时,唯一的区别就是:公平锁有hasQueuedPredecessors()判断是否需要排队;而非公平锁只要判断锁是空闲的就直接尝试CAS获取锁,而不会判断自己是否需要排队。
ReentrantLock加锁时,如果通过CAS方式成功将state从0改为1(即获取了锁)就会调用setExclusiveOwnerThread()方法,setExclusiveOwnerThread()方法在AQS的父类AbstractOwnableSynchronizer中:
public abstract class AbstractOwnableSynchronizer {
private transient Thread exclusiveOwnerThread;
protected final void setExclusiveOwnerThread(Thread thread){
exclusiveOwnerThread = thread;
}
protected final Thread getExclusiveOwnerThread(){
return exclusiveOwnerThread;
}
}
exclusiveOwnerThread变量代表当前拥有锁的线程。
总结ReentrantLock加锁原理:
①通过lock()方法加锁,调用lock()方法后内部会以CAS原子方式去尝试修改AQS中持有的state变量,即判断当前锁的状态,只有在state等于0时,锁才是空闲的。
②如果state=0,非公平锁会立刻去尝试获取锁;而公平锁并不能立刻加锁,还要判断等待队列中有没有等待的线程,如果没有等待的线程则直接尝试加锁,如果有等待的线程则优先从队列中获取线程加锁,并把当前线程入队列。
此时如果线程获取锁成功,就会将当前AQS中的exclusiveOwnerThread更新为当前线程对象,以便于后期锁重入时直接返回获取锁成功。
如果最开始修改失败,则调用acquire()方法去获取锁。其方法会内部尝试获取锁一次 ,并且将当前线程添加到等待队列中,然后通过CAS的方式不断自旋,一直获取前驱节点, 如果前驱节点是 head头节点就说明当前节点在队列首部,就尝试获取锁,如果获取成功则更新队列头节点为当前node,并移除当前遍历的前驱节点。如果获取锁失败,则通过前驱节点判断当前线程是否被阻塞,如果当前线程已经被中断,则更新标记位,并且暂停此循环,等待唤醒。
③如果state>0,说明锁被其他线程持有,持有该锁的线程在exclusiveOwnerThread属性中关联。判断exclusiveOwnerThread==currentThread是否成立,如果成立说明当前线程持有该锁,则对state累加1(实现锁重入);如果锁不是当前线程持有,则把该线程加入到队列中等待。
(3)ReentrantLock释放锁
ReentrantLock释放锁会调用unlock()方法,其实就是state -1,并设置owner线程为null ,释放成功就唤醒下一个节点。
public void unlock() {
sync.release(1);
}
ReentrantLock的unlock()方法会调用sync的release()方法,release()的具体实现在AQS里,公平锁和非公平锁的release一样的。在release()方法中,tryRelease()是模板方法。
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); //修改当前拥有锁的线程为null
}
setState(c);//修改锁状态
return free;
}
tryRelease()方法尝试释放当前锁,返回释放后锁资源是不是空闲(true空闲,false不空闲)。如果锁空闲的话会调用unparkSuccessor方法唤醒头节点的下一个节点。
5.ReentrantReadWriteLock读写锁
ReentrantReadWriteLock是一个可重入读写锁,内部提供了读锁和写锁的单独实现。其中读锁用于只读操作,可被多个线程共享;写锁用于写操作,只能互斥访问。
ReentrantReadWriteLock适合读多写少的应用场景。在一些业务场景中,大部分只是读数据,写数据很少,如果这种场景下依然使用独占锁会大大降低性能。因为独占锁会使得本该并行执行的读操作变成了串行执行。
读写锁维护着一对锁:一个读锁和一个写锁。通过分离读锁和写锁,使得并发性比一般的排他锁有了较大的提升:在同一时间可以允许多个读线程同时访问,但是在写线程访问时,所有读线程和其他的写线程都会被阻塞。
读写锁还是沿用AQS中的state变量,高16位表示读锁的数量,低16位表示写锁的数量。
(1)第一个问题:
为什么不用两个变量表示?假设用两个变量。int writeState表示写锁状态。int readState表示读锁状态。
因为分成了两个操作,这两个操作不是原子性的,所以造成了上述问题。那能不能再引入一个锁?比如:
但是这样不就使得读锁变成互斥锁了么?就不可以并发获取读锁的能力了。
所以最后读写锁用一个变量32位的state来表示,state的高16位表示读锁,低16位表示写锁。
(2)第二个问题:
怎么表示每个线程持有的读锁数量?
state变量的高16位表示的是所有线程持有的读锁数量总量。如果要表示每个线程各自持有的变量,就需要用到ThreadLocal。即源码中的这个变量
假设一段时间内,永远只有一个线程去获取读锁,那么就没有必要去初始化ThreadLocal,所以读写锁中有一个first变量。用来记录第一个获取读锁的线程以及持有锁的数量。
读写锁的主要特性:
①公平性:支持非公平(默认)和公平的锁获取方式,其中非公平性能优于公平。
②重入性:支持重入。即读线程获取读锁之后能够再次获取读锁;写线程获取写锁之后能够再次获取写锁,同时也可以获取读锁。
③重入时不支持锁升级,即同一个线程获取读锁后,直接申请写锁是不能获取成功的。
④重入时支持降级,即同一个线程获取写锁后,直接申请读锁是可以成功的。
(3)问题三:线程局部计数器
Sync类定义了一个线程局部变量readHolds,用于保存当前线程重入读锁的次数。如果该线程的读锁数减为0,则将该变量从线程局部域中移除。
static final class HoldCounter {
int count = 0; // 这里使用线程的id而非直接引用,是为了方便GC
final long tid = getThreadId( Thread.currentThread());
}
内部类,用于记录当前线程重入读锁的次数 。
static final class ThreadLocalHoldCounter extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
内部类,继承自ThreadLocal,该类型的变量是每个线程各自保存一份,其中保存的是HoldCounter对象,用set方法保存,get方法获取
private transient ThreadLocalHoldCounter readHolds;
由于readHolds变量是线程局部变量(继承ThreadLocal类),每个线程都会保存一份副本,不同线程调用其get方法返回的HoldCounter对象不同。
readHolds中的HoldCounter变量保存了每个读线程的重入次数,即其持有的读锁数量。这么做的目的是便于线程释放读锁时进行合法性判断:线程在不持有读锁的情况下释放锁是不合法的,需要抛出IllegalMonitorStateException异常。
(4)缓存
Sync类定义了一个HoldCounter变量cachedHoldCounter,用于保存最近获取到读锁的线程的重入次数。
private transient HoldCounter cachedHoldCounter;
设计该变量的目的是:将其作为一个缓存,加快代码执行速度。因为获取、释放读锁的线程往往都是最近获取读锁的那个线程,虽然每个线程的重入次数都会使用readHolds来保存,但使用readHolds变量会涉及到ThreadLocal内部的查找,这是存在一定开销的。有了cachedHoldCounter这个缓存后,就不用每次都在ThreadLocal内部查找,加快了代码执行速度,相当于用空间换时间。
1)读写锁的用法
以缓存为例,多个线程同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行。但是如果有一个线程想去写共享资源,就不应该再有其他线程可以对该资源进行读或写操作。
缓存是⼀个键值对,读缓存时get()⽅法使⽤ReentrantReadWriteLock.ReadLock(),写缓存时put()⽅法使⽤ReentrantReadWriteLock.WriteLock()。这样就可以避免写被打断,实现多个线程同时读。
class Cache {
private Map<String, Object> map = new ConcurrentHashMap<>();
private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
//写缓存
public void put(String key, Object value){
readWriteLock.writeLock().lock();
try {
Log.e(TAG,key+"正在写入缓存: "+ value);
TimeUnit.SECONDS.sleep(1);//模拟耗时
map.put(key, value);
System.out.println("写入缓存完成");
} finally {
readWriteLock.writeLock().unlock();
}
}
//读缓存
public Object get(String key){
Object result = null;
readWriteLock.readLock().lock();
try {
Log.e(TAG, key + "正在读取缓存:");
TimeUnit.SECONDS.sleep(1);//模拟耗时
result = map.get(key);
System.out.println(key+"读取缓存完成");
} finally {
readWriteLock.readLock().unlock();
}
return result;
}
}
多线程调用读写方法:
for (int i = 1; i <= 5; i++) {
final int temp = i;
new Thread(()->{
cache.put("线程名"+temp, "数据"+temp);
}, String.valueOf(i)).start();
}
for (int i = 1; i <= 5; i++) {
final int temp = i;
new Thread(()->{
cache.get("线程名"+temp);
}, String.valueOf(i)).start();
}
2)读写锁源码分析
(1)构造方法
ReentrantReadWriteLock有五个内部类,五个内部类之间也是相互关联的。
ReentrantReadWriteLock有两个变量,分别存放读锁和写锁:
public class ReentrantReadWriteLock implements ReadWriteLock {
private final ReentrantReadWriteLock.ReadL ock readerLock; //读锁
private final ReentrantReadWriteLock.WriteL ock writerLock; //写锁
final Sync sync;
public ReentrantReadWriteLock() {
this(false); //默认为非公平模式
}
public ReentrantReadWriteLock(boolean fair) {
sync=fair?new FairSync():new NonfairSync();
readerLock = new ReadLock(this); //内部把ReentrantReadWriteLock中的sync传递给ReadLock的sync
writerLock = new WriteLock(this);//内部把ReentrantReadWriteLock中的sync传递给WriteLock的sync
}
}
(2)写锁(可重入的排他锁)
public static class WriteLock implements Lock, java.io.Serializable {
private final Sync sync;
protected WriteLock( ReentrantReadWriteLock lock) {
sync = lock.sync; //获取ReentrantReadWriteLock的sync对象
}
}
①写锁加锁
public void lock() {
sync.acquire(1);
}
public boolean tryLock( ) { //尝试获取锁
return sync.tryWriteLock();
}
写锁使用lock()方法加锁,lock()调用AQS里的acquire(1)方法:
AbstractQueuedSynchronizer.java:
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
这里与ReentrantLock一样,不同的是tryAcquire()的具体实现。在ReentrantReadWriteLock中tryAcquire方法():
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c); //写线程数量(即获取独占锁的重入数)
if (c != 0) { //锁不空闲,即有线程加锁(可能是读锁可能是写锁)
//有线程加了读锁但没有线程加写锁,或者线程加的是写锁但加锁的线程不是当前线程,则直接返回false,无法获取写锁
if (w == 0 || current != getExclusiveOwnerThread())
return false;
//有线程加了写锁,且就是当前线程加的,则可以获取写锁(重入),但是需要判断是否超过最高写线程数量
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
setState(c + acquires); //写锁重入成功
return true;
}
//能够执行到这说明state为0,表示当前没有任何线程加读锁或写锁
if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
return false;
//当前线程不阻塞并且CAS成功获取到写锁,则设置写锁的持有线程为当前线程
setExclusiveOwnerThread(current);
return true;
}
该方法除了重入条件(当前线程为获取了写锁的线程)之外,增加了一个读锁是否存在的判断。如果存在读锁,则写锁不能被获取,原因在于:读写锁要确保写锁的操作对读锁可见,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当前写线程的操作。因此只有等待其他线程都释放了读锁,写锁才能被当前线程获取,而写锁一旦被读取,则其他读写线程的后续访问均被阻塞。
其中,writerShouldBlock()方法表示当前线程是否需要阻塞,false表示不需要阻塞直接获取锁,true表示要阻塞无法获取锁。该方法在公平锁和非公平锁中的实现不同。
static final class FairSync extends Sync {
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
}
static final class NonfairSync extends Sync {
final boolean writerShouldBlock() {
return false;
}
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclysive();
}
}
如果是非公平锁,则writerShouldBlock始终返回false,因为不需要排队,非公平策略下可直接参与抢锁;如果是公平锁,则需要查看当前队列中是否存在其他排队的线程,如果存在则返回true,当前线程不能获取写锁,否则返回false可直接CAS获取锁。
写锁是一个支持重进入的排他锁。如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁时,读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程,则当前线程进入等待状态。
②写锁释放锁
public void unlock() {
sync.release(1);
}
写锁使用unlock()方法释放锁,unlock()调用AQS里的release(1)方法:
AbstractQueuedSynchronizer.java:
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()是模板方法,在ReentrantReadWriteLock中,该方法的实现为:
protected final boolean tryRelease(int releases){
if(!isHeldExclusively()) //如果写锁的持有者不是当前线程,直接抛出异常
throw new IllegalMonitorStateException();
int nextc = getState() - releases;//计算释放后的写锁数
//free表示写锁是否被完全释放
boolean free = exclusiveCount(nextc) == 0;
if(free)//如果写锁状态为0,表示写锁被完全释放,即独占模式被释放
setExclusiveOwnerThread(null);
setState(nextc);
return free; //返回独占模式是否被释放
}
写锁的释放与ReentrantLock的释放过程基本类似,每次释放均减少写状态,当写状态为0时表示写锁已被释放,从而等待的读写线程能够继续访问读写锁,同时前次写线程的修改对后续读写线程可见。
(3)读锁—可重入的共享锁
读锁是一个可重入的共享锁,它能够被多个线程同时持有,在没有其他写线程访问时,读锁总是会获取成功。
①读锁加锁
读锁的加锁调用ReadLock内部类中的lock方法:
public void lock() {
sync.acquireShared(1);
}
调用AQS的acquireShared()方法:
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
其中tryAcquireShared是模板方法,由子类具体实现。
ReentrantReadWriteLock的tryAcquireShared方法:
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
//exclusiveCount获取写线程数量。如果存在写锁,且持有锁的线程不是当前线程,则获取失败,返回-1
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; //第一个读线程占用的资源数为1
} else if (firstReader == current) {//原读锁数量不为0,且第一个读线程即为当前线程,更新该线程占用的资源数,即锁重入
firstReaderHoldCount++;
} else { //原读锁数量不为0,且第一个读线程不为当前线程
HoldCounter rh = cachedHoldCounter; //获取计数器
if (rh == null || rh.tid != LockSupport.ge tThreadId(current)) //计数器为空或者计数器的tid不为当前线程的tid
cachedHoldCounter = rh = readHolds.get();// 获取当前线程对应的计数器
else if (rh.count == 0) //当前线程的计数为0
readHolds.set(rh);//设置readHolds中值为当前线程的计数器
rh.count++; //count加1
}
return 1;
}
//如果上面三个条件有一个不满足
return fullTryAcquireShared(current);
}
其中readerShouldBlock用于判断当前读线程是否需要等待。在公平锁和非公平锁中有不同的实现。
非公平锁中readerShouldBlock方法:
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null && (s = h.next) != null && !s.isShared() && s.thread != null;
}
公平锁的readerShouldBlock方法:
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
判断等待队列中是否有线程在排队。
tryAcquireShared方法最后一行表示在不满足获取读锁条件时会调用fullTryAcquireShared方法:
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
int c = getState();
if (exclusiveCount(c) != 0) {//写锁数量不为0
if (getExclusiveOwnerThread() != current) //不是当前线程持有写锁,直接返回-1
return -1;
} else if (readerShouldBlock()) { //写锁数量为0,且读线程需要被阻塞
if (firstReader == current) { //当前线程是第一个读线程,不操作
// assert firstReaderHoldCount > 0;
} else { //当前线程不是第一个读线程
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) { //计数器还是为空或者计数器的tid不为当前正在运行的线程的tid
rh = readHolds.get();
if (rh.count == 0)
readHolds.remove();
}
}
if (rh.count == 0)
return -1;
}
}
if (sharedCount(c) == MAX_COUNT) //读锁数量大于最大值,直接抛出异常
throw new Error("Maximum lock count exceeded");
if (compareAndSetState(c, c + SHARED_UNIT)) {
if (sharedCount(c) == 0) { //读线程数量为0,逻辑同上
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
if (rh == null)
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh;
}
return 1;
}
}
}
在tryAcquireShared函数中,如果下列三个条件不满足(读线程是否应该被阻塞、小于最大值、比较设置成功)则会进行fullTryAcquireShared函数中,它用来保证相关操作可以成功。
tryAcquireShared的返回值说明:
负数:获取失败,线程会进入同步队列阻塞等待;
0:获取成功,但是后续以共享模式获取的线程都不可能获取成功;
正数:获取成功,且后续以共享模式获取的线程也可能获取成功
firstReader、firstReaderHoldCount分别用于记录第一个获取到写锁的线程及其持有读锁的数量;
cachedHoldCounter用于记录最后一个获取到写锁的线程持有读锁的数量;
readHolds是一个线程局部变量(ThreadLocal变量),用于保存每个获得读锁的线程各自持有的读锁数量;
tryAcquireShared的流程如下:
1、如果其他线程持有写锁,那么获取失败(返回-1);
2、否则,根据公平性判断是否应该阻塞。如果不用阻塞且读锁数量未饱和,则CAS请求读锁。如果CAS成功,获取成功(返回1),并记录相关信息;
3、如果根据公平性判断应该阻塞,或者读锁数量饱和,或者CAS竞争失败,那么交给完整版本的获取方法fullTryAcquireShared去处理;
其中步骤2如果发生了重入读(当前线程持有读锁的情况下,再次请求读锁),但根据公平性判断该线程需要阻塞等待,而导致重入读失败。按照正常逻辑重入读不应该失败。不过tryAcquireShared并没有处理这种情况,而是将其放到了fullTryAcquireShared中进行处理。此外CAS竞争失败而导致获取读锁失败,也交给fullTryAcquireShared去处理。
fullTryAcquireShared方法是尝试获取读锁的完全版本,用于处理tryAcquireShared方法未处理的:CAS竞争失败或因公平性判断应该阻塞而导致的重入读失败这两种情况。
fullTryAcquireShared其实和tryAcquire存在很多的冗余之处,但这么做的目的主要是让tryAcquireShared变得更简单,不用处理复杂的CAS循环。
fullTryAcquireShared主要是为了处理CAS失败和readerShouldBlock判true而导致的重入读失败,这两种情况在理论上都应该成功获取锁。fullTryAcquireShared的做法就是将这两种情况放在for循环中,一旦发生就重新循环,直到成功为止。
②读锁的释放
读锁的释放调用ReadLock内部类的unlock方法:
public void unlock() {
sync.releaseShared(1);
}
调用AQS的releaseShared()方法:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
其中tryReleaseShared是模板方法,由子类具体实现。
ReentrantReadWriteLock的tryReleaseShared()方法:
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) { //当前线程是第一个读线程
if (firstReaderHoldCount == 1) //当前线程的计数为1,释放后需要清除
firstReader = null;
else //否则直接-1
firstReaderHoldCount--;
} else {//当前线程不是第一个读线程
HoldCounter rh = cachedHoldCounter; //获取缓存的计数器
if (rh == null || rh.tid != getThreadId(current)) //计数器为空或者计数器的tid不为当前正在运行的线程的tid
rh = readHolds.get(); //获取当前线程的计数器
int count = rh.count; //获取计数
if (count <= 1) { //计数小于等于1,则需要移除该线程对应的计数器
readHolds.remove();
if (count <= 0) //计数小于等于0,直接抛出异常
throw unmatchedUnlockException();
}
--rh.count; //计数减一
}
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
//CAS更成功后返回状态是否是0,即锁是否完全被释放
return nextc == 0;
}
}
该方法返回锁(共享锁+独占锁)是否完全被释放。