JUC源码解析--ReentrantLock和AbstractQueuedSynchronizer

1.前言

在了解j.u.c包的时候,总是会避不开AbstractQueuedSynchronizer这个抽象类,这个类是j.u.c包多线程操作的核心类,像CountDownLatch,ReentrantLoack,Semaphore等类都是以此类为基础实现的。本篇文章将尝试通过阅读AbstractQueuedSynchronizer的源码,去了解并发大师Doug Lea在解决高并发问题时的思路。

2.ReentrantLock

在处理高并发任务时,很多时候都会用到ReentrantLock对线程共有资源加锁,解决因多线程竞争共有资源而产生的问题。
ReentrantLock的典型使用使用方式Doug Lea在ThreadPoolExecutor中的addWaitor()方法中已经给出示范。

final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
    // Recheck while holding lock.
    // Back out on ThreadFactory failure or if
    // shut down before lock acquired.
    int rs = runStateOf(ctl.get());

    if (rs < SHUTDOWN ||
        (rs == SHUTDOWN && firstTask == null)) {
        if (t.isAlive()) // precheck that t is startable
            throw new IllegalThreadStateException();
        workers.add(w);
        int s = workers.size();
        if (s > largestPoolSize)
            largestPoolSize = s;
        workerAdded = true;
    }
} finally {
    mainLock.unlock();
}

其中workers是一个由多个线程共有的HashSet,HashSet的add方法是线程不安全的,因此我们用ReentrantLock加锁,保证同一时刻只能有一个线程进行add操作。

看一下ReentrantLock的类结构

    private final Sync sync;
    abstract static class Sync extents AbstractQueuedSynchronizer{ 
        abstract void lock();
    
        final boolean nonfairTryAcquire(int acquires) {...}
        
        ...
    }
    static final class NonfairSync extends Sync {...}
    
    static final class FairSync extends Sync { 
        final void lock() {acquire(1);}
        
        protected final boolean tryAcquire(int acquires) {...}
    }

    //ReentrantLock获取锁的方法
    public void lock() {
        sync.lock();
    }
    ...

可以看到ReentrantLock中存在一个继承了AbstractQueuedSynchronizer类的Sync对象,ReentrantLock的lock方法会调用sync的lock方法,sync的lock方法由两个实现类NonfairSync和FairSync实现。

3.AbstractQueuedSynchronizer

再来简单看一下AbstractQueuedSynchronizer的类结构。

    //构建队列的节点类
    static final class Node{
        volatile int waitStatus;	//节点的等待状态,0代表初始化,-1代表等待唤起
        volatile Node prev;				//上一个节点
        volatile Node next;				//下一个节点
        volatile Thread thread;		//Node所在的线程
        Node nextWaiter;
    }
    private transient volatile Node head;	//头节点
    private transient volatile Node tail;	//尾节点
    private volatile int state;	//锁状态

可以看出AbstractQueuedSynchronizer中维护了一个由Node构成的双向链表,指明了head和tail节点,并且通过state来管理锁的状态,当state=0的时候代表锁未被持有,当state>0时代表锁已被持有。Node类中的prev, next以及AQS的head, tail, state对象都是volatile标示的,方便进行CAS操作。

4.ReentrantLock获取锁的源码分析

下图简单展示了ReentrantLock在执行lock方法时的调用过程。

sync对象在lock()方法中调用AbstractQueuedSynchronizer的acquire()方法。代码如下

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

acquire方法会首先调用tryAcquire()方法,尝试获取锁,返回true代表成功获取锁。tryAcquire()方法在AQS的实现类,也就是ReentrantLock中的Sync类中实现。

protected final boolean tryAcquire(int acquires) {
    //获得当前线程的线程对象
    final Thread current = Thread.currentThread();
    int c = getState();
    //锁是否被持有
    if (c == 0) {
        //队列中是否有线程在等待获取锁
        if (!hasQueuedPredecessors() &&
            //CAS竞争获取锁
            compareAndSetState(0, acquires)) {
            //标记当前线程持有锁
            setExclusiveOwnerThread(current);
            //获取锁成功
            return true;
        }
    }
    //当前线程是否持有锁
    else if (current == getExclusiveOwnerThread()) {
        //锁重入,当前线程锁持有次数加1
        int nextc = c + acquires;
        //锁重入次数是否超出int最大值,超出报异常
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        //设置state
        setState(nextc);
        //持有锁成功
        return true;
    }
    return false;
}

上方代码是tryAcquire方法在FairSync类中的实现,FairSync顾名思义就是公平锁竞争。此处有两个细节。

