AQS 独占&共享模式以及ReenterantLock、CountLatchDown实现原理

AQS

抽象队列同步器,是并发包当中非常重要的一个类.大部分使用到锁的地方都有继承它.它有个重要的内部类Node-队列节点结构.AQS分独占和共享模式.主要属性有如下字段

AQS主要属性:
private transient volatile Node head; // 队列头节点
private transient volatile Node tail;  //队列尾节点
 private volatile int state; //同步器状态
 private transient Thread exclusiveOwnerThread; //当前获取到锁的线程

内部结构Node属性如下:
        static final Node SHARED = new Node();  //共享模式
        static final Node EXCLUSIVE = null;  //独占模式
		 // 节点状态
        static final int CANCELLED =  1;
        static final int SIGNAL    = -1;  
        volatile Node prev; //前驱节点
        volatile Node next;  //后继节点
        volatile Thread thread;  //当前线程
        Node nextWaiter; //共享模式下

独占模式&ReentrantLock实现

我们先了解独占模式的代码. 获取独占锁的入口函数是acquire方法源码如下,它做如下行为:

  1. 尝试获取锁 tryAcquire
  2. 如果获取成功则结束, 失败则将当前线程封装成节点Node放到等待AQS的队列尾部 addWaiter
  3. 让Node自旋, 不断检查自己的前驱节点是否是头节点, 如果是就尝试获取锁, 不是就阻塞等待.只有头节点能获取到锁 acquireQueued
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 
        selfInterrupt(); //获取锁的整个过程不会响应中断,过程当中会记录是否有被中断过,有的话,结束后会自行中断
}

其中的tryAcquire由子类实现.addWaiter,acquireQueued源码如下

private Node addWaiter(Node mode) {
   Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node); //队列为空,则构造队列
    return node;
}

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);   //获取锁成功之后, 将头节点设置为当前节点,thread为空
                p.next = null; // help GC 
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt()) // 看是否当前线程是否需要挂起,有则挂起
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

以上是获取锁的过程,下面讲讲释放锁的过程,上层的方法就是release方法,做了如下动作

  1. 尝试释放锁 tryRelease
  2. 释放成功后,将后继节点唤起.
public final boolean release(int arg) {
   if (tryRelease(arg)) {  //tryRelease放给子类去实现
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);   //唤起后继节点, 里面的代码是从尾节点往上遍历,找到最前面的一个没有被取消的节点唤醒.
        return true;
    }
    return false;
}

总结AQS独占锁的获取如下:

  • 尝试获取锁.
  • 获取不到则加入到队列中去, 并且让当前加入的线程自旋(循环不断检查自己的前驱节点是否是头节点, 是的话尝试获取锁, 否则就挂起)
  • 当当前线程释放锁之后, 会唤起后继节点.(因为节点一旦被唤醒, for循环就会继续执行,后继节点检查自己的前驱节点就是刚刚释放锁的头节点的话, 它就会尝试去获取锁)

并发包当中的可重入锁就实现了AQS.通过它来更加了解并发包中其它锁是如何使用AQS的独占锁模式的.

ReenterantLock

可重入锁分为公平锁和非公平锁.

  • 公平锁的意思是永远是等待最久的线程(即最先入等待队列的线程)先获取到锁, 打算获取锁的新线程必须加入到等待队列.
  • 非公平锁就是一旦有线程来获取锁,就让它尝试获取,获取不到才加入到等待队列.
    接下来分开以源码的形式来解读.
    首先看看ReenterantLock的重要属性和内部类
private final Sync sync;  //ReenterantLock的获取锁和释放锁底层都是Sync来实现.

public ReentrantLock() {
    sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
从它两个构造函数来看, 如果不传参数,默认就是非公平锁.

Sync实现了AQS,它有两个子类分别是NonfairSync,FairSync . 它实现了AQS的tryRelease方法,因为不管是公平锁还是非公平锁,
释放锁的实现是一样的. 唯有获取锁的方式来体现公平与否.
tryRealse方法实现如下:

protected final boolean tryRelease(int releases) {
    int c = getState() - releases; 
      // 因为是可重入锁,所以同一线程可以多次获得锁,每获得一次同步器的state就会+1,那么释放的时候就要-1.
     //直到state=0才表示锁被释放
    if (Thread.currentThread() != getExclusiveOwnerThread()) //判断释放锁的线程和当前拥有锁的线程是否是同一个
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) { // 当c=0就代表该释放锁了
        free = true;
        setExclusiveOwnerThread(null);  //设置当前拥有锁的线程为null
    }
    setState(c); //设置同步器状态
    return free;
}

当该函数返回后,就会在AQS,release方法下继续执行下面的步骤, 即如果释放锁是成功的, 唤起后继节点.

NonfairSync非公平锁获取锁的源码如下:

final boolean nonfairTryAcquire(int acquires) {
     final Thread current = Thread.currentThread();
     int c = getState();  //获取同步器状态
     if (c == 0) {
         if (compareAndSetState(0, acquires)) {  //如果是第一次获取锁, 设置同步器状态=1
             setExclusiveOwnerThread(current); //设置拥有锁的线程为当前线程
             return true;  //锁获取成功
         }
     }
     else if (current == getExclusiveOwnerThread()) {  
     //如果当前线程已经是拥有锁的线程了,就是代表该线程的重入情况, 也会返回true,但是同步器状态state+1, 
     //每释放一次就会-1.
         int nextc = c + acquires;  // state +1
         if (nextc < 0) // overflow
             throw new Error("Maximum lock count exceeded");
         setState(nextc);  //更新同步器状态
         return true;
     }
     return false; //又不是重入的,获取锁也失败了,返回false.加入等待队列尾部.
 }

