并发编程之AQS(抽象队列同步器)独占锁

AQS抽象的队列同步器-详解 state:同步状态。=0:表示锁空闲,可以抢占资源。>=1:表示已经有线程占有资源。下面来看一下Node对象定义的内容。java复制代码static final class Node {//共享节点,表示线程以共享的方式等待锁/排他节点,表示线程以独占的方式等待锁//等待状态:取消状态。表示线程获取锁的请求已经取消了//等待状态:阻塞状态。表示线程都准备好了,就等资源释放了//等待状态:条件等待状态。表示节点在等待队列中,节点线程等待唤醒//等待状态:获取共享锁时使用。 阅读详情

前言

在介绍AQS之前,我专门写了CAS、Volatile、synchronized、阻塞队列,链表以及线程中断的笔记,因为在学习AQS之前,如果对于这些基础的东西不了解的话,那么AQS你是看不懂的,AQS我自己认为还是比较难的,但是只要是学习技术,那么如果都那么简单的话,那也就没有专门来学习的必要了,我也难得写笔记来记录它了,因为如果简单,就想1+1=2,没有必要来记录,来记录的目的也是对自己这么多年工作所得的经验的一个记录,不管写的怎么样,既然写出来和大家分享,那么可能写的不是那么好,但是你要是喜欢就看,不喜欢就可以退出,不喜勿喷。我只是将自己这么多年的学习的技术栈记录下来,我本人很喜欢研究底层,所以大多数都是我底层研究所得,不会为了写一个技术去网络上各种搜索,说实话,与其去网络上搜索来记录,还不如自己去研究底层,因为底层的实现原理不会骗人,你自己研究懂了,你就是最专业的,同一个技术原理,可能网络上五花八门,把你都绕晕了,你都不知道该信谁的,要是你自己去研究底层所得,那么它就是对的,你就是专业的,所以写笔记或者博客也只是为了记录自己的所得,也可以作为生活的轨迹,人生轨迹的一部分吧;好了,今天废话有点多,下面开始今天的主题,AQS同步器的记录。

AQS原理

Java并发编程的核心在于java.util.concurrent包。而juc当中大多数同步器实现都是围绕着共同的基础行为,比如等待队列、条件队列、独占获取、共享获取等,而这个行为的抽象就是基于AbstractQueuedSynchronizer,简称AQS。AQS定义了一套多线程访问共享资源的同步器框架,是一个依赖状态(state)的同步器
AQS具备特性:
阻塞等待队列
共享/独占
公平/非公平
可重入
允许中断

这些特性是怎么实现的,以ReentrantLock为例:
一般通过定义内部类Sync [sɪŋk] 继承AQS
将同步器所有调用都映射到Sync对应的方法

同步器队列结构

同步器实现的数据结构为双向链表结构,我们先来了解下双向链表结构,比如下图:
在这里插入图片描述
双向链表的几个基本元素:
head:头结点(指向第一个节点)
tail:尾结点(指向最后一个节点)
prev:前驱节点
nex:后驱节点
双向链表结构在在内存中不是连续的,可能分布在内存的不同区域cell中,它们是通过指针引用关系进行联系的;

ReentrantLock

ReentrantLock是可重入,公平,非公平,可独占锁,它和synchronized的区别这里就不说了,前几篇文章已经说得够多了,这里先看一段程序代码:

public class AqsTest {


    private final static Lock lock = new ReentrantLock();
    static int sum = 0;

    public static void main(String[] args) throws InterruptedException {

        for (int i = 1; i <= 3; i++) {
           Thread thread =  new Thread(new Runnable() {
                @Override
                public void run() {
                    lock.lock();
                    try {
                        for (int j = 1; j <= 10000; j++) {
                            sum ++;
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        lock.unlock();
                    }
                }
            }, "Thread-" + i);
           thread.start();
        }

        Thread.sleep(5000);
        System.out.println(sum);


    }


}

上面的程序代码中启动而来3个线程,每个线程给sum+=10000,如果是线程安全的,那么最后sum的结果肯定是30000,如果我们去掉lock,那么肯定没有办法输出正确结果的,所以lock和synchronized想要达到的目的是一样的,就看在什么场景下使用,在jdk的底层代码阶段,synchronized、lock、cas、Unsafe用的非常多,可以说是开发jdk api的基础,通过上面的例子来分析下ReentrantLock

类构造

public class ReentrantLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = 7373984872572414699L;
    /** Synchronizer providing all implementation mechanics */
    private final Sync sync;

    /**
     * Base of synchronization control for this lock. Subclassed
     * into fair and nonfair versions below. Uses AQS state to
     * represent the number of holds on the lock.
     */
    abstract static class Sync extends AbstractQueuedSynchronizer {

ReentrantLock 实现了Lock接口,Lock接口中定义了锁的基本操作接口方法,比如我们自己要实现一个自己的锁,那么我们也可以实现Lock接口即可;然后ReentrantLock 中有个属性Sync,Sync是继承AbstractQueuedSynchronizer (AQS)抽象队列同步器,而AQS中定义了很多同步队列操作方法,为什么说AQS是抽象队列同步器,因为AQS中定义了很多同步操作方法,包括数据队列双向链表结构,以及很多同步操作的方法,是一个标准;简单来说就是AQS采用了模板设计方法的设计模式来设计AQS,后面的同步器都是基于它的标准来的。
我这边基于3个线程来把ReentrantLock的lock和unlock分析下,首先看构造方法,我们知道ReentrantLock是可公平,非公平的,就是你在构造的时候如果传入true就是公平锁,如果不传入fair或者传入的false就是非公平锁,非公平锁意思就是插队,公平的话就每次进入队列,不能进行插队,我们看下构造:

//下面这个构造就是非公平锁的实现
//非公平的锁是由NonfairSync实现的
public ReentrantLock() {
    sync = new NonfairSync();
}

/**
 * Creates an instance of {@code ReentrantLock} with the
 * given fairness policy.
 *
 * @param fair {@code true} if this lock should use a fair ordering policy
 */
 //下面这个构造可以构造公平或者非公平,根据你传入的
 //fair是否为true,公平锁是由FairSync来实现的
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;

//公平锁直接放入队列
    final void lock() {
        acquire(1);
    }
    
    
    static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;

    /**
     * Performs lock.  Try immediate barge, backing up to normal
     * acquire on failure.
     */
     //非公平锁,lock的时候下面的cas和setExclusiveOwnerThread就是
     //插队
    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }

队列的数据结构信息Node

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;

    /**
     * 节点在等待队列中,节点的线程等待在Condition上,当其他线程对Condition调用了signal()方法后,
     * 该节点会从等待队列中转移到同步队列中,加入到同步状态的获取中
     */
    static final int CONDITION = -2;
    /**
     * 表示下一次共享方式同步状态获取将会被无条件的传播下去
     */
    static final int PROPAGATE = -3;

    /**
     * 标记当前节点的信号量状态(1,0,-1,-2,-3)5种状态
     * 使用CAS更改状态,volatile保证线程可见性,并发场景下,
     * 即被一个线程修改后,状态会立马让其他线程可见
     */
    volatile int waitStatus;

    /**
     * 前驱节点,当前节点加入到同步队列中被设置
     */
    volatile Node prev;

    /**
     * 后继节点
     */
    volatile Node next;

    /**
     * 节点同步状态的线程
     */
    volatile Thread thread;

    /**
     * 等待队列中的后继节点,如果当前节点是共享的,那么这个字段是一个SHARED常量
     * 也就是说节点类型(独占和共享)和等待队列中的后继节点公用一个字段
     * (用在条件队列里面)
     */
    Node nextWaiter;
    }

这个笔记目前只介绍独占锁,但是 AQS是有共享锁的,共享锁在下一个笔记中记录

lock过程

我也在想如何写这个笔记,才能让之后的自己或者别人能够很快理解AQS独占锁的原理,我这边通过程序和debug来分析,在idea中设置debug的时候看线程:
在这里插入图片描述
这样设置了,启动了程序过后,就可以看指定的线程
在这里插入图片描述
我这边只有3个线程,所以就可以debug切换看线程,因为循环是从1开始的,所以很肯定线程1先启动,我们看下:在这里插入图片描述
线程1执行到这里,开始上锁,因为我设置的锁是非公平锁,所以这里进入的非公平锁,
我们分析下代码:

final void lock() {
    //线程的有一个state非常重要,因为ReentrantLock是可重入的锁
    //所以这里cas将state由0修改为1,这里肯定是能成功的,因为
    //目前就一个线程到这里,所以它肯定能成功,这个时候state=1
    if (compareAndSetState(0, 1))
    //cas成功过后,这里是独占锁,独占锁,exclusiveOwnerThread
    //表示独占这个线程,也就是说执行了下面的代码,当前只有这个线程
    //可以运行,而Thread.currentThread()目前是Thread-1
        setExclusiveOwnerThread(Thread.currentThread());
    else
    //这里Thread-1肯定就是不能进入的,这里的意思是如果cas失败,cas失败
    //的情况就是当前有线程获取了锁,我没法获取所,我就进入队列中
           acquire(1);
}

现在线程1获取了锁,就可以执行自己的业务逻辑了,现在我们切换到Threa-2,看下
在这里插入图片描述
在这里插入图片描述
看上图,此时,Threa-1获取了锁,正在执行,这个时候Thread-2又来获取所,很显然,Thread-2是没有办法获取锁的,所以进入队列,我们来看下acquire方法

//arg传入的是1
public final void acquire(int arg) {
    //这个if分为了2部分,第一部分是尝试下获取所,万一
    //这个时候Thread-1释放了,但是我这边是用debug,Thread-1还堵塞
    //在,所以第一个条件肯定是false
    //第二个条件中的括号中的是添加到waiter,也就是添加到等待队列
    if (!tryAcquire(arg) &&
    //这个Node.EXCLUSIVE表示锁的类型,这个是独占模式,目前是null
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

在这里插入图片描述
这个添加到等待队列的deubg如上,首先来创建一个节点,这个节点要存放我们的添加的线程的信息,这个时候
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;//null
this.thread = thread;//Thread-1
}

//添加Thread-2到等待队列
private Node addWaiter(Node mode) {
    //构建一个节点,用来存放Threa-2,mode=null
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    //声明一个pred节点,将尾结点赋值给pred,但是Thread-2进入到这里
    //Threa-1已经在运行,不在队列,所以这里Thread-2到这里的时候
    //tail肯定为空
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    //进队
    enq(node);
    return node;
}

在这里插入图片描述
在这里插入图片描述
这个时候enq进队,上面创建的一个节点node,看下截图,这个时候前驱,后驱都是空的,因为这个队列刚创建了一个节点

private Node enq(final Node node) {
    for (;;) {
        //首先把尾结点给Node t,这个时候|Thread-2到这里时,
        //看上图就知道这个时候head、tail都是空的,所以这里进入if
        Node t = tail;
        if (t == null) { // Must initialize
        //创建一个空节点,将空节点通过cas设置到head,也就是创建的这个
        //节点是头节点head,因为这个时候在构建整个队列
        //如果只创建一个节点,所以这个时候head=tail
        //也就是说head也是tail,tail也是head
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            //因为这里是一个循环,所以第二次循环肯定能到这里
            //第一次循环已经创建了头节点和尾结点了,
            //这个时候t是指向尾结点的
            //所以这里构建队列的结构的时候,node是上面构建的一个
            //Threa-2的节点,这里将尾结点tail指向node当前前驱节点
            //什么意思呢,就是尾结点发送变化,尾结点为node
            //tail.prev = node
            node.prev = t;
            //将尾结点设置为node
            if (compareAndSetTail(t, node)) {
                //cas成功过后,整个队列就构建成功了
                //head=new Node()
                //tail=node
                //node.prev=t
                //t.next=node
                t.next = node;
                return t;
            }
        }
    }
}

enq执行完成过后到这里,这里其实就是阻塞线程的
在这里插入图片描述

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            //Thread-2到这里node是Threa2,这里获取
            //的p就是node的前驱节点,这里判断前驱节点是否
            //是head节点和尝试获取锁,这个时候获取锁
            //失败的
            final Node p = node.predecessor();
            //获取锁过后,设置当前线程为Thead-2,但是这里不能成功的
            //因为Thead-1还没有释放锁
            if (p == head && tryAcquire(arg)) {
                //获取锁过后,设置头节点的线程为空
                //这里要记得,获取锁过后的前驱节点永远是空的
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            //这个时候Thread-2的waitStatus=0,修改状态为-1,表示
            //下次可以被唤醒
            //parkAndCheckInterrupt是调用parak进行阻塞
            //所以线程2在这里就被阻塞在这里,等待下次唤醒
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
//这个方法就是修改waitStatus,-1表示可以被唤醒启动
//如果waitStatus=0,则修改为-1,下次可启动
//如果waitStatus=-1表示可以启动
//如果waitStatus > 0表示线程无效的,要剔除
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true;
    if (ws > 0) {
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * waitStatus must be 0 or PROPAGATE.  Indicate that we
         * need a signal, but don't park yet.  Caller will need to
         * retry to make sure it cannot acquire before parking.
         */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

切换线程3,debug如下:
在这里插入图片描述
这个时候tail不为空,所以进入if,cas修改尾结点为新创建的节点

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) {
        //Threa-3进入这里,新创建的节点的前驱节点为尾结点
        //就是入队的操作,原来的尾结点往前移动,新创建的节点作为
        //尾结点,然后把新创建的节点通过cas修改为尾结点
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

然后Threa3和线程2一样的调用acquireQueued进行阻塞,这个过程中我也不知道如何表述才能完全的表述出来,大概的意思就是同时3个线程进入lock的时候,只有一个lock能进入执行,其他两个线程都进入了等待队列阻塞,然后线程1执行完成过后,调用unlock解锁过后,线程2和线程3才能去获取锁,然后执行完解锁,后面的线程才能获取锁,我们先通过图来理解下过程,lock完了之后再进行unlock的解读,我先来看下获取锁的过程

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    //state =0才能进行尝试获取锁,每次获取锁过后+1
    int c = getState();
    if (c == 0) {
        //cas修改state=1
        if (compareAndSetState(0, acquires)) {
            //修改成功设置当前线程为独占线程
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //下面这个判断是如果已经获取了锁,再次获取锁,也就是
    //重复获取锁,因为lock是可以重入的,所以下面的state+1
    //直到state=0才会释放锁
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

获取锁的逻辑非常简单,就是一般的逻辑代码

lock图解过程

在这里插入图片描述
如上图所示,循环启动3个线程,每个线程循环1万次,上图的过程只是每个线程lock的过程,其中只有第一个线程能够获取锁即Thread-1,Thread-2和Thread-3都是先入队,然后进行各自阻塞,通过上面的分析可知,每次获取不到锁过后,在阻塞之前,都会回去前一个节点然后修改前一个节点的waitStatus=-1,-1表示可以被唤醒的意思;文字描述下3个线程lock的过程:
Thread-1:线程1进入lock时,这个时候没有其他线程在运行,它肯定是能够获取到锁的,所以cas是能够成功的,cas修改的是state由0修改为1,state是控制线程是否能够运行的前置条件,cas成功,设置独占锁为当前线程;
Thread-2:线程2进来就比较悲催了,因为Thread-1获取了锁,那么线程2还是需要尝试去获取锁,也是想把state由0修改为1,但是这个时候cas失败的,因为Thread-1把state已经修改为1了,cas肯定失败,所以失败了就入队,这个时候队列还是空的,所以分两步:
1.创建Node节点对象node,Node属性为:

waitStatus=0
thread=Thread-2
nextWait=null
pre=null

1.创建一个队列的空节点t,这个节点作为队头head和队尾tail,Node属性为

waitStatus=0
thread=null
nextWait=null
pre=null

2.第二次循环将node节点通过cas修改为尾结点,然后node.prev=t,t.next=node;
3.将线程2节点添加的队列中过后,然后进行获取node.prev节点,修改prev的节点的waitStatus由0修改为-1,表示可以被唤醒的线程;
Thread-3:线程3一样的进行lock的时候也要尝试cas,很显然cas也是失败的,和线程2一样去添加到等待队列中,这个时候:
head node

waitStatus=-1
thread=null
nextWait=Thread-1
pre=null

thread-1 node:

waitStatus=-1
thread=Thread
nextWait=thread2 node
pre=head node

thread-2 node(tail):

waitStatus=0
thread=Thread-2
nextWait=null
pre=thread-1 node

最后就完成了这种双向链表的数据结构

unlock过程

通过上面的debug过程过后,我们的3个线程目前是这样的状况:
在这里插入图片描述
Thread-1已经执行完了自己的业务逻辑,准备解锁了
在这里插入图片描述
而线程2和3都是出于这种状况,就是线程被挂起的状态,2和3已经被阻塞挂起了,我们继续debug让Thread-1解锁
在这里插入图片描述
线程1解锁debug进入这里,我先来分析写这个方法的业务逻辑

final boolean release(int arg) {
    //tryRelease解锁的过程
    if (tryRelease(arg)) {
        //解锁成功过后,从对头开始唤醒下一个线程开始执行
        //也就是说每一个线程在结束过后会自动去唤醒下一个线程开始执行
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
protected final boolean tryRelease(int releases) {
    //线程解锁的过程,需要将state-1
    int c = getState() - releases;
    //如果要解锁的线程和目前独占的线程不一样,则知报错,
    //报的非法monitor状态异常,我们还记得吗?
    //我们直接在非synchronized的代码块中使用wait也会报这个错误
    //所以这里的判断就是你解锁的线程根本不是独占的线程
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        //将独占线程设置为null,下一个获取锁的线程就可以设置当前线程
        setExclusiveOwnerThread(null);
    }
    //将state回写到内存,让所有的线程都可以看到
    setState(c);
    return free;
}
//向被阻塞的线程发一个许可unpark,告诉它可以执行了
//node=head节点,然后唤醒的目标是node.next
private void unparkSuccessor(Node node) {
    /*
     * If status is negative (i.e., possibly needing signal) try
     * to clear in anticipation of signalling.  It is OK if this
     * fails or if status is changed by waiting thread.
     */
    int ws = node.waitStatus;
    if (ws < 0)
    //把node=head的头节点的waitstatus修改为0
        compareAndSetWaitStatus(node, ws, 0);

    /*
     * Thread to unpark is held in successor, which is normally
     * just the next node.  But if cancelled or apparently null,
     * traverse backwards from tail to find the actual
     * non-cancelled successor.
     */
    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;
    }
    if (s != null)
    //唤醒下一个线程
        LockSupport.unpark(s.thread);
}

我们线程1唤醒线程2过后,其实s.thread就是线程2,我们再切到线程2看下线程2在哪里
在这里插入图片描述
可以看到线程2还在原来的哪里阻塞到再,therad-1唤醒它过后,它开始执行了,现在线程2要获取锁是能获取成功了
在这里插入图片描述
线程2执行tryAcquire已经成功了,首先node.prev肯定是等于head的,因为如果从队列中唤醒的线程的节点永远是第二个节点,也就是head.next=node,所以这里会进去
我们着重分析下if的代码块

//下面的代码其实简单来说就是出队,将头节点出队
//设置当前执行的节点为头结点
setHead(node);
//节点往前移动了,原来的节点进行回收,方便gc
p.next = null; // help GC
failed = false;
return interrupted;

private void setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}

执行完上述代码过后,节点信息如下:
head(del head head = node1):

waitStatus=0
thread=null
prev=null
next=node2

node1(del node1 ):没有了,node1就是当前运行的线程1
node2:

waitStatus=0
thread=thread-3
prev=head
next=null

这就是整个解锁的过程,是配合加锁一起完成的,原理就是每个获得lock的线程在完成过后解锁完成过后,要讲队列中head.next的节点进行出队执行,所以aqs独占锁有个特点
就是头结点head用元是空节点,在构建节点的时候第一次创建的也是空节点,所以head节点我觉得就是一个过渡,为了得到next节点的信息的一个数据结构;
还有就是有两个重要的信息需要注意:
state:线程获取锁过后+1,每个线程需要获取锁的条件是state=0
waitStatus:线程被唤醒的状态,-1才能阻塞被唤醒,但是不包括最后一个线程
因为每次入队更新的时候都是prev更新为-1,那么最后一个节点的线程永远是0;

unlock图解过程

在这里插入图片描述
unlock的解锁过程如上图,简单来说就是执行完成过后,释放锁,然后从队列中取第一个节点然后唤醒它可以出队执行了,然后唤醒的节点开始获取锁,获取锁成功过后出队执行就是这么个过程,过程简单,主要是吸收Doug Lea大神的思想,别人为什么要这么做;这其中涉的两个参数state和WaitStatus和和加锁的一样。

总结

AQS独占锁这个原理,我有时候也不知道如何去写,自己懂和表述是两个概念,因为Doug Lea在实现的时候写的非常绕,首先要理解它的设计理念以及原理,否则代码能力再高的人也看不懂,懂了理念和设计原理再看就很轻松了,所以我就采用debug的模式截图和源码解释来解读AQS底层实现原理,如果谁看到了这篇文章,如果有疑问的大家可以进行交流。

Java并发编程之AQS AQS是AbstractQueueSynchronizer是缩写,其意思就是抽象队列同步器。原理就是通过一个FIFO队列维护一个state同步状态值,只需要继承该抽象类重写对应的方法便可实现一套线程同步机制。AQS抽象类在java.util.concurrent.locks包下定义。如上图所示,AQS属于一个抽象类,主要是用来构建锁结构以及同步器的一个抽象类。AQS继承了AbstractOwnableSynchronizer,其作用为设置和获取独占锁的拥有者线程方法。 阅读详情

相关推荐

【Java并发编程】4. 一文搞定JUC半壁江山 AQS

文章目录AQSAQS了解AQS中模版模式AQS重要方法模板方法独占式获取共享式获取独占式释放共享式释放子类需实现方法独占式获取独占式释放共享式获取共享式释放 AQS AQS了解 在JDK中我们一般用AQS来构建跟实现显示锁,语言层面我们一般用Syn来实现。AQS(AbstractQueuedSynchronizer 抽象队列同步器),在上一文中被广泛使用。比如CountDownLatch,Thre...

sowhat 3740

AQS(抽象队列同步器)

一、什么是 AQS ? AQS即AbstractQueuedSynchronizer的缩写,是并发编程中实现同步器的一个框架。 AQS基于一个FIFO双向队列实现,被设计给那些依赖一个代表状态的原子int值的同步器使用。我们都知道,既然叫同步器,那个肯定有个代表同步状态(临界资源)的东西,在AQS中即为一个叫state的int值,该值通过CAS进行原子修改。 在AQS中存在一个...

青鱼入云的博客 2163

Java 并发编程:ReentrantLock 锁与 AQS

大家好,我是栗筝i,这篇文章是我的 “栗筝i 的 Java 技术栈” 专栏的第 030 篇文章,在 “栗筝i 的 Java 技术栈” 这个专栏中我会持续为大家更新 Java 技术相关全套技术栈内容。专栏的主要目标是已经有一定 Java 开发经验,并希望进一步完善自己对整个 Java 技术体系来充实自己的技术栈的同学。与此同时,本专栏的所有文章,也都会准备充足的代码示例和完善的知识点梳理,因此也十分适合零基础的小白和要准备工作面试的同学学习。

栗筝i的博客 2817

AQS 抽象队列同步器

ReentrantLock, Semaphore, CountDownLatch 等。 AQS 的核心思想是基于一个同步状态变量来管理线程之间的共享资源访问。这个状态变量是一个整数,通过内部队列(CLH lock 队列的一种变种)来协调多个线程对这个状态的访问。当一个线程请求访问被另一个线程占用的状态时,它会被放入队列中等待

junxinsiwo的专栏 1392

BMS | 从一串到几百串硬件架构简介

锂电池在生活中越来越多的方面扮演能源的角色,除了大家熟知的电动汽车行业,在一些消费类,工业类电子产品比如手机,电动自行车,电动工具中都有应用,追本溯源今天我们一起来学习下最基础的BMS架构。 1~4串架构 以S8241为代表的无通信/无固件/模拟器件 可实现过充保护、过放保护、过流/短路,参数由器件后缀代码决定;通信类手持设备供电系统;1~4节电池保护板架构 如图使用S-8241系列保护IC,一对充放电MOS管控制充放电的回路的切断和接通,系统供电来自于电芯电压,VM引脚用于检测过...

ningvay的博客 9920

从零到一:Mx_yolo本地训练与K210部署实战指南

本文详细介绍了从零开始进行Mx_yolo本地模型训练到K210部署的全流程实战指南。涵盖环境配置、数据准备、模型训练与调优、模型转换及K210部署等关键步骤,并提供了常见问题解决方案和性能优化技巧,帮助开发者高效完成AI模型在嵌入式设备的落地应用。

weixin_29081163的博客 252

并发编程——07 深入理解AQS之独占锁ReentrantLock源码分析

管程——Java同步的设计思想、AQS原理分析、ReentrantLock 源码分析

念头通达 701

Python爬虫(一)京东商品价格及详情页抓取

这是一个以http://item.jd.com/2957726.html为例的单网页抓取案例 import urllib.request; #载入urllib.request,用于获取页面html源代码 from pandas import Series; #载入series包 from pandas import DataFrame; #载入dataframe包 from bs4 impo

Data Proc 1万+

AQS核心原理分析《上》

本篇文章是关于并发编程中AQS的简介和组成知识记录,由于篇幅原因,独占模式和共享模式将会在下篇记录。

小威的博客 5723

致远OA公文管理模块相关操作

致远oa协同办公系统 公文管理模板 相关操作

weixin_59666050的博客 4278

微信小程序 - 最新详细实现模板消息订阅通知功能,给用户微信发送“服务通知“模板消息并点击进入跳转到小程序页面路由,订阅消息通知详细后台申请流程及示例代码,解决授权申请弹框被拒绝后无法调起、常见报错!

微信小程序,vue2,vue3,模板消息,订阅通知,服务通知,小程序模板通知,订阅微信模板消息功能,唤起模板消息订阅弹框并发送服务通知,消息订阅与模板消息发送,一次性订阅,长期永久订阅,申请模板id,详细示例代码教程,配置流程,消息模板,模板ID,用户收不到消息,订阅了通知但微信不来服务通知,报错,小程序平台模板消息订阅功能,唤起订阅模板消息弹框,订阅模板消息发送服务通知功能,如何做模板消息订阅,一直收不到通知,用户通过弹窗订阅,订阅消息,无法打开申请授权弹框,用户拒绝/关闭后,如何引导用户再开启,拒绝后怎

🏆 前端领域优质创作者 5329

抽象队列同步器 AQS

抽象队列同步器AQS

scj1022的博客 1547

华为OD机试双机位A卷 - 小明减肥 (C++ & Python & JAVA & JS & GO)

华为OD机试2025双机位A卷 小明减肥,提供Java、C++、Python、Go、JavaScript源码实现,并提供完整思路讲解

qq_45776114的博客 710

并发编程学习之AQS抽象队列同步器

一、简介 AQS是AbstractQueuedSynchronizer的简写,翻译过来就是:抽象队列同步器。AbstractQueuedSynchronizer在java.util.concurrent.locks包中,声明如下: public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchroni...

weixiaohuai的博客 716

【异常】安装hermes-agent提示Git克隆仓库网络中断 fetch-pack: unexpected disconnect while reading sideband packet

本文分析了Git克隆仓库时出现网络中断错误的常见原因及解决方案。报错表现为连接被远程主机关闭、管道破裂或数据包意外终止,通常由网络不稳定、仓库过大或配置不当引起。提供了5种优先级排序的解决方法:1)使用浅克隆减少数据量;2)优化Git配置参数;3)切换网络或配置代理;4)检查SSH密钥设置;5)改用HTTPS方式克隆。特别推荐先尝试「浅克隆+HTTPS」组合方案快速验证,若问题持续再根据具体报错深入排查。文中包含详细的命令示例和配置说明,适合不同网络环境下的Git用户参考。