1.判断nextc<0,当c等于int的最大值时,c+1就是int值的最小值,也就是负2的31次方减1,继续递增会使state再次等于0,而state=0代表锁未被持有,其他线程就可以持有锁,出现这种情况就会发生多个线程同时持有锁的情况,这和锁的目标是相悖的。

2.hasQueuedPredecessors方法判断是否有线程在等待获取锁,之后再讨论这个方法
下图展示了tryAcquire方法的调用过程

当tryAcquire返回false,也就是尝试获取锁失败的时候,执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)),首先来看addWaiter方法,也就是新增waiter节点。

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    //新引用指向当前的尾节点
    Node pred = tail;
    //尾节点tail不为null
    if (pred != null) {
        node.prev = pred;
        //CAS尝试将新节点node设置为尾节点tail
        if (compareAndSetTail(pred, node)) {
            //CAS成功,node节点已经是tail节点,将之前的尾节点的next指针指向当前的尾节点node节点
            pred.next = node;
            return node;
        }
    }
    //尾节点tail为null或者CAS竞争尾节点失败,enq入队
    enq(node);
    return node;
}

注意判断tail!=null的含义,tail==null会出现在以下可能的几种情况:
1.AQS初始化完成,没有任何线程获取过锁。
2.AQS初始化完成,有线程获取过锁,但通过tryAcquire方法直接获取到了锁。
这两种情况下,AQS的head节点和tail节点都是null。看下enq(node)方法

private Node enq(final Node node) {
    //无限循环
    for (;;) {
        Node t = tail;
        //尾结点为null,上面讨论了tail为null那么head节点也一定为null
        if (t == null) { // Must initialize
            //CAS竞争设置新的头节点
            if (compareAndSetHead(new Node()))
                //CAS竞争头结点成功,让尾节点=头结点,此时头、尾节点都是空节点
                tail = head;
            } else {
            node.prev = t;
            //CAS尝试设置尾节点tail
            if (compareAndSetTail(t, node)) {
                //CAS成功,之前的尾节点,也就是现在的到时第二个节点的next指向当前尾节点node
                t.next = node;
                //返回已经入队的节点,结束循环
                return t;
            }
        }
    }
}

联系之前enq方法调用前AQS中node队列的状态,我们可以知道AQS中node队列的生成过程。下放的流程图中简化了第一次在addWaiter方法中竞争成为tail的逻辑,因为在enq方法中这段逻辑已经存在。注意当AQS的head和tail节点被初始化时,他们是没有封装Thread对象的

通过addWaiter方法,当前线程已经封装程node对象进入了等待队列。接下来执行acquireQueued方法。

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            //node的prev节点
            final Node p = node.predecessor();
            //如果node的prev节点是头节点,则尝试获取锁,
            if (p == head && tryAcquire(arg)) {
                //成功获取锁,设置头节点为当前节点
                setHead(node);
                p.next = null; // help GC
                failed = false;
                //返回当前线程是否需要被中断
                return interrupted;
            }
            //如果node的prev节点不是头结点或者prev是头节点但node竞争锁失败,判断当前线程是否需要挂起
            if (shouldParkAfterFailedAcquire(p, node) &&
                //线程需要挂起,挂起当前线程。
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            //获取锁过程中发生异常,node节点出队
            cancelAcquire(node);
    }
}

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

需要注意以下几个细节:
1.在执行parkAndCheckInterrupt方法,LockSupport.park(this)使得当前线程挂起,不会返回。
2.在node的prev节点是head节点的时候执行了tryAcquire方法,这是因为在非公平锁NonFair的情况下,线程不会判断当前node队列中是否有线程在等待,而会直接tryAcquire尝试获取锁,这就会形成锁竞争,因此这里也使用tryAcquire方法,如果失败则继续等待。
3.如果成功获取锁,那么当前线程node成为head节点;因此当队列不为空的时候,head节点有封装的thread和未封装thread两种状态,这里回看下FairSync中tryAcquire时判断是否有线程节点在等待的方法hasQueuedPredecessors:

public final boolean hasQueuedPredecessors() {
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    return h != t &&
    ((s = h.next) == null || s.thread != Thread.currentThread());
}

这里我们看下在什么状态下队列没有thread在等待获取锁。
1.之前我们讨论了head和tail节点的状态,当head=tail节点时有两种可能,head=tail=null或者head=tail=new Node()。也就是队列中没有任何封装了thread的node节点,队列为空,因此head!=tail是队列有thread在等待的必要条件。
2.head.next=null或者head.next.thread!=Thread.currentThread(),首先第一个判断条件已经决定了node队列不是初始化的head=tail=new Node()状态,那么就有两种可能,一种是head.next=null,说明队列中的node经过排队获取锁,现在只有head节点在等待;第二种可能:head和next节点在初始化后,后序已经有thread在等待。注意FairSync在执行hasQueuedPredecessors前判断了state==0,说明当前的head节点已经释放锁,即将唤醒head.next节点,因此如果head.next节点的线程就是当前线程,说明可以执行CAS获取锁,当前线程不需要排队,否则当前线程需要排到队尾。

