AQS 源码(ReentrantLock、ReentrantReadWriteLock 为例)

AQS 源码(ReentrantLock、ReentrantReadWriteLock 为例)

前言

      AQS(Abstract Queued Synchronizer) 是 Java 里面并发篇的一个重要的抽象类,像 ReentrantLock、ReentrantReadWriteLock 以及很多并发包中的都依赖于这个抽闲类,可见这个类有多种重要。吃透这个类,并发这块会有更深的理解。这里以 ReentrantLock、ReentrantReadWriteLock 这2个类的源码展开分析,只分析主要的源码,并不会将所有的流程方法分析完,因为明白了这2个类的主要地方之后,再看别的地方或者别的并发包的类时候,就一点不怂了,不会那么难以理解了。

      AQS 内部使用 CAS 自旋 + Unsafe 去实现同步操作,内部使用双向链表存储等待的线程。在 ReentrantReadWriteLock 中核心的思想使用了位运算的操作,所以比 ReentrantLock 难理解的多,我在看时候也是一边看文章,一边模拟二进制的位操作才明白。 所以看之前需要这三点:CAS、链表、位运算,如果不明白还是做下功课吧,不然就跟看天书一样。 其中 CAS 与 链表搜下文章看下就行,不是太难理解。主要位运算需要多试验操作一下,当然我下面也会使用做几个示例去说明。

      记得刚自学完并发时候,就直接怼源码,找了大量的文章,最后把自己看到迷迷糊糊,感觉是懂非的。时至今日,再去看这些东西就有点脉络清晰的感觉了,能把源码读下去。看个两三篇文章就能明白个大概。源码肯定不是看一遍就会懂的,需要自己多断点多调试。把流程走个两三遍。要是遇到不懂的跳过,保证整个流程大概知道。然后再去深究细节。如果以下如有理解偏差的地方可以留言指出。

ReentrantLock

原理

继承结构
AQS 源码(ReentrantLock、ReentrantReadWriteLock 为例)_第1张图片
ReentrantLock
      有三个内部类 Sync、FairSync、NonfairSync。从 ReentrantReadWriteLock、CountDownLatch、Semaphore … 都能看到这三个的影子,所以在 java 中并发包中很多工具类都是原理都基本差不多。
Sync类:抽象类,作为同步的最重要基础类,ReentrantLock 的很多方法都是需要依赖 sync 类。就是这个同步的基础类去实现AQS。

     FairSync:公平锁,实现上面的 Sync,也可以说是 Sync 的细化,当线程需要挂起时候,放入队列尾部。然后从表头开始唤醒

     NonfairSync:非公平锁,也是实现了 Sync。当线程需要等待时候,也是会进入队列,并且唤醒时候从表头唤醒。不同的是当检查到信号量为 0 时候,不管等待队列中是否存在等待节点,都会竞争执行

AbstractQueuedSynchronizer
     就是此篇的主角,Java并发包中的核心基础类。其中比较主要的地方如下:
变量
     state:信号量,就是一个计数器,只是说的比较牛逼而已。存储锁的重入次数。假设一个线程 A 获取了锁,就会将信号量+1,然后线程 B 去获取锁时候发下已经 >=1 了就去等待了。然后线程 A 如果再获取锁时候,信号量再+1。说白了就是标识有没有线程获取锁,获取了之后,那个线程每次进来就+1,别的线程就只能等待。

     head:链表头,表头不存数据,当来了一个等待线程时候,就会进入链表第二个节点
     tail:链表尾部

内部类
     Node:作为双向等待队列的节点,里面存储 挂起的线程、等待状态(waitStatus)。其中等待状态比较重要状态如下:
     0:初始化状态
     CANCELLED(1):取消状态,表示一个线程取消了等待,可能中断了
     SIGNA(-1)L:表示等待状态,需要被唤醒。
     CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
     PROPAGATE(-3):表示下一次共享式同步状态获取将会被无条件的被传播下去(读写锁中存在的状态,代表后续还有资源,可以多个线程同时拥有同步状态)
AQS 源码(ReentrantLock、ReentrantReadWriteLock 为例)_第2张图片
     负值表示结点处于有效等待状态,而正值表示结点已被取消。所以源码中很多地方用>0、<0来判断结点的状态是否正常。