奶爸的编程之路,也就一周冷个三天 1421

到底什么是指令集?什么是微架构?他们是什么关系?

指令集、处理器架构、芯片厂商、ARM、x86,其间关系还在迷糊吗?一文迅速理清!!

车载系统攻城狮 7813

学Java线程,你不知道什么是AQS?一文带你了解Java多线程同步的灵魂

AQS 定义了同步器的基本操作,如获取、释放和状态管理,并提供了一个等待队列来管理等待资源的线程,解决了在实现同步器时涉及的大量细节问题,例如自定义标准同步状态、FIFO 同步队列。

fumeidonga的博客 5612

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

五、聊聊并发- 聊聊AQS独占锁

文章目录前言为什么有Synchronized还要设计AQS呢?AQS的设计思想AQS核心实现AQS核心要素状态(state)同步队列(FIFO)独占锁&共享锁Exclusive(独占模式): ReenTranLock exampleFairSync(公平锁)tryAcquireaddWaiterenqacquireQueuedshouldParkAfterFailedAcquirepark...

livolador’s blog 842

WINCC7.3破解文件

WINCC7.3硬件够破解文件,有需要的下载!WINCC7.3硬件够破解文件,有需要的下载!

android 蓝牙通讯demo

android 下蓝牙通讯demo,实现了发现蓝牙,使能蓝牙,通过蓝牙发送消息。