再看下判断当前线程是否需要挂起的shouldParkAfterFailedAcquire方法

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    //当前节点的prev节点是等待状态,那么当前节点应当等待(挂起)
    if (ws == Node.SIGNAL)
        return true;
    //prev节点的waitStatus>0,查看Node类中的waitStatus的各种状态,waitStatus只能是CANCELLED=1,那么当前节点的prev节点取消了排队,向前寻找未取消排队的节点,并链接上,node链表就去除掉了取消排队的节点
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        //将prev节点的waitStatus设置为SIGNAL等待状态,acquireQueued方法中的循环继续,再次进行shouldParkAfterFailedAcquire判断,prev节点的waitStatus状态就是SIGNAL状态,需要挂起,这段代码用来检查前置节点的状态
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

下图是acquireQueued方法的简略执行过程

回到文章开始,ReentrantLock.lock()获取锁,如果获取到则继续执行被锁的代码块,如果没有获取到则进去node队列等待获取锁,当前线程在lock()中挂起,等待被唤起;获取到锁的线程执行被锁的代码块,然后执行ReentrantLock.unlock()方法。unlock方法代码如下

public void unlock() {
    //因为当前线程可能不止一次获取锁,因此每次解锁都释放1
    sync.release(1);
}

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

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;
        //将当前持有锁的线程置为null
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

private void unparkSuccessor(Node node) {
    //node节点是head节点
    int ws = node.waitStatus;
    //将head节点的waitStatus设置为0
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    Node s = node.next;
    //如果head.next==null代表队列为空,head.next.waitStatus>0代表head.next节点取消了等待
    if (s == null || s.waitStatus > 0) {
        s = null;
        //从队尾开始向前寻找到第一个waitStatus<=0也就是在等待的节点,这里包含waitStatus==0是考虑到该节点没有next节点或者其next节点尚未设置其waitStatus=Node.SIGNAL,参见shouldParkAfterFailedAcquire方法
    for (Node t = tail; t != null && t != node; t = t.prev)
        if (t.waitStatus <= 0)
            s = t;
    }
    //如果寻找到了等待节点,唤起该节点的线程,该节点继续执行accquireQueued方法中的循环来获取锁
    if (s != null)
        LockSupport.unpark(s.thread);
}

unlock()方法调用sync.release(1),因为ReentrantLock是可重入的锁,当前线程可以多次获取锁,因此这里每次unlock释放1,直到state=0,锁被完全释放,执行unparkSuccessor(head)方法唤起head的next节点。注意在判断是否执行unparkSuccessor方法的判断条件中有head.waitStatus != 0,考量head节点waitStatus的可能情况:

1.队列刚刚初始化,head=tail=new Node(), head.waitStatus=tail.waitStatus=0,此时链表为空,不需要唤起head.next;

2.有节点在挂起等待,那么该节点执行过shouldParkAfterFailedAcquire方法,其prev节点的waitStatus一定是SIGNAL,依次类推,head节点的waitStatus应该是Node.SIGNAL,需要唤起head.next;

3.node队列的节点已经依次获取锁,只剩head节点,因为我们是在唤醒后续线程的unparkSuccessor方法中将head设置为0,因此此时的head.waitStatus应该是Node.SIGNAL,需要唤起head.next

下图是release方法的简易逻辑

5.小结

通过阅读代码的方式我们了解了下ReentrantLock和AQS在多线程并发过程中加锁和解锁的原理。AQS的锁竞争过程可以大致理解为一个排队过程,就像我们排队在ATM取钱一样。大家都来取钱的时候发现ATM机并没有被占用,那么大家就抢,抢到的先用ATM,抢不到的要排队等。排队的时候要按照规则,不能插队,只能抢当队尾,入队的时候要看看前面的人是不是还想用ATM,不想用的把他踢出去,知道前面一个是想用ATM的人,这样依次排队形成一个队列。为了防止队列中的人等ATM等的太累(线程消耗太多资源),可以先让他睡会儿(挂起),正在用ATM的人用好后叫醒后面还想用ATM的人,把不想用的人踢出队列。这样我们就可以依次来使用ATM(获得锁)了。

Doug Lea大神的代码非常简洁,其中包含了大量的CAS操作以及对各种情况的判断,个人水品有限,如果文章中有谬误,还请留言指正。ps:转载请注明出处

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值