java并发编程 AbstractQueuedSynchronizer(AQS)详解一

文章目录

    • [1 概要](#1 概要)
    • [2 技术名词解释](#2 技术名词解释)
    • [3 AQS核心方法原理](#3 AQS核心方法原理)
      • [3.1 acquire(int arg)](#3.1 acquire(int arg))
      • [3.2 release(int arg)](#3.2 release(int arg))
      • [3.3 acquireInterruptibly(int arg)](#3.3 acquireInterruptibly(int arg))
      • [3.3 acquireShared(int arg)](#3.3 acquireShared(int arg))
      • [3.4 doReleaseShared()](#3.4 doReleaseShared())
      • [3.5 releaseShared(int arg)](#3.5 releaseShared(int arg))
      • [3.6 acquireSharedInterruptibly](#3.6 acquireSharedInterruptibly)
      • [3.7 hasQueuedPredecessors()](#3.7 hasQueuedPredecessors())
    • [4 总结](#4 总结)

1 概要

AQS在类的注释上说的已经很明白,提供一个框架,用于实现依赖先进先出(FIFO)等待队列的阻塞锁和相关同步器(信号量、事件等)。此类被设计做为大多数类型的同步器的一个有用的基础类,这些同步器依赖于单个原子int值(state字段)来表示状态。

2 技术名词解释

CAS:cas是比较并交换(compare and swap)的缩写,java对其具体实现是Usafe类,它有一系列的compareAndSwap方法

3 AQS核心方法原理

3.1 acquire(int arg)

从代码逻辑上可以看到,首先尝试获取,如果此时返回true, 执行结束。相当于获取到锁了。tryAcquire方法是抽象方法,比如公平锁和非公平锁的不同实现逻辑

如果尝试获取失败,会进入acquireQueued(addWaiter(Node.EXCLUSIVE), arg)。

java 复制代码
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        //没获取到锁但是park住了,如果unpack且如果在等待的过程中发生中断走到这,进行中断位标记
        selfInterrupt();
}

此时具体看下acquireQueued(addWaiter(Node.EXCLUSIVE), arg)逻辑。

  1. addWaiter(Node.EXCLUSIVE)
java 复制代码
private Node addWaiter(Node mode) {
	//创建Node
    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;
        }
    }
    //走下述逻辑,上面尝试未成功,或者此时tail == null(因为初始化的时候都是null 在后续才会设置值)
    enq(node);
    return node;
}
private Node enq(final Node node) {
	//死循环+原子性加到列表中
    for (;;) {
        Node t = tail;
        if (t == null) { // 初始化头结点和尾结点
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}
  1. acquireQueued(final Node node, int arg)
    因为addWaiter(Node mode)最终是死循环+原子性加到列表中,是肯定成功的。也就意味着需要阻塞当前线程了。但是有没有挽救的余地呢?
    todo1
java 复制代码
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
        	//获取当前线程节点的前一个节点,因为在上步已经添加到tail中了
            final Node p = node.predecessor();
            //如果此时前一个节点时头节点,因为在概要中已经说明,先进先出队列,所以此时前面就一个头
            //结点,此时你就是阻塞队列第一个,那没线程阻塞啊,所以此时再尝试下获取锁
            if (p == head && tryAcquire(arg)) {
            	//此时获取到锁,那么线程安全的情况下设置头结点的node
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            //尝试还是没获取到锁,那么原子设置pred.waitStatus = -1。waitStatus 状态下面会描述
            if (shouldParkAfterFailedAcquire(p, node) &&
            	//设置pred.waitStatus = -1成功了 会park阻塞线程,并返回该过程中是否被中断过
                parkAndCheckInterrupt())
                //这地方是unpark 或者中断之后的逻辑,又会回到上面去获取锁,如果pre不是头节点,又会被park住。todo1 做个标记
                interrupted = true;
        }
    } finally {
        if (failed)//正常情况下不会到这
            cancelAcquire(node);
    }
}

总结acquire(int arg):在多线程的情况下,有一个线程tryAcquire获取到锁,其余的cas进入等待队列,当然头节点的下一个或尝试下,没有获取成功的都会被park当前线程。进入阻塞状态等待被唤醒

3.2 release(int arg)

先看整体逻辑,和获取锁是一致且相反的逻辑。先尝试释放锁,释放失败直接false。还可以释放失败?? 如果锁是可重入的,那么需要把state缩减成0才算释放。

tryRelease(arg)是个接口方法,子类实现。

java 复制代码
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) {
	//waitStatus下面描述
    int ws = node.waitStatus;
    if (ws < 0)
    	//把当前节点的waitStatus修改成0,如果失败了呢,好像也不影响,
        compareAndSetWaitStatus(node, ws, 0);

   	//s.waitStatus > 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;
    }
    //正常情况下从头取等待的线程唤醒,但是如果头节点的next节点是被取消了,那就从尾部找了,好像也不是严格的先进先出啊....
    if (s != null)
        LockSupport.unpark(s.thread);
}

3.3 acquireInterruptibly(int arg)

相对于3.1的acquire(int arg),该方法会对线程获取锁的过程中线程发生中断抛出异常

java 复制代码
public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        //先尝试获取锁,未获取到进度会抛异常的获取锁方法
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
}
//对比下
private void doAcquireInterruptibly(int arg)
    throws InterruptedException {
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                //区别就在这,当parkAndCheckInterrupt()返回true时,也就是获取锁结果被阻塞住了,期间发生线程中断会抛出打断异常
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

3.3 acquireShared(int arg)

获取共享锁。同比上述排它锁,相同逻辑先尝试获取共享锁tryAcquireShared(arg),然后进入doAcquireShared

java 复制代码
public final void acquireShared(int arg) {
   if (tryAcquireShared(arg) < 0)
        doAcquireShared(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 (p == head) {
          		//和上述一样排它锁一样
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                	//区别在这 独占锁setHead(node);只是设置了头节点,但是共享锁在获取到锁之后,不仅要设置头节点,
                	//此时还需要判断后续节点。因为是共享的,你拿到锁之后,后续链表中挨着的共享节点也可以被unpark
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                    	//和独占锁一样,不带Interruptibly的方法只是帮你设置下中断位
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                //和独占锁一样,不带Interruptibly的方法只是帮你设置下中断位
                //此时这唤醒之后 是不是又到循环上面去了,这个节点就是头结点的next节点
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
 private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head;
        //此时node就是头节点 设置进去
        setHead(node);
        //这是共享锁在获取到锁之后的多余的操作,就是顺着链表传播下去。为什么呢?举个场景,共享读锁 如果此时下一个节点还是共享锁,其实是需要唤醒的,此情况下是链表中的顺序 独占,共享,共享,共享这种情况
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            //
            if (s == null || s.isShared())
            	//如果下一个节点是共享的就释放锁
                doReleaseShared();
        }
    }
}

3.4 doReleaseShared()

java 复制代码
for (;;) {
    Node h = head;
    if (h != null && h != tail) {
        int ws = h.waitStatus;
        if (ws == Node.SIGNAL) {
        	//可能有其他线程调用doReleaseShared(),unpark操作只需要其中一个调用就行了
        	//正常情况下就是修改成0,unpark改线程,unparkSuccessor上面已描述
            if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                continue;            // loop to recheck cases
            unparkSuccessor(h);
        }
        //如果上述失败,修改成-3 为了同时release的情况,此时设想下,如果不改成-3,只变成0 release就下不来了,就会一直hang住 
        //为啥是-3呢 我猜是符合<0的条件吧
        else if (ws == 0 &&
                 !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
            continue;                // loop on failed CAS
    }
    if (h == head)                   // loop if head changed
        break;
}

3.5 releaseShared(int arg)

上面已描述

java 复制代码
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

3.6 acquireSharedInterruptibly

按照上面的思路很明白的知道会主动抛出中断异常的获取锁的方法

3.7 hasQueuedPredecessors()

java 复制代码
public final boolean hasQueuedPredecessors() {
    Node t = tail;
    Node h = head;
    Node s;
    //就是判断当前阻塞队列里是否有阻塞的线程node
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

总结acquireShared:可以资源共享,共享锁,相对于上述独占锁,在创建Node的时候会标记成SHARED,释放资源唤醒头结点的next节点的时候会唤醒紧挨着的SHARED Node节点吗,当然是一个唤醒另一个,因为在if (p == head)的那里循环产生的。

4 总结

如类名一样,通过Queue的Node链表来保存阻塞的线程信息,通过state字段的原子操作代表获取到的资源情况,这个字段是给子类实现用的。

关于AQS的ConditionObject 详解请看详解二
java并发编程 AbstractQueuedSynchronizer(AQS)详解二

相关推荐
神仙别闹1 分钟前
基于C#实现的(WinForm)模拟操作系统文件管理系统
java·git·ffmpeg
小爬虫程序猿2 分钟前
利用Java爬虫速卖通按关键字搜索AliExpress商品
java·开发语言·爬虫
组合缺一7 分钟前
Solon v3.0.5 发布!(Spring 可以退休了吗?)
java·后端·spring·solon
程序猿零零漆10 分钟前
SpringCloud 系列教程:微服务的未来(二)Mybatis-Plus的条件构造器、自定义SQL、Service接口基本用法
java·spring cloud·mybatis-plus
猿来入此小猿11 分钟前
基于SpringBoot在线音乐系统平台功能实现十二
java·spring boot·后端·毕业设计·音乐系统·音乐平台·毕业源码
愤怒的代码25 分钟前
Spring Boot对访问密钥加解密——HMAC-SHA256
java·spring boot·后端
带多刺的玫瑰26 分钟前
Leecode刷题C语言之切蛋糕的最小总开销①
java·数据结构·算法
栗豆包41 分钟前
w118共享汽车管理系统
java·spring boot·后端·spring·tomcat·maven
夜半被帅醒1 小时前
MySQL 数据库优化详解【Java数据库调优】
java·数据库·mysql
万亿少女的梦1681 小时前
基于Spring Boot的网络购物商城的设计与实现
java·spring boot·后端