AQS 核心原理实现(独占锁)

AQS (AbstractQueuedSynchronizer) 抽象队列同步器 通过源码我们可以看到其实AQS是使用了CAS,保证原子性。 那么CAS到底是什么呢? CAS:Compare and Swap,即比较再交换。 CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,...

weixin_38982591的博客 294

efm32 入门.pdf

本PDF共计22页,介绍的是操作驱动API,下载与否自己斟酌。

抽象队列同步器(AQS)的原理与应用

尽管 JUC 提供的工具已经非常丰富,但在某些极端复杂的业务场景下,我们可能需要自定义同步语义。这时,继承 AQS 就是最佳选择。MutexLock下面我们实现一个简单的、不可重入的独占锁MutexLock。// 自定义不可重入独占锁// 静态内部类,继承AQS// 判断是否处于锁定状态@Override// 尝试获取锁@Override// 使用CAS操作,期望state为0,如果成功则设置为1// 尝试释放锁@Override// 直接设置为0,因为不可重入。

EscaRC的博客 1117

Java并发编程-AQS

AQS既AbstractQueuedSynchronizer队列同步器 ,**是用来构建锁或者其他同步组件的基础框架**,它使用了一个 int 成员变量表示同步状态,通过内置的 FIFO 队列来完成资源获取线程的排队工作。