AbstractOwnableSynchronizer
      这和是一个抽象了,AQS 依赖于这个类,主要用于存储当前获取锁的线程。别的就没啥了

获取锁源码

AQS 源码(ReentrantLock、ReentrantReadWriteLock 为例)_第3张图片
这里以公平锁为示例展开,上面流程图为使用 lock() 上锁时候的调用流程图下面就以每个环节去详细说明

acquire(int arg)

      以独占的方式获取锁,在这过程中忽略线程中断的状态.。这个方法在 AQS 中,具体实现锁的去调用的( ReentranLock > FairSync),主要思想为,你先去尝试获取下锁,如果获取不到那就去进入队列。而如何获取锁的逻辑为抽象方法让自己具体去实现,

public final void acquire(int arg) {
    //先调用 tryAcquire(arg) 尝试去获取锁,如果获取失败就去加入队列
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
tryAcquire(int arg)

     实现自己的尝试获取锁逻辑,此方法为抽象类在 ReentranLock > FairSync 中。

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;
}

详细说明如下:
第8行:if (c == 0)
如果信号量等于0,那分为2中情况
1. 目前没有线程获取锁,可以获取锁了
2. 上个持有锁的刚释放,就是这个地方公平锁和非公平处理有点不同。公平锁是查看队列是否存在等待节点,非公平步检查竞争执行

第9行:if (!hasQueuedPredecessors() && compareAndSetState(0, acquires))
hasQueuedPredecessors(): 查看等待队列中是否存在等待节点。
compareAndSetState(0, acquires):信号量原子自增,表示锁已经被持有了
如果说队列中存在了等待节点,那就返回 false 去增加队列。如果不存在那就尝试 cas 操作将信号量增加

第10行:setExclusiveOwnerThread(current)
当前线程获取了锁,那就将当前线程记录起来,这个方法在 AbstractOwnableSynchronizer 接口中,这个接口很简单,就是提供 返回当前获取锁线程、记录当前获取锁的线程。至此获取到锁。

第14行:else if (current == getExclusiveOwnerThread())
     如果成立,那就说明之前这个线程已经获取了锁,这里就是我们常用的锁的重入,每次信号量再 +1,在释放锁时候每次再去 -1,直到将所有的锁释放。else if 内部就是将信号量增加。

hasQueuedPredecessors()

这个方法主要就是判断队列中第一个节点(非头结点)是否为空。

public final boolean hasQueuedPredecessors() {
    Node t = tail;    //尾节点
    Node h = head;    //头节点
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

h != t:头不等于尾意思就是不等于 null,只有当链表中没有节点才会相等(都是null)。

((s = h.next) == null:头结点下一个节点是否为null(这里接统称第二个节点)

s.thread != Thread.currentThread():第二个节点的线程是否为当前线程。这里应该有个疑问,既然线程已经在等待队列在睡眠等待了,怎么还会再活跃起来又去请求锁了?还真可能出现这种情况。比如说线程 A 去第一次尝试获取锁,发现锁已经被占用了,然后入队列了,这个时候还没有挂起线程,又第二次去尝试获取锁了,所以就会出现当前线程 与 队列中的线程一样了。这里我也是思考良久才明白为啥需要这样
AQS 源码(ReentrantLock、ReentrantReadWriteLock 为例)_第4张图片

addWaiter(Node.EXCLUSIVE), arg)

方法在 AQS 中 创建一个新节点,然后添加到队列

方法的调用:addWaiter(Node.EXCLUSIVE), Node.EXCLUSIVE 表示此节点为独占方式

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);  //创建新节点,指定独占模式
    Node pred = tail;   //获取尾节点
    if (pred != null) {   //尾节点不等于null,表示存数据
        node.prev = pred;   //将新节点执行加到尾部
        if (compareAndSetTail(pred, node)) {   //CAS 尾节点指向新增节点
            pred.next = node;
            return node;
        }
    }
    
    //如果上面的快速修改失败,那就 CAS 自旋去修改。在源码里面很多地方都有类似的写法
    //先快速修改,如果失败了。那就再 CAS 自旋
    //如果是刚看这个源码,我不建议去把注意力放在 enq(node) 这些不主要的地方,只要知道是 CAS 自旋加入队列尾部就行,不然容易晕晕乎乎,刚开始我就是这样
    enq(node);  
    return node;
}
acquireQueued(final Node node, int arg)

