2.从AbstractQueuedSynchronizer(AQS)说起(1)——独占模式的锁获取与释放

  首先我们从java.util.concurrent.locks包中的AbstraceQueuedSynchronizer说起,在下文中称为AQS。

  AQS是一个用于构建锁和同步器的框架。例如在并发包中的ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock等都是基于AOS构建,这些锁都有一个特点,都不是直接扩展自AQS,而是都有一个内部类继承自AQS。为什么会这么设计而不是直接继承呢?简而言之,锁面向的是使用者,同步器面向的是线程控制,在锁的实现中聚合同步器而不是直接继承AQS很好的隔离了二者所关注的领域。

  AbstractQueuedSynchronizer在内部依赖一个双向同步队列来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将该线程和等待状态信息构造成一个节点并将其加入到同步队列中。Node节点以AQS的内部类存在,其字段属性如下:

image.png

  在AQS同步器中由一个头节点和尾节点来维护这个同步队列。

image.png
image.png

  以上内容我们需要知道一点的就是:同步器中是依靠一个同步队列来完成的同步状态管理,当线程获取锁(或者称为同步状态)失败时,会将线程构造为一个Node节点新增到同步队列的尾部。

  在锁的获取当中,并不一定是只有一个线程才能持有这个锁(或者称为同步状态),所以此时有了独占模式和共享模式的区别,也就是在Node节点中由nextWait来标识。比如ReentrantLock就是一个独占锁,只能有一个线程获得锁,而WriteAndReadLock的读锁则能由多个线程同时获取,但它的写锁则只能由一个线程持有。本章先介绍独占模式下锁(或者称为同步状态)的获取与释放,在此之前要稍微提一下“模板方法模式”,在AQS同步器中提供了不少的模板方法,关于模板方法模式可以移至《模板方法模式》,总结就是一句话:定义一个操作中的算法的骨架,而将一些步骤的实现延迟到子类中。

  1)独占模式同步状态的获取

  此方法即为一个模板方法,它的实现代码如下:

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

  tryAcquire:AQS中有一个默认实现,其默认实现即抛出一个UnsupportedOperationException异常,意为默认下独占模式是不支持此操作的。而这个操作在子类又是怎样的呢?我们可以通过查看ReentrantLock中的Sync实现:

//ReentrantLock
protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}

  可以看到在AQS的其中一个实现中,ReentrantLock$Sync对它进行了重写,具体意义在这里不做讨论。这个在AQS定义的方法表示该方法保证线程安全的获取同步状态,如果同步状态获取失败(返回false)则构造同步节点并将节点加入到同步队列的尾部,这个操作即是addWaiter方法的实现:

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);  //将线程构造成Node节点。
    /*尝试强行直接挂到同步队列的尾部*/
    Node pred = tail;   
    if (pred != null) {
       node.prev = pred;
       if (compareAndSetTail(pred, node)) {
           pred.next = node;
           return node;
       }
    }
    /*如果此时有多个线程都在想把自己挂到同步队列的尾部,上面的操作就会
    失败,此时将“无限期”线程安全的等待着挂到同步队列的尾部*/
    enq(node);
    return node;

}

  在enq的实现中实际就是一个for“死循环”,其目的就是直到成功地添加到同步队列尾部才推出循环。

  在获取同步状态失败(tryAcqurie) ->构造节点(addWaiter)->添加到同步队列尾部(addWaiter)过后,接下来就是一个很重要的操作acquireQueued自旋。这个动作很重要,其目的就在于每个节点都各自的在做判断是否能获取到同步状态,每个节点都在自省地观察,当条件满足获取到了同步状态则可以从自旋过程中退出,否则继续。

final boolean acquireQueued(final Node node, int qrg) {
    boolean failed = true;
    try {
       boolean interrupted = false;
       for (;;) {
           final Node p = node.predecessor();
           if (p == head && tryAcquire(arg)) {
              setHead(node);
              p.next = null;
              failed = false;
              return interrupted;
           }
        if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) {
              interrupted = true;
        }
    }
} finally {
    if (failed)
       cancelAcquire(node);
    }
}                         

  从上面的代码实现我们可以看到,尽管每个节点都在“无限期”的获取锁,但并不是每个节点能有获取锁的这个资格,而是当它的前驱节点是头节点时才会去获取锁(tryAcquire)。当这个节点获取同步状态时,接下来的方法shouldParkAfterFailedAcquire则会判断当前线程是否需要被阻塞,而这个判断方法则是通过它的前驱节点的waitStatus判断。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;   //首先获取当前节点的前驱节点等待状态
    if (ws == Node.SIGNAL)   //当前线程需要被阻塞,即需要被unpark(唤醒)
       return true;
    if (ws > 0) { //pred.waitStatus == CANCELLED
       do {
           node.prev = pred = pred.prev;   //前驱节点等待状态已经处于取消,即不会再获取同步状态时,把前驱节点从同步状态中移除。
        } while (pred.waitStatus > 0);
    pred.next = node;
    } else {   //pred.waitStatus == CONDITION || PROPAGATE
       compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}                

  如果调用该方法判断为当前线程需要被阻塞(返回true),则接着执行parkAndCheckInterrupt阻塞当前线程,直到当前线程被唤醒的时候才从parkAndCheckInterrupt返回。