千月落 8757

7、深入理解AQS独占锁之ReentrantLock源码分析

java.util.concurrent包中的大多数同步器实现都是围绕着共同的基础行为,比如等待队列、条件队列、独占获取、共享获取等,而这些行为的抽象就是基于AbstractQueuedSynchronizer(简称AQS)实现的,AQS是一个抽象同步框架,可以用来实现一个依赖状态的同步器。JDK中提供的大多数的同步器如Lock, Latch, Barrier等,都是基于AQS框架来实现的一般是通过一个内部类Sync继承 AQS将同步器所有调用都映射到Sync对应的方法阻塞等待队列共享/独占。

weixin_46825305的博客 548

AQS的核心原理分析

之前已经写过一篇关于AQS的介绍了,基本概念我就不多逼逼了,我直接把我那篇文章复制过来。后面来根据java的源码分析这个AQS的内部实现。 前言:这一部分的基础概念讲解,全部都是我之前的一篇博客:谈谈你对AQS的了解 1.写在前面: 这篇文章,我们来聊聊面试时一个比较有杀伤力的问题:聊聊你对AQS的理解? 之前有同学反馈,去互联网公司面试,面试官聊到并发时就问到了这个问题。当时那位同学内心估计受到了一万点伤害。。。因为首先,很多人可能连AQS是什么都不知道。或者仅仅是听说过AQS这个名词,但是可能连

oldshaui的博客 5250

手写Lock锁与AQS抽象队列同步器

手写Lock锁 锁的实现需要: 两个方法:lock()、unlock() 两个组件:锁的持有者、等待队列 代码: public class CustomLock implements Lock{ //当前前程 AtomicReference<Thread> current = new AtomicReference(); //等待队列 LinkedBl...

Der_Dream的博客 363
上一篇: 并发编程之线程中断&CAS
下一篇: 并发编程之AQS(抽象队列同步器)共享锁
Life  journey
博客等级 码龄17年 1万+粉丝 80原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值