加入队列,过程中会尝试继续获取锁,此方法在 AQS 中

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);
    }
}

第8行:if (p == head && tryAcquire(arg))
上一个节点如果是头结点,证明整个等待队列就一个节点,就是这个新创建的节点,并没有别的线程竞争。这个时候可以去再尝试去获取一下锁,也可能这个时候上个锁就释放嘞。如果获取锁了,那就将头尾节点都赋值为 null。

第14行:shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()
     shouldParkAfterFailedAcquire
          方法是每次一个节点睡眠前先检查一下前面的节点状态。如果没有这一步,万一我前面的节点是一个跳过的节点,那是不是我永远都不可能被唤醒了。简单说下内部的源码:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)  //判断自己前面节点是不是需要被唤醒的
        return true;
    if (ws > 0) {   //如果是跳过节点,那就一直向前查找,直到前面的都不是跳过节点
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {  //别的情况都标记为等待唤醒
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

     parkAndCheckInterrupt():内部挂起这个线程。线程醒时候判断下是不是已经被中断了。

cancelAcquire(node);

此方法在 AQS 中,将此节点从队列中移除

private void cancelAcquire(Node node) {
    if (node == null)
        return;

    //节点中引用的线程清空
    node.thread = null;

    //这里绕死人,别纠结,我直接告诉你,只要知道 >0 的节点需要跳过,这里就是从屁股后面开始找,找到就修改引用然后跳过
    Node pred = node.prev;
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;

    Node predNext = pred.next;
    node.waitStatus = Node.CANCELLED;  //标记取消节点

    //下面都去移除这个节点的,不细纠了
    if (node == tail && compareAndSetTail(node, pred)) {
        compareAndSetNext(pred, predNext, null);
    } else {
        int ws;
        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
    }
}

到这里 lock(); 上锁的整个过程就结束了,自己多断点调试几遍,上面明白个大概之后,释放锁基本上就差不多明白了。

释放锁源码

释放源码总体逻辑不多,只需要减去信号量,如果完全释放了锁 ( state = 0 ),再将记录获取锁的线程变量清空。最后唤醒下队列中的线程就行了。

release(int arg)

释放对应的信号量,方法在 AQS 中

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(int releases)

尝试释放信号量,在 AQS 中调用,需要具体的子类去失效释放操作。

protected final boolean tryRelease(int releases) {
    //减去信号量,这个锁每次都是 - 1
    int c = getState() - releases;
    
    //如果是锁调用释放锁的线程,不是获得所的线程,直接报错。
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    
    //如果的等于0表示,已经完全释放完了,那就清空记录获取线程的变量
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    
    //将新的信号量重新设置。
    setState(c);
    return free;
}
unparkSuccessor(Node node)

唤醒队列中第一个等待的线程,方法在 AQS

//这里传进来的是头节点
private void unparkSuccessor(Node node) {
    //这块不清楚不太明白想干啥,为什么在这个锁中将头结点的状态置为0。0为初始化状态。
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, 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;
    }
    
    //唤醒这个节点
    if (s != null)
        LockSupport.unpark(s.thread);
}

上面的方法属于 AQS 的我都有备注出来,可以看出大部分属于 AQS 提供的。所以可见这个类有多重要。这里非公平就不说了,2个都差不太多。

ReentrantReadWriteLock

     读写锁比上面的 ReentrantLock 就麻烦的多了,如果有上面的基础的话建议可以看下这个锁的源码,如果没有阅读过 AQS 源码,直接从读写锁开始看,那我感觉跟读天书没有区别。很多人就是卡在二进制操作部分,云里雾里的感觉。我当时也是通过在二进制方面多做试验,反复推敲才捋顺的。我将通过一些例子去说明读写锁,在二进制操作信号量时候的原因。

原理

继承结构图
AQS 源码(ReentrantLock、ReentrantReadWriteLock 为例)_第5张图片

     先来梳理下这个类的结构,其实与 ReentrantLock 结构差不太多,ReentrantLock 也是内部有个 Sync 同步基础类,然后再细化出 2 个同步实现,分为公平锁与非公平锁。
     但是在 ReentrantReadWriteLock 中多出来 2 个内部锁,读锁与写锁。这 2 个锁内部都有一个 sync 变量,依赖于这个实现实现同步,在初始化 ReentrantReadWriteLock 就指定了使用公平同步器还是非公平同步器。

核心思想

      由于在了解了 ReentrantLock 之后有了一定的基础,我认为在明白 ReentrantReadWriteLock 最核心的地方就在于信号量的表示,而难点就在与一个变量,既表示读锁重入数据量,又表示写锁重入数量。使用位运算去操作去获取对应重入次数。这里就要明白二进制了的操作(很多操作信号量的地方都需要去使用二进制的思维去多试验)。那么如何通过一个变量来表示2个锁的数量的呢?

      信号量 state 为 int 类型,我们都知道一个 int 为 4 字节,1 字节为 8位,也就是 32 位。读写锁将高 16 位记录读锁,低 16 位记录写锁,读锁还好理解,主要写锁如果要获取重入次数,还要右位移16位,然后获取比较绕点。 表现形式如下。
在这里插入图片描述
二进制表示的 信号量 举几个例子:

--*** 读锁 ***
0000 0000 0000 0001 = 1个读锁
0000 0000 0001 0000 = 16个读锁
1000 0000 0000 0000 = 32768个读锁
1111 1111 1111 1111 = 65535个读锁

-- *** 写锁,通过右位移 16 位获取锁 ***
1 0000 0000 0000 0000 = 十进制对应 65536
                    1 = 位移16位后的二进制
------------------------------------
                    1 = 最后得到写锁重入110 0000 0000 0000 0000 = 十进制对应 131072
                    10 = 位移16位后的二进制
------------------------------------
                     2 = 最后得到写锁重入211 0000 0000 0000 0000 = 十进制对应 196608
                    11 = 位移16位后的二进制
------------------------------------
                     3 = 最后得到写锁重入3

这里总结一下上面规律:

  1. 读锁最大也就只能表示 65535 次重入,
  2. 写锁是从 65536 开始的
  3. 而写锁每次获取个数时候,需要右位移 16 位,才能获取。相当于抹除低 16 位就是写锁的个数了
  4. 写锁获取时候每次自增信号量时候,需要加上 65536,这是因为低16为不能有数字,不然获取时候就会获取错误。因为获取时候直接抹除低 16 位了

     上面有一个点我要说明下,我们可以通过示例发现,65536 表示1个写锁,131072 表示2个写锁,196608 表示3个写锁。为什么每次都是加上 65536(自己相减一下就知道了,中间都差 65536)才表示加了一个锁,而不是 +1 自增去表示一个锁?我之前也是纳闷这个,后来做了试验就明白了。那么就来试验下,65536(表示一个写锁),加上 1 都会得到什么。

1 0000 0000 0000 0001 = 65537
                    1 = 位移16位后的二进制
-------------------------
                    1 发现得到写锁1

发现规律没?低位如果有数据,位移后直接抹掉了,就意味着出来的数据就不准确了。那么为什么加上 65536 就不会导致为以后数据不准确?那就要看一下 65536 的二进制是多少

1 0000 0000 0000 0000

其实就代表高位开始,每一个高位的二进制 +1

如果之前没接触过位移,上面的二进制操作看会比较朦胧也没啥(赶紧去看下二进制运算的最基本的操作,然后在 demo 几下就行了,有个大概就可以),我下面还会根据一些示例去说明二进制都做了什么操作

读锁源码

在 “核心思想” 那里说了那么多理论,那么这里就是对应的代码了

static final int SHARED_SHIFT   = 16;

//1 左位移 16 位 = 65536(1 0000 0000 0000 0000)
//用来每次读锁重入信号量就加上一个这个数
static final int SHARED_UNIT    = (1 << SHARED_SHIFT);

//1 左位移 16 位 - 1 = 65535(1111 1111 1111 1111)
//最大重入次数
static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;

static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

//获取写读锁的个数,这里就是读锁右移 16 位后获取到读锁个数
static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }

//获取写锁的个数,& EXCLUSIVE_MASK(最大的写锁个数)。信号量不管怎么 & 都会获取的都是 65535 之间
//所以,如果此时要是写锁持有的话,始终获取 0
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

获取读锁流程图
AQS 源码(ReentrantLock、ReentrantReadWriteLock 为例)_第6张图片
这里流程其实与 ReentrantLock 差不多,主要就是位运算比较绕了

acquireShared(int arg)
public final void acquireShared(int arg) {
    //尝试获取锁了
    if (tryAcquireShared(arg) < 0)
        //获取失败那就如队列
        doAcquireShared(arg);
}
tryAcquireShared(int unused)

尝试获取共享锁

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);
    if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) {
        if (r == 0) {
            //firstReader:读锁第一次进来,记录第一个获取读锁的线程
            //firstReaderHoldCount:如果第一个获取读锁的线程再次重入,这个变量用记录重入的次数
            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);
            rh.count++;
        }
        return 1;
    }
    
    //获取读锁失败了(比如说,在CAS更新时候与预期值不一样),那就再尝试下获取
    return fullTryAcquireShared(current);
}

第4行:if (exclusiveCount© != 0 && getExclusiveOwnerThread() != current)
exclusiveCount©
          获取写锁个数。内部直接用:信号量 & 写锁最大重入范围(0-65535)(c & EXCLUSIVE_MASK)。那么为什么我能 & 一下就能知道信号量是读锁占用还是写锁占有了?这就需要知道二进制怎么操作的了,这里再重复一下,低16 位占有写锁,高 16 位占有读锁。那么写锁低 16 位来表示(65535):1111 1111 1111 1111

