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)详解二

相关推荐
吾日三省吾码3 小时前
JVM 性能调优
java
弗拉唐4 小时前
springBoot,mp,ssm整合案例
java·spring boot·mybatis
oi774 小时前
使用itextpdf进行pdf模版填充中文文本时部分字不显示问题
java·服务器
少说多做3435 小时前
Android 不同情况下使用 runOnUiThread
android·java
知兀5 小时前
Java的方法、基本和引用数据类型
java·笔记·黑马程序员
蓝黑20205 小时前
IntelliJ IDEA常用快捷键
java·ide·intellij-idea
Ysjt | 深5 小时前
C++多线程编程入门教程(优质版)
java·开发语言·jvm·c++
shuangrenlong5 小时前
slice介绍slice查看器
java·ubuntu
牧竹子5 小时前
对原jar包解压后修改原class文件后重新打包为jar
java·jar