Java并发(八):AbstractQueuedSynchronizer

Wesley13
• 阅读 684

先做总结:

1、AbstractQueuedSynchronizer是什么?

AbstractQueuedSynchronizer(AQS)这个抽象类,是Java并发包 java.util.concurrent 的基础工具类,是实现 ReentrantLock、CountDownLatch、Semaphore、FutureTask 等类的基础。

AbstractQueuedSynchronizer其实是锁的主体。Lock类会有一个AQS类型的属性,来实现锁。

2、AQS如何实现锁?

(1)Lock类会有一个AQS类型的属性sync,sync是AQS的子类,重写了tryAcquire()方法(获取锁)和tryRelease()方法(释放锁)。

(2)线程T获取到锁标志:AQS.state > 0  && AQS.exclusiveOwnerThread == 当前线程T。tryAcquire()/tryRelease()方法其实就是对AQS.state AQS.exclusiveOwnerThread 的操作。

3、AQS原理:

(1)AQS中维护着一个同步队列。

  头结点headNode只能是null或者是已经获取到锁的线程,只有第二个节点能够尝试获取锁。

  第二个节点获取到锁之后变为头结点。

(2)acquire()方法会调用tryAcquire()获取锁。tryAcquire()获取到锁,完成。

  如果获取锁失败(没有竞争到锁),当前线程T会封装成Node插入同步队列中,并且将当前线程T park()。

(3)release()方法调用tryRelease()方法释放锁,当前线程释放锁之后,会unpark()下一节点(也就是唤醒第二节点,因为持有锁的一定是头节点线程或者不在队列中的线程)

一、CLH同步队列

AQS通过内置的FIFO同步队列来完成资源获取线程的排队工作。

如果当前线程获取同步状态失败(锁)时,AQS则会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入同步队列,同时会park当前线程;

当同步状态释放时,则会把节点中的线程唤醒,使其再次尝试获取同步状态。

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;// 当前node的后继节点对应的线程需要被唤醒(表示后继节点的状态)
    static final int CONDITION = -2;// 当前节点线程状态 0-没有获得锁 >0-线程取消了等待
    volatile int waitStatus;
    volatile Node prev;
    volatile Node next;
    volatile Thread thread;// 每一个节点对应一个线程
    Node nextWaiter;// 共享模式/独占模式
}

入列:

private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        Node pred = tail;
        if (pred != null) {// 加入队尾
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {// 失败:其它线程抢先入列了
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }
    
    private Node enq(final Node node) {
        for (;;) {// 循环入列,直到成功
            Node t = tail;
            if (t == null) {// 初始化head
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {// 失败:其它线程抢先入列了
                    t.next = node;
                    return t;
                }
            }
        }
    }

两个方法都是通过一个CAS方法compareAndSetTail(Node expect, Node update)来设置尾节点,该方法可以确保节点是线程安全添加的

二、属性

private transient volatile Node head;
    private transient volatile Node tail;
    private volatile int state;// 0代表没有被占用,大于0代表有线程持有当前锁(锁可以重入,每次重入都+1)
    private transient Thread exclusiveOwnerThread; // 继承自AbstractOwnableSynchronizer 当前持有锁的线程

三、主要方法

getState()// 返回同步状态的当前值;
    setState(int newState)// 设置当前同步状态;
    compareAndSetState(int expect, int update)// 使用CAS设置当前状态,该方法能够保证状态设置的原子性;
    
    acquire(int arg)// 独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则,将会进入同步队列等待,该方法将会调用可重写的tryAcquire(int arg)方法;
    acquireInterruptibly(int arg)// 与acquire(int arg)相同,但是该方法响应中断,当前线程为获取到同步状态而进入到同步队列中,如果当前线程被中断,则该方法会抛出InterruptedException异常并返回;
    tryAcquireNanos(int arg,long nanos)// 超时获取同步状态,如果当前线程在nanos时间内没有获取到同步状态,那么将会返回false,已经获取则返回true;
    acquireShared(int arg)// 共享式获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待,与独占式的主要区别是在同一时刻可以有多个线程获取到同步状态;
    acquireSharedInterruptibly(int arg)// 共享式获取同步状态,响应中断;
    tryAcquireSharedNanos(int arg, long nanosTimeout)// 共享式获取同步状态,增加超时限制;
    release(int arg)// 独占式释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒;
    releaseShared(int arg)// 共享式释放同步状态;
    
    tryAcquire(int arg)// 独占式获取同步状态;通过子类重写实现
    tryRelease(int arg)// 独占式释放同步状态;通过子类重写
    tryAcquireShared(int arg)// 共享式获取同步状态;子类重写
    tryReleaseShared(int arg)// 共享式释放同步状态;子类重写

四、以独占锁为例解析AQS(共享锁几乎一样)

AQS的设计模式采用的模板方法模式,子类通过继承的方式,实现它的抽象方法来管理同步状态,对于子类而言它并没有太多的活要做,AQS提供了大量的模板方法来实现同步。

独占式同步状态获取

public final void acquire(int arg) {
        if (!tryAcquire(arg) && // 尝试获取锁-由子类重写
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 获取锁失败: addWaiter()-当前线程new Node+入列; acquireQueued()-设置前驱节点状态-1,当前线程park()
            selfInterrupt();
    }
    
    /**    
     * 1.尝试获取锁  获取成功,将当前节点置为头结点
     * 2.获取锁失败,将前驱节点状态置为-1,当前节点线程park()等待
     * 3.前驱点释放锁时,会将当前节点unpark(),继续自旋获取锁
     */
    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)) { // 前驱节点是head时,当前节点才能请求锁(请求锁的是第二个节点)
                    setHead(node); // 当前线程获取到锁之后,将当前节点置为头结点
                    p.next = null;
                    failed = false;
                    return interrupted;
                }
                
                if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
    
    /**
     * 1.park开头的方法来阻塞当前线程,unpark(Thread thread)方法来唤醒一个被阻塞的线
     * 2.前驱点 waitStatus==-1,当前节点线程才能park
     * 3.ws>0表示已经获取过锁,从CLH队列删除,CLH队列存放没有获取到锁被挂起的线程节点
     * 4.当前线程没有获取到锁,需要设置前驱节点状态为-1,这样当前节点线程才能park()
     */
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus; // 前驱节点 
        if (ws == Node.SIGNAL) // 当前节点线程可以park
            return true;
        if (ws > 0) { // ws>0 已经获取过锁,从CLH队列删除
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else { // 需要设置前驱节点状态为-1,这样当前节点线程才能park()
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
    
    /**
     * 当前线程park()等待,直到unpark()
     */
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }
    

acquire(int arg)方法流程图如下:

 Java并发(八):AbstractQueuedSynchronizer

独占式同步状态释放

public final boolean release(int arg) {
        if (tryRelease(arg)) { // 尝试释放锁-由子类重写
            Node h = head; // 头结点占有锁(原因:第二节点获取到锁之后被置为头结点)
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
    
    private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            // 从队尾往前找,找到waitStatus<=0的所有节点中排在最前面的
            // 从队尾往前找原因:node.next可能会存在null或者取消了。入列时是先设置node.prev,CAS之后再设置node.next
            for (Node t = tail; t != null && t != node; t = t.prev) 
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null) // 唤醒下一节点
            LockSupport.unpark(s.thread);
    }

参考资料 / 相关推荐:

一行一行源码分析清楚AbstractQueuedSynchronizer (超详细,一定能看懂)

【死磕Java并发】—–J.U.C之AQS:AQS简介

【JUC】JDK1.8源码分析之AbstractQueuedSynchronizer(二)

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
3个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
AQS实现原理分析——ReentrantLock
在Java并发包java.util.concurrent中可以看到,不少源码是基于AbstractQueuedSynchronizer(以下简写AQS)这个抽象类,因为它是Java并发包的基础工具类,是实现ReentrantLock、CountDownLatch、Semaphore、FutureTask等类的基础。 AQS的主要使用方式是继承,子类通
九路 九路
3年前
一行一行源码分析清楚AbstractQueuedSynchronizer
在分析Java并发包java.util.concurrent源码的时候,少不了需要了解AbstractQueuedSynchronizer(以下简写AQS)这个抽象类,因为它是Java并发包的基础工具类,是实现ReentrantLock、CountDownLatch、Semaphore、FutureTask等类的基础。Google一下A
Wesley13 Wesley13
3年前
Java日期时间API系列31
  时间戳是指格林威治时间1970年01月01日00时00分00秒起至现在的总毫秒数,是所有时间的基础,其他时间可以通过时间戳转换得到。Java中本来已经有相关获取时间戳的方法,Java8后增加新的类Instant等专用于处理时间戳问题。 1获取时间戳的方法和性能对比1.1获取时间戳方法Java8以前
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
9个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这