假设情况1:信号量=55就不用想了,肯定是写锁(0-65535之间)。那么二进制 & 一下为:
1111 1111 1111 1111 = 65535
0000 0000 0000 0101 = 5
-----------------------------
0000 0000 0000 0101 = 5

假设情况2:信号量=65534
1111 1111 1111 1111 = 65535
1111 1111 1111 1110 = 65534
-----------------------------
1111 1111 1111 1110 = 65534
获得写锁个数为65534

假设情况3:信号量=65536  注意了这里超过了65535
0 1111 1111 1111 1111 = 65535
1 0000 0000 0000 0000 = 65536  
-----------------------------
0 = 0
获得写锁个数为 0
如果不明白多做一下 demo 看下最后出来的数

     有没有发现规律? '&'运算的作用,只要信号量在 0-65535 之间, 怎么& 获取的都是自己,超过了 65535 只会获取到低 16 位的对应数,反正写锁个数不会超过 65535。
      如果小于65535 那就是写锁,直接获取就行了。但是 jdk 没有这也做,灵活的采用位运算,虽然效率是高,代码也简洁了,但是可读性就贼低了,不知道有多少人挂在位运算的云里雾里了

exclusiveCount© != 0 && getExclusiveOwnerThread() != current
     就表示有线程持有了写锁,并且不是当前这个线程(锁降级),那就返回 -1 去进队列吧

     那这里有个疑问,目前这个代码是使用 readlock.lock(); 才进来的,说明我要获取读锁,发现已经有写锁占有了,直接返回 -1 进入等待就行了,干嘛还要再判断下 getExclusiveOwnerThread() != current ?
      这是因为在读锁里面还是可以再获取写锁的,所以需要判断下是否为当前,线程有又获取了读锁(锁降级了),如果是,那就不要进队列了。(这里跑下代码跟下你就明白了,场景为:一个线程先使用先打开写锁,然后再使用读锁)

第7行:int r = sharedCount©;
      获取读锁的个数,采用右位移 16 为,就可以获取高位的 16 为是多少了,那么什么意思呢?看下下面的二进制运算就明白了:
     信号量=65536,上面说过先认为大于 65535 就是读锁,所以这就表示读锁个数为 1

1 0000 0000 0000 0000 = 65536
                    1   (位移16位后)
------------------------
                    1 个读锁

10 0000 0000 0000 0000 = 131072
                    10 (位移16位后)