当执行完之后,如果没有成功获取到锁就会继续执行AQS的acquire方法中的其它代码,构造Node节点,加入等待队列.
上面代码可以看出,非公平锁是一旦有线程来获取锁,它就会执行获取锁操作,不会去检查等待队列的情况.公平锁就不一样.

FairSync源码如下:

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (!hasQueuedPredecessors() &&    // 公平锁与非公平锁最大的不同就在于会判断,队列中是否有等待线程, 有的话就会返回false
            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;
}

公平锁和非公平锁在释放锁的实现一样,在获取锁的方式差不多,
唯一一点不同就是公平锁会去判断等待队列是否等待线程, 有的话它不会马上尝试去获取,而是加入队列尾部.

共享模式以及CountDownLatch实现

共享模式下获取锁的顶层入口及源码如下:

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)  //跟独占模式一样tryAcquireShared由子类实现
        doAcquireShared(arg); 
}

值得说明的是独占模式下,锁只有被抢到和没有抢到两个返回值,即tryAcquire返回的是boolean值类型。而共享模式下,锁可以被多个线程占有,tryAcquireShared定义方法返回整形,会有三种情况作如下说明:

  • 如果该值小于0,则代表当前线程获取共享锁失败
  • 如果该值大于0,则代表当前线程获取共享锁成功,并且接下来其他线程尝试获取共享锁的行为很可能成功
  • 如果该值等于0,则代表当前线程获取共享锁成功,但是接下来其他线程尝试获取共享锁的行为会失败

举个例子说明,共享锁定义了同时能获取锁的最大线程数是n。那么当一个线程tryAcquireShared成功时,返回值n-1,所以代表获取锁成功,当返回n=0时,说明线程数满了,其它线程来获取的时候就会返回服输,要等释放锁才能有其它线程获取成功。这个后面结合CountDownLatch实现来说明。

接着讲源码tryAcquireShared失败时会执行doAcquireShared,源码如下:

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

仔细比较独占式和共享式在获取锁之后的执行的方法不同之处仅在于当获取锁成功后r >= 0时,会执行setHeadAndPropagate(node, r)方法的内容,即如下代码:

if (propagate > 0 || h == null || h.waitStatus < 0 ||
    (h = head) == null || h.waitStatus < 0) {
    Node s = node.next;
    if (s == null || s.isShared())
        doReleaseShared();
}

doReleaseShared方法其实是唤起后继节点去获取锁。所以在共享模式下如果获取锁成功返回值>0后还是继续唤起后继节点。

看CountDownLatch的实现tryAcquireShared、tryReleaseShared方法的源码:

public void countDown() {
    sync.releaseShared(1);
}
public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

protected int tryAcquireShared(int acquires) {
    return (getState() == 0) ? 1 : -1;
}

protected boolean tryReleaseShared(int releases) {
    // Decrement count; signal when transition to zero
    for (;;) {
        int c = getState();
        if (c == 0)
            return false;
        int nextc = c-1;
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}

释放锁的过程和独占式没有什么区别,取决于tryReleaseShared实现方式。

public final boolean releaseShared(int arg) {
   if (tryReleaseShared(arg)) {
       doReleaseShared();
       return true;
   }
   return false;
}

举例CountDownLatch使用方法:

public class CountDownLatchTest {

    public static class CountDownLatchTest1 implements Runnable {

        private CountDownLatch countDownLatch;

        public CountDownLatchTest1(CountDownLatch countDownLatch) {
            this.countDownLatch = countDownLatch;
        }


        @Override
        public void run() {
            try {
                Thread.sleep(10000l);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            countDownLatch.countDown();
            System.out.println("CountDownLatchTest!");

        }
    }

    public static void main(String[] args) throws InterruptedException {
        final CountDownLatch countDownLatch = new CountDownLatch(2);

        for (int i=0;i<2;i++) {
            new Thread(new CountDownLatchTest1(countDownLatch)).start();
        }

        System.out.println("await start");
        countDownLatch.await();
        System.out.println("await end");
    }
}

输出如下:
await start
CountDownLatchTest!
CountDownLatchTest!
await end

解读如下:

  • new CountDownLatch(2),初始化时AQS的state会设置成2
  • await会执行加锁方法,由CountDownLatch实现的tryAcquireShared可知,如果state的值不等于0返回的就是-1,即获取锁失败,当前线程会阻塞,加入到AQS的等待队列中。所以在输出“await start”后main线程在等待。
  • 当两个新线程在执行countDown方法会释放锁,实现的tryReleaseShared方法可知,释放锁state会-1.所以当第一个线程释放锁之后,state为1,释放锁是不成功的。当第二个线程执行tryReleaseShared方法后state-1 == 0条件成立返回true,就会执行doReleaseShared,即唤起等待队列中的线程main线程,继续往下执行。

再复习一下, 用一张图来解释一下AQS的结构, 因为它是并发包的基础:在这里插入图片描述
AQS中维护了一个Node结构的队列. 一个node代表着一个等待线程.当一个线程尝试获取lock失败时会加入到该队列的队尾.然后不断自旋获取锁,得不到锁就会阻塞等待被唤醒,头节点一般代表着当前获取到锁的线程, 当它释放锁的时候就会唤醒后继节点, 后继节点恢复while自旋尝试获取锁, AQS中state用作尝试获取锁的次数, 用在可重入锁和共享锁,state不会只是1和0. exclusiveOwnerThread代表着独占锁当前获取锁的线程.

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值