关于独占模式获取同步状态可以总结为下面一段话:

  AQS的模板方法acquire通过调用子类自定义实现的tryAcquire获取同步状态失败后->将线程构造成Node节点(addWaiter)->将Node节点添加到同步队列对尾(addWaiter)->每个节点以自旋的方法获取同步状态(acquirQueued)。在节点自旋获取同步状态时,只有其前驱节点是头节点的时候才会尝试获取同步状态,如果该节点的前驱不是头节点或者该节点的前驱节点是头节点单获取同步状态失败,则判断当前线程需要阻塞,如果需要阻塞则需要被唤醒过后才返回。

  2).独占模式同步状态的释放

image.png

当线程获取到了同步状态并且执行了相应的逻辑过后,此时就应该释放同步状态。

public final boolean release(int arg) {
    if (tryRelease(arg)) {
       Node h = head;
       if (h != null && h.waitStatus != 0)
           unparkSuccessor(h);      //唤醒头节点的后继节点
       return true;
    }
    return false;
}

  AQS中的release释放同步状态和acquire获取同步状态一样,都是模板方法,tryRelease释放的具体操作都有子类去实现,父类AQS只提供一个算法骨架。

private void unparkSucessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)   //ws != CANCELLED
    compareAndSetWaitStatus(node, ws, 0);  //利用CAS将当前线程的等待状态置为CANCELLE
    Node s = node.next;
    if (s == null || s.waitSatatus > 0) {  //如果当前线程的后继节点为空,则从同步队列的尾节点开始向前寻找当前线程的下一个不为空的节点
       s = null;
       for (Node t = tail; t != null && t != node; t = t.prev)
           if (t.waitStatus <= 0)  
               s = r;
    }
    if (s != null)
        LockSuport.unpark(s.thread);    //如果当前线程的后继节点不为空,则调用LockSuport.unpark唤醒其后继节点,使得后继节点得以重新尝试获取同步状态
}

  对AQS的源码解读才刚刚开始,本节只介绍了AQS在内部使用一个同步队列来管理同步状态,并且介绍了在AQS在模板方法模式的基础上实现独占模式同步状态的获取与释放。下一节会继续解读AQS共享模式下同步状态的获取与释放。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏一个会写诗的程序员的博客

【Java 并发】 之 AQS 详解 & volatile关键字CPU内存架构volatile关键字的作用

谈到并发,不得不谈ReentrantLock;而谈到ReentrantLock,不得不谈AbstractQueuedSynchronizer(AQS)!

1073
来自专栏java达人

使用Redis做MyBatis的二级缓存

使用Redis做MyBatis的二级缓存  通常为了减轻数据库的压力,我们会引入缓存。在Dao查询数据库之前,先去缓存中找是否有要找的数据,如果有则用缓存中的数...

4335
来自专栏Android群英传

Android Native Crash 收集

本文是『张涛的NDK之旅』,本来很早以前就有很多读者希望我能写一些关于MDK的文章,但是由于我本身对NDK不熟悉,所以找来了同事张涛的文章。欢迎大家关注他的博客...

2941
来自专栏编程札记

深入golang之---goroutine并发控制与通信

本文章通过goroutine同步与通信的一个典型场景-通知子goroutine退出运行,来深入讲解下golang的控制并发。

1.2K7
来自专栏菩提树下的杨过

ZooKeeper 笔记(5) ACL(Access Control List)访问控制列表

zk做为分布式架构中的重要中间件,通常会在上面以节点的方式存储一些关键信息,默认情况下,所有应用都可以读写任何节点,在复杂的应用中,这不太安全,ZK通过ACL机...

7876
来自专栏Android开发指南

17:网络编程

3265
来自专栏瞎说开发那些事

[Java并发系列]Java中的锁

2249
来自专栏Golang语言社区

Golang工程经验(上)

作为一个C/C++的开发者而言,开启Golang语言开发之路是很容易的,从语法、语义上的理解到工程开发,都能够快速熟悉起来;相比C、C++,Golang语言更简...

4902
来自专栏java一日一条

Java 并发开发:Lock 框架详解

我们已经知道,synchronized 是java的关键字,是Java的内置特性,在JVM层面实现了对临界资源的同步互斥访问,但 synchronized 粒度...

972
来自专栏cs

linux 学习笔记七

来自实验楼的学习笔记,文字基本复制,粘贴。 ? 下载了一个录制gif图的软件,还不错 参考与:在Linux(Ubuntu)下超好用的录屏gif软件!!...

3455

扫码关注云+社区

领取腾讯云代金券