------------------------
                    2 个读锁

     因为高16为就表示读锁,可以发现右移16位后,获取到高位剩下的就是锁的数量如果还不明白,那就多试几个数,会慢慢找到规律的

第8行:if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT))
readerShouldBlock(): 这里分公平和非公平,取决于你实例化是传的参数然后走不同方法。
1. 公平:
      内部判断队列中是否存在节点,如果不存在直接就获取锁,不用进队。如果队列有节点那就进队列去等待。
2. 非公平:
     内部走了一串判断其实目的就是为了获取表头的下一个节点是不是共享节点(表头不存数据),不是共享节点那就是独占节点,独占节点其实就是写锁,
      如果存在写锁,因为在非公平模式下,写锁的优先级比较高,所以读锁要让着写锁,那怎么让呢?
     其实就是让读的线程直接进队列去。 因为发现了队列中第一个节点居然是写节点,那就赶紧然当前获取到线程的锁赶紧执行完,然后好执行写锁,就是
     通过这种方式来让步的。可以自己模拟一下场景,三个线程,顺序是:读、写、读,这个时候后面2个都会进入队列

     总结就是:在获取读锁时候,如果队列中有节点,处理方式为:公平方式就是直接进队列。非公平方式是判断第一个节点是不是写节点,如果是,就进入队列。

r < MAX_COUNT:判断读锁重入次数是不是达到了上限

compareAndSetState(c, c + SHARED_UNIT):
     原子的将信号量加上 65535(SHARED_UNIT),按自己的正常理解是当读锁重入时候信号量应该每次自增1。但是现实不是这样的,而是每次加上 65535,那为啥要这样呢?
     这个我也是懵逼了一下,然后看了下二进制,才顿悟了。如果每次自增1,会导致最后获取读锁数量不对。看下下面几组数据:

情况1(正常情况):信号量为:65536 
1 0000 0000 0000 0000 = 65536
                    1   (位移16位后)
------------------------
                    1


情况2(正常情况):信号量为:65536 * 2 = 131072
10 0000 0000 0000 0000 = 131072
                    10   (位移16位后)
------------------------
                    2           

情况3(正常情况):信号量为:65536 * 3 = 196608
11 0000 0000 0000 0000 = 196608
                    11   (位移16位后)
------------------------
                     3
                     
情况4(异常情况):我们假设信号量为自增的方式,看下位移16为之后的结果,信号量为:65537
1 0000 0000 0000 0001 = 65537
                    1   (位移16位后)
------------------------
                    1  最后发现应该为2个读锁,但是位移后只获取一个

     通过上面4个例子可以看出,位移16位后剩下都是读锁的个数了,如果此时低位(第四个例子)低位有值,那这些值将会丢失,导致获取不准确。

fullTryAcquireShared(Thread current)

其实这里尝试获取的代码与上面差不太多,可以认为外面的是快速获取,这里就去自旋获取

final int fullTryAcquireShared(Thread current) {
    HoldCounter rh = null;
    for (;;) {
        int c = getState();
        
        //查看是否被写锁持有,与 不是写锁内重入了
        if (exclusiveCount(c) != 0) {
            if (getExclusiveOwnerThread() != current)
                return -1;
                
        //公平时候,队列有节点就进队列
        //非公平时候,队列中又节点,并且第一个节点为写节点时候才进来。
        } else if (readerShouldBlock()) {
            if (firstReader == current) {   //看下当前线程是不是第一个进来的读线程。
                // assert firstReaderHoldCount > 0;
            } else {
                //下面都是代码意思就是,走到这就表示应该进入队列的,因为队列中都节点在等待,所以检查一下当前是不是记录了什么东西
                //如果有就都删除,返回 -1 表示去入队列去
                if (rh == null) {    
                    rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current)) {
                        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) {
                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; // cache for release
            }
            return 1;
        }
    }
}
doAcquireShared(int 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 快的逻辑就是,判断下这个新创建的节点是不是队列中唯一个节点,如果是,那就尝试下再次获取下锁,
            //如果获取到了那就将新建的节点移除
            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);
    }
}

到这就差不多了,上面的都大概理解的话,像 获取写锁、unlock(); 这些都可以自己去看个明白了。上面的要多调试多试验。

你可能感兴趣的:(多线程)