AQS

news/2024/12/29 15:09:48/文章来源:https://www.cnblogs.com/leizia/p/18523403

目录
  • AQS简介
  • AQS原理
    • AbstractQueuedSynchronizer类
    • state共享变量
    • CLH队列
    • Node属性
    • 独占式分析
      • acquire独占式获取锁
        • tryAcquire
        • addWaiter
          • enq
        • acquireQueued
          • shouldParkAfterFailedAcquire
          • parkAndCheckInterrupt
          • finally代码块
          • selfInterrupt
      • release独占式锁释放
        • unparkSuccessor
      • acquirelnterruptibly独占式可中断获取锁
        • doAcquireInterruptibly
        • finally代码块
        • cancelAcquire取消获取锁请求
          • cancelAcquire案例演示
      • tryAcquireNanos独占式超时获取锁
        • doAcquireNanos
        • finally代码块
      • 独占式获取/释放锁总结
        • acquire流程图
        • release流程图
    • 共享式分析
      • acquireShared共享锁获取
      • tryAcquireShared尝试获取共享锁
      • doAcquireShared自旋获取共享锁
        • setHeadAndPropagat设置结点并传播信息
          • doReleaseShared唤醒后继结点
      • reaseShared共享式释放锁
      • acquireSharedInterruptibly共享式可中断获取锁
        • doAcquireSharedInterruptibly共享式可中断获取锁
      • tryAcquireSharedNanos共享式超时获取锁
        • doAcquireSharedNanos共享式超时获取锁
      • 共享式获取/释放锁总结


大佬地址:

Java并发之AQS详解

AbstractQueuedSynchronizer 详细解读

Java 并发编程 AQS 介绍和源码解析(配视频)

AQS很难,面试不会?看我一篇文章吊打面试官

【Java并发】并发编程的意义是什么?月薪30K必知必会的Java AQS机制

AQS(AbstractQueuedSynchronizer)源码深度解析(3)—同步队列以及独占式获取锁、释放锁的原理【一万字】

AQS(AbstractQueuedSynchronizer)源码深度解析(4)—共享式获取锁、释放锁的原理【一万字】

精美图文讲解Java AQS 共享式获取同步状态以及Semaphore的应用

深入浅出理解Java并发AQS的共享锁模式


AQS简介

AbstractQueuedSynchronizer(AQS)提供了一套可用于实现锁同步机制的框架,不夸张地说,AQSJUC同步框架的基石。AQS通过一个FIFO队列维护线程同步状态,实现类只需要继承该类,并重写指定方法即可实现一套线程同步机制。

"Abstract"指的是抽象,这通常意味着AQS是一个旨在被继承的抽象类,为子类提供共通的功能模板。紧接着,"Queued"诠释了队列的概念,暗示在高并发环境中,当多个线程竞争同一个资源时,未能获取资源的线程将会被排列在一个阻塞队列中,依次等待获取机会。最后,"Synchronizer"即同步器,强调了AQS的设计初衷——为线程同步提供支持和框架。简而言之,AQS是一个为同步而设计的抽象队列同步器。

public abstract class AbstractQueuedSynchronizerextends AbstractOwnableSynchronizerimplements java.io.Serializable {}

在JUC中,最核心的组件便是AQS,AQS是对CAS的一种封装和丰富,AQS根据资源互斥级别提供了独占和共享两种资源访问模式;同时其定义Condition结构提供了wait/signal等待唤醒机制。在JUC中,诸如ReentrantLockCountDownLatch等都基于AQS实现。

AQS解决了在实现同步器时涉及的大量细节问题,例如等待线程采用FIFO队列操作顺序。在不同的同步器中还可以定义一些灵活的标准来判断某个线程是应该通过还是需要等待。

AQS原理

AbstractQueuedSynchronizer类

public abstract class AbstractQueuedSynchronizer extendsAbstractOwnableSynchronizer implements java.io.Serializable {/*** 当前获取锁的线程,该变量定义在父类中,AQS直接继承。在独占锁的获取时,如果是重入锁,那么需要知道到底是哪个线程获得了锁。没有就是null*/private transient Thread exclusiveOwnerThread;/*** AQS中保持的对同步队列的引用* 队列头结点,实际上是一个哨兵结点,不代表任何线程,head所指向的Node的thread属性永远是null。*/private transient volatile Node head;/*** 队列尾结点,后续的结点都加入到队列尾部*/private transient volatile Node tail;/*** 同步状态*/private volatile int state;/*** Node内部类,同步队列的结点类型*/static final class Node {/*AQS支持共享模式和独占模式两种类型,下面表示构造的结点类型标记*//*** 共享模式下构造的结点,用来标记该线程是获取共享资源时被阻塞挂起后放入AQS 队列的*/static final Node SHARED = new Node();/*** 独占模式下构造的结点,用来标记该线程是获取独占资源时被阻塞挂起后放入AQS 队列的*/static final Node EXCLUSIVE = null;/*线程结点的等待状态,用来表示该线程所处的等待锁的状态*//*** 指示当前结点(线程)需要取消等待* 由于在同步队列中等待的线程发生等待超时、中断、异常,即放弃获取锁,需要从同步队列中取消等待,就会变成这个状态* 如果结点进入该状态,那么不会再变成其他状态*/static final int CANCELLED = 1;/*** 指示当前结点(线程)的后续结点(线程)需要取消等待(被唤醒)* 如果一个结点状态被设置为SIGNAL,那么后继结点的线程处于挂起或者即将挂起的状态* 当前结点的线程如果释放了锁或者放弃获取锁并且结点状态为SIGNAL,那么将会尝试唤醒后继结点的线程以运行* 这个状态通常是由后继结点给前驱结点设置的。一个结点的线程将被挂起时,会尝试设置前驱结点的状态为SIGNAL*/static final int SIGNAL = -1;/*** 线程在等待队列里面等待,waitStatus值表示线程正在等待条件* 原本结点在等待队列中,结点线程等待在Condition上,当其他线程对Condition调用了signal()方法之后* 该结点会从从等待队列中转移到同步队列中,进行同步状态的获取*/static final int CONDITION = -2;/*** 释放共享资源时需要通知其他结点,waitStatus值表示下一个共享式同步状态的获取应该无条件传播下去*/static final int PROPAGATE = -3;/*** 记录当前线程等待状态值,包括以上4中的状态,还有0,表示初始化状态*/volatile int waitStatus;/*** 前驱结点,当结点加入同步队列将会被设置前驱结点信息*/volatile Node prev;/*** 后继结点*/volatile Node next;/*** 当前获取到同步状态的线程*/volatile Thread thread;/*** 等待队列中的后继结点,如果当前结点是共享模式的,那么这个字段是一个SHARED常量* 在独占锁模式下永远为null,仅仅起到一个标记作用,没有实际意义。*/Node nextWaiter;/*** 如果是共享模式下等待,那么返回true(因为上面的Node nextWaiter字段在共享模式下是一个SHARED常量)*/final boolean isShared() {return nextWaiter == SHARED;}/*** 用于建立初始头结点或SHARED标记*/Node() {}/*** 用于添加到等待队列** @param thread* @param mode*/Node(Thread thread, Node mode) {this.nextWaiter = mode;this.thread = thread;}//......}}
}

state共享变量

锁的状态是通过一个使用volatile的整形变量state来表示当前是否可以获取锁,volatile保证了线程之间的可见性。可见性简单来说,就是当一个线程修改了state的值,其他线程下一次读取都能读到最新值。通过getState(),setState(),compareAndSetState()三个方法进行维护。

关于state的几个要点:

使用volatile修饰,保证多线程间的可见性。

getState()、setState()、compareAndSetState()使用final修饰,限制子类不能对其重写。

compareAndSetState()采用乐观锁思想的CAS算法,保证原子性操作。

一般来说0表示锁没被占用,大于0表示所已经被占用了。

state的类型是int,为什么不是boolean?

private volatile int state

这里就要谈到线程获取锁的两种模式,独占和共享。简单介绍一下,当一个线程以独占模式获取锁时,其他任何线程都必须等待,而当一个线程以共享模式获取锁时,其他也想以共享模式获取锁的线程也能够一起访问共享资源,但其他想以独占模式获取锁的线程需要等待。这就说明了,共享模式下,可能有多个线程正在共享资源,所以state需要表示线程占用数量,因此是int类型值。如在某些非共享锁里,state=1 则代表当前锁已经被占有,此时如果有线程来请求锁时且当前独占模式的锁已经被其他线程占有,则会进入AQS里维持的CLH队列(FIFO)里排队,HEAD 位置的线程为正在占用锁的线程,当它释放锁时,会唤醒Head的next 节点的线程,next节点使用自旋的方式不断的尝试获取锁。 head和tail变量表示这个队列的头尾。

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {private transient volatile Node head;private transient volatile Node tail;private volatile int state;}

CLH队列

在CLH队列中,每个节点代表一个等待锁的线程,通过自旋锁进行等待。state变量被用来表示是否阻塞,即锁是否被占用。

CLH队列的设计使得多个线程可以高效地竞争同一个锁资源。由于每个线程只需要在自己的节点上进行自旋等待,而不需要遍历整个队列,因此减少了不必要的上下文切换和资源消耗。

同步队列是"CLH" (Craig, Landin, andHagersten) 锁队列的变体,它的head引用指向的头结点作为哨兵结点,不存储任何与等待线程相关的信息,或者可以看成已经获得锁的结点。第二个结点开始才是真正的等待线程构建的结点,后续的结点会加入到链表尾部。

将新结点添加到链表尾部的方法是compareAndSetTail(Node expect,Node update)方法,该方法是一个CAS方法,能够保证线程安全。

最终获取锁的线程所在的结点,会被设置成为头结点(setHead方法),该设置步骤是通过获取锁成功的线程来完成的,由于只有一个线程能够成功获取到锁,因此设置的方法并不需要使用CAS来保证。

同步队列遵循先进先出(FIFO),头结点的next结点是将要获取到锁的结点,线程在释放锁的时候将会唤醒后继结点,然后后继结点会尝试获取锁。

Node属性

  • EXCLUSIVE
static final Node EXCLUSIVE = null;

表示一个独占节点,即只有一个线程可以获取锁资源,如ReentrantLock。当一个线程成功获取锁时,会创建一个EXCLUSIVE节点对象并将其设置为当前线程的节点状态。当其他线程获取锁时,发现已经有线程持有了锁,则将自身封装成一个EXCLUSIVE节点并加入等待队列中。

  • SHARED
static final Node SHARED = new Node();

表示一个共享节点,即多个线程可以同时获取锁资源,如ReentrantReadWriteLock。与EXCLUSIVE不同,SHARED允许多个线程同时持有锁,但仍需要循序公平性。当一个线程请求共享锁时,如果锁是可用的,则线程可以直接获取锁;否则,线程会被封装成一个SHARED节点并加入等待队列中。

  • waitStatus
volatile int waitStatus;

waitStatus当前节点状态,该字段主要包含几种状态:

状态值 说明
0 节点初始化时的默认值或节点已经释放锁
CANCELLED = 1 节点引用线程由于等待超时或获取锁的请求已经被取消
SIGNAL = -1 表示当前节点的后续节点需要被唤醒
CONDITION = -2 表示当前节点正在等待某一个Condition对象
PROPAGATE = -3 仅在释放共享锁releaseShared时对头节点使用,当一个线程释放锁或者资源时,如果头节点是PROPAGATE状态,它会将释放操作传播到后续的节点,以便这些节点也能尝试获取共享资源。

独占式分析

独占式:顾名思义就是同一时刻只能有一个线程获取到锁,其他获取锁线程只能处于同步队列中等待,只有获取锁的线程释放了锁,后继的线程才能够获取到锁。

  • 大概流程:

独占式获取的锁是与具体线程绑定的,就是说如果一个线程获取到了锁,exclusiveOwnerThread字段就会记录这个线程,其他线程再尝试操作state 获取锁时会发现当前该锁不是自己持有的,就会在获取失败后被放入AQS 同步队列。比如独占锁ReentrantLock 的实现, 当一个线程获取了ReentrantLock 的锁后,在AQS 内部会首先使用CAS操作把state 状态值从0变为1 ,然后设置当前锁的持有者为当前线程,当该线程再次获取锁时发现它就是锁的持有者,则会把状态值从1变为2,也就是设置可重入次数,而当另外一个线程获取锁时发现自己并不是该锁的持有者就会被放入AQS 同步队列后挂起。

acquire独占式获取锁

通过调用AQS的acquire模版方法可以独占式的获取锁,该方法不会响应中断,也就是由于线程获取同步状态失败后进入同步队列中,后续对线程进行中断操作时,线程不会从同步队列中移出。基于独占式实现的组件有ReentrantLock等。

/*** 独占式的尝试获取锁,一直获取不成功就进入同步队列等待*/
public final void acquire(int arg) {if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();
}
  • 大概步骤如下:

首先调用tryAcquire方法尝试获取锁,如果获取锁成功会返回true,方法结束;否则获取锁失败返回false,然后进行下一步的操作。

通过addWaiter方法将线程按照独占模式Node.EXCLUSIVE构造同步结点,并添加到同步队列的尾部。

然后通过acquireQueued(Node node,int arg)方法继续自旋获取锁。

一次自旋中如果获取不到锁,那么判断是否可以挂起并尝试挂起结点中的线程(调用LockSupport.park(this)方法挂起自己,注意这里的线程状态是WAITING)。而挂起线程的唤醒主要依靠前驱结点或线程被中断来实现,注意唤醒之后会继续自旋尝试获得锁。

最终只有获得锁的线程才能从acquireQueued方法返回,然后根据返回值判断是否调用selfInterrupt设置中断标志位,但此时线程处于运行态,即使设置中断标志位也不会抛出异常(即acquire(lock)方法不会响应中断)。

线程获得锁,acquire方法结束,从lock方法中返回,继续后续执行同步代码!

tryAcquire

tryAcquire是一个被protected修饰的方法,参数是一个int值,代表对int state的增加操作,返回值是boolean,获取成功该方法就返回true,失败就返回false,用于首次尝试获取独占锁,一般来说就是对state的改变、或者重入锁的检查、设置当前获得锁的线程等等。

该方法只有一行实现,抛出一个throw new UnsupportedOperationException();异常,AQS规定继承类必须override tryAcquire方法,否则就直接抛出UnsupportedOperationException

protected boolean tryAcquire(int arg) {throw new UnsupportedOperationException();
}

那么为什么这里一定需要上层自己实现?因为尝试获取锁这个操作中可能包含某些业务自定义的逻辑,比如是否"可重入"等。若上层调用tryAcquire返回true,线程获得锁,此时可以对相应的共享资源进行操作,使用完之后再进行释放。如果调用tryAcquire返回false,且上层逻辑上不想等待锁,那么可以自己进行相应的处理;如果上层逻辑选择等待锁,那么可以直接调用acquire方法,acquire方法内部封装了复杂的排队处理逻辑,非常易用。一般情况下,如果是独占锁,获取锁时通过CAS 将 state 置为 1 ,释放锁时 通过 CAS 将 state 置为 0。

addWaiter
/*** addWaiter(Node node)方法将获取锁失败的线程构造成结点加入到同步队列的尾部** @param mode 模式。独占模式传入的是一个Node.EXCLUSIVE,即null;共享模式传入的是一个Node.SHARED,即一个静态结点对象(共享的、同一个)* @return 返回构造的结点*/
private Node addWaiter(Node mode) {/*1 首先构造结点*/Node node = new Node(Thread.currentThread(), mode);/*2 尝试将结点直接放在队尾*///直接获取同步器的tail结点,使用pred来保存Node pred = tail;/*如果pred不为null,实际上就是队列不为null* 那么使用CAS方式将当前结点设为尾结点* */if (pred != null) {node.prev = pred;//通过使用compareAndSetTail的CAS方法来确保结点能够被线程安全的添加,虽然不一定能成功。if (compareAndSetTail(pred, node)) {//将新构造的结点置为原队尾结点的后继pred.next = node;//返回新结点return node;}}/** 3 走到这里,可能是:* (1) 由于可能是并发条件,并且上面的CAS操作并没有循环尝试,因此可能添加失败* (2) 队列可能为null* 调用enq方法,采用自旋方式保证构造的新结点成功添加到同步队列中* */enq(node);return node;
}/*** addWaiter方法中使用到的Node构造器** @param thread 当前线程* @param mode   模式*/
Node(Thread thread, AbstractQueuedSynchronizer.Node mode) {//等待队列中的后继结点 就等于该结点的模式//由此可知,共享模式该值为Node.SHARED结点常量,独占模式该值为nullthis.nextWaiter = mode;//当前线程this.thread = thread;
}

addWaiter方法用于将按照独占模式构造的同步结点Node.EXCLUSIVE添加到同步队列的尾部。

  • 大概步骤为:

按照给定模式,将当前线程构建成一个新结点Node,然后加入等待队列,返回值即为该Node

如果同步队列不为null,则尝试将新结点添加到队列尾部(只尝试一次),如果添加成功则返回新结点,方法结束。

如果队列为null或者添加失败,则调用enq方法循环尝试添加,直到成功,返回新结点,方法结束。

由于在多线程环境下执行,方法中重点细节:

1、某线程执行到第13行时,pred引用指向的对象可能已经不再是尾节点,所以CAS失败;

2、如果CAS成功,诚然CAS操作是具有原子性的,但是14、15两行在执行时并不具备原子性,只不过此时pred节点和当前节点的相对位置已经确定,其他线程只是正在插入新的尾节点,并不会影响到这里的操作,所以线程是安全的。

3、需要记住的是,当前后两个节点建立连接的时候,首先是后节点的pre指向前节点,当后节点成功成为尾节点后,前节点的next才会指向后节点。

如果理解了这些,再来看18行。如果程序运行到这一行,说明出现了两种情况之一:

队列为空

快速插入失败,想要进行完整流程的插入,这里所说的快速插入,指的就是11~17行的逻辑,当并发线程较少的情况下,快速插入成功率很高,程序不用进入完整流程插入,效率会更高。

enq

enq方法用在同步队列为null或者一次CAS添加失败的时候,enq要保证结点最终必定添加成功。

/*** 循环,直到尾结点添加成功*/
private Node enq(final Node node) {/*死循环操作,直到添加成功*/for (; ; ) {//获取尾结点tNode t = tail;/*如果队列为null,则初始化同步队列*/if (t == null) {/*调用compareAndSetHead方法,初始化同步队列* 注意:这里是新建了一个空白结点,这就是传说中的哨兵结点* CAS成功之后,head将指向该哨兵结点,返回true* */if (compareAndSetHead(new Node()))//尾结点指向头结点(哨兵结点)tail = head;/*之后并没有结束,而是继续循环,此时队列已经不为空了,因此会进行下面的逻辑*/}/*如果队列不为null,则和外面的的方法类似,调用compareAndSetTail方法,新建新结点到同步队列尾部*/else {/*1 首先修改新结点前驱的指向,这一步不是安全的但是没关系,因为这一步如果发生了冲突,那么下面的CAS操作必然之后有一条线程会成功其他线程将会重新循环尝试*/node.prev = t;/** 2 调用compareAndSetTail方法通过CAS方式尝试将结点添加到同步队列尾部* 如果添加成功,那么才能继续下一步,结束这个死循环,否则就会不断循环尝试添加* */if (compareAndSetTail(t, node)) {//3 修改原尾结点后继结点的指向t.next = node;//返回新结点,结束死循环return t;}}}
}/*** CAS添加头结点. 仅仅在enq方法中用到** @param update 头结点* @return true 成功;false 失败*/
private final boolean compareAndSetHead(Node update) {return unsafe.compareAndSwapObject(this, headOffset, null, update);
}/*** CAS添加尾结点. 仅仅在enq方法中用到** @param expect 预期原尾结点* @param update 新尾结点* @return true 成功;false 失败*/
private final boolean compareAndSetTail(Node expect, Node update) {return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}
  • 大概步骤为:

开启一个死循环,在死循环中进行如下操作;

如果队列为空,那么初始化队列,添加一个哨兵结点,结束本次循环,继续下一次循环;

如果队列不为空,那么向前面的方法一样,则尝试将新结点添加到队列尾部,如果添加成功则返回新结点的前驱,循环结束;如果不成功,结束本次循环,继续下一次循环。

enq方法返回的是新结点的前驱,当然在addWaiter方法中没有用到。

另外,添加头结点使用的compareAndSetHead方法和添加尾结点使用的compareAndSetTail方法都是CAS方法,并且都是调用Unsafe类中的本地方法,因为线程挂机、恢复、CAS操作等最终会通过操作系统中实现,Unsafe类就提供了Java与底层操作系统进行交互的直接接口,这个类的里面的许多操作类似于C的指针操作,通过找到对某个属性的偏移量,直接对该属性赋值,因为与Java本地方法对接都是Hospot源码中的方法,而这些的方法都是采用C++写的,必须使用指针!

在addWaiter和enq方法中,成为尾结点需要三步:

设置前驱prev

设置tail

设置后继next

由于第二步设置tail是CAS操作,那么只能保证node的前驱prev一定是正确的,但是此后设置后继的操作却不一定能够马上成功就切换到了其他线程,此时next可能为null,但实际他的后继并不一定真的为null。

因此同步队列只能保证前驱prev一定是可靠的,但是next却不一定可靠,所以后面的源码的遍历操作基本上都是从后向前通过前驱prev进行遍历的。

acquireQueued

能够走到该方法,那么说明通过了tryAcquire()和addWaiter()方法,表示该线程获取锁已经失败并且被放入同步队列尾部了。

acquireQueued方法表示结点进入同步队列之后的动作,实际上就进入了一个自旋的过程,自旋过程中,当条件满足,获取到了锁,就可以从这个自旋中退出并返回,否则可能会阻塞该结点的线程,后续即使阻塞被唤醒,还是会自旋尝试获取锁,直到成功或者而抛出异常。

最终如果该方法会因为获取到锁而退出,则会返回否被中断标志的标志位 或者 因为异常而退出,则会抛出异常!

  • 大概步骤为:

同样开启一个死循环,在死循环中进行下面的操作;

如果当前结点的前驱是head结点,那么尝试获取锁,如果获取锁成功,那么当前结点设置为头结点head,当前结点线程出队,表示当前线程已经获取到了锁,然后返回是否被中断标志,结束循环,进入finally;

如果当前结点的前驱不是head结点或者尝试获取锁失败,那么判断当前线程是否应该被挂起,如果返回true,那么调用parkAndCheckInterrupt挂起当前结点的线程(LockSupport.park 方法挂起线程,线程出于WAITING),此时不再执行后续的步骤、代码。

如果当前线程不应该被挂起,即返回false,那本次循环结束,继续下一次循环。

如果线程被其他线程唤醒,那么判断是否是因为中断而被唤醒并修改标志位,同时继续循环,直到在步骤2获得锁,才能跳出循环!(这也是acquire方法不会响应中断的原理—park方法被中断时不会抛出异常,仅仅是从挂起状态返回,然后需要继续尝试获取锁)

最终,线程获得了锁跳出循环,或者发生异常跳出循环,那么会执行finally语句块,finally中判断线程是否是因为发生异常而跳出循环,如果是,那么执行cancelAcquire方法取消该结点获取锁的请求;如果不是,即因为获得锁跳出循环,则finally中什么也不干!

/*** @param node 新结点* @param arg  参数* @return 如果在等待时中断,则返回true*/
final boolean acquireQueued(final Node node, int arg) {//failed表示获取锁是否失败标志boolean failed = true;try {//interrupted表示是否被中断标志boolean interrupted = false;/*死循环*/for (; ; ) {//获取新结点的前驱结点final Node p = node.predecessor();/*只有前驱结点是头结点的时候才能尝试获取锁* 同样调用tryAcquire方法获取锁* */if (p == head && tryAcquire(arg)) {//获取到锁之后,就将自己设置为头结点(哨兵结点),线程出队列setHead(node);//前驱结点(原哨兵结点)的链接置空,由JVM回收p.next = null;//获取锁是否失败改成false,表示成功获取到了锁failed = false;//返回interrupted,即返回线程是否被中断return interrupted;}/*前驱结点不是头结点或者获取同步状态失败*//*shouldParkAfterFailedAcquire检测线程是否应该被挂起,如果返回true* 则调用parkAndCheckInterrupt用于将线程挂起* 否则重新开始循环* */if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())/*到这一步,说明是当前结点(线程)因为被中断而唤醒,那就改变自己的中断标志位状态信息为true* 然后又从新开始循环,直到获取到锁,才能返回* */interrupted = true;}}/*线程获取到锁或者发生异常之后都会执行的finally语句块*/ finally {/*如果failed为true,表示获取锁失败,即对应发生异常的情况,这里发生异常的情况只有在tryAcquire方法和predecessor方法中可能会抛出异常,此时还没有获得锁,failed=true那么执行cancelAcquire方法,该方法用于取消该线程获取锁的请求,将该结点的线程状态改为CANCELLED,并尝试移除结点(如果是尾结点)另外,在超时等待获取锁的的方法中,如果超过时间没有获取到锁,也会调用该方法如果failed为false,表示获取到了锁,那么该方法直接结束,继续往下执行;*/if (failed)//取消获取锁请求,将当前结点从队列中移除,cancelAcquire(node);}
}/*** 位于Node结点类中的方法* 返回上一个结点,或在 null 时引发 NullPointerException。 当前置不能为空时使用。 空检查可以取消,表示此异常无代码层面的意义,但可以帮助 VM?所以这个异常到底有啥用?** @return 此结点的前驱*/
final Node predecessor() throws NullPointerException {//获取前驱Node p = prev;//如果为null,则抛出异常if (p == null)throw new NullPointerException();else//返回前驱return p;
}/*** head指向node新结点,该方法是在tryAcquire获取锁之后调用,不会产生线程安全问题** @param node 新结点*/
private void setHead(Node node) {head = node;//新结点的thread和prev属性置空//即丢弃原来的头结点,新结点成为哨兵结点,内部线程出队//设置里虽然线程引用置空了,但是一般在tryAcquire方法中轨记录获取到锁的线程,因此不担心找不到是哪个线程获取到了锁//这里也能看出,哨兵结点或许也可以叫做"获取到锁的结点"node.thread = null;node.prev = null;
}
shouldParkAfterFailedAcquire

shouldParkAfterFailedAcquire方法在没有获取到锁之后调用,用于判断当前结点是否需要被挂起。

  • 大概步骤如下:

如果前驱结点已经是SIGNAL(-1)状态,说明前置节点也在等待拿锁,并且之后将会唤醒当前节点,所以当前结点可以挂起,返回true,方法结束;

否则,如果前驱结点状态大于0,即 Node.CANCELLED,表示前驱结点放弃了锁的等待,那么由该前驱向前查找,直到找到一个状态小于等于0的结点,当前结点排在该结点后面,返回false,方法结束;

否则,前驱结点的状态既不是SIGNAL(-1),也不是CANCELLED(1),尝试CAS设置前驱结点的状态为SIGNAL(-1),返回false,方法结束!

只有前驱结点状态为SIGNAL时,当前结点才能安心挂起,否则一直自旋!

从这里能看出来,一个结点的SIGNAL状态一般都是由它的后继结点设置的,但是这个状态却是表示后继结点的状态,表示的意思就是前驱结点如果释放了锁,那么就有义务唤醒后继结点!

/*** 检测当前结点(线程)是否应该被挂起** @param pred 该结点的前驱* @param node 该结点* @return 如果前驱结点已经是SIGNAL状态,当前结点才能挂起,返回true;否则,可能会查找新的前驱结点或者尝试将前驱结点设置为SIGNAL状态,返回false*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {//获取 前取的waitStatus_等待状态//回顾创建结点时候,并没有给waitStatus赋值,因此每一个结点最开始的时候waitStatus的值都为0int ws = pred.waitStatus;/*如果前驱结点已经是SIGNAL状态,即表示当前结点可以挂起*/if (ws == Node.SIGNAL)return true;/*如果前驱结点状态大于0,即 Node.CANCELLED 表示前驱结点放弃了锁的等待*/if (ws > 0) {/*由该前驱向前查找,直到找到一个状态小于等于0的结点(即没有被取消的结点),当前结点成为该结点的后驱,这一步很重要,可能会清理一段被取消了的结点,并且如果该前驱释放了锁,还会唤醒它的后继,保持队列活性*/do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);pred.next = node;}/*否则,前驱结点的状态既不是SIGNAL(-1),也不是CANCELLED(1)*/else {/*前驱结点的状态CAS设置为SIGNAL(-1),可能失败,但没关系,因为失败之后会一直循环*/compareAndSetWaitStatus(pred, ws, Node.SIGNAL);}//返回false,表示当前结点不能挂起return false;
}
parkAndCheckInterrupt

shouldParkAfterFailedAcquire方法返回true之后,将会调用parkAndCheckInterrupt方法挂起线程并且后续判断中断状态。

分两步:

使用LockSupport.park(this)挂起该线程,不再执行后续的步骤、代码。直到该线程被中断或者被唤醒(unpark)

如果该线程被中断或者唤醒,那么返回Thread.interrupted()方法的返回值,该方法用于判断前线程的中断状态,并且清除该中断状态,即,如果该线程因为被中断而唤醒,则中断状态为true,将中断状态重置为false,并返回true,如果该线程不是因为中断被唤醒,则中断状态为false,并返回false。

/*** 挂起线程,在线程返回后返回中断状态** @return 如果因为线程中断而返回,而返回true,否则返回false*/
private final boolean parkAndCheckInterrupt() {/*1)使用LockSupport.park(this)挂起该线程,不再执行后续的步骤、代码。直到该线程被中断或者被唤醒(unpark)*/LockSupport.park(this);/*2)如果该线程被中断或者唤醒,那么返回Thread.interrupted()方法的返回值,该方法用于判断前线程的中断状态,并且清除该中断状态,即,如果该线程因为被中断而唤醒,则中断状态为true,将中断状态重置为false,并返回true,注意park方法被中断时不会抛出异常!如果该线程不是因为中断被唤醒,则中断状态为false,并返回false*/return Thread.interrupted();
}

park()会让当前线程进入waiting状态。在此状态下,有两种途径可以唤醒该线程:

1)被unpark()

2)被interrupt()

此时当前Node中的线程将阻塞在此处,直到持有锁的线程调用release方法,release方法会唤醒后续节点。

通过对acquireQueued这个方法的分析,我们可以这么说,如果当前线程所在的节点处于头节点的后一个,那么它将会不断去尝试拿锁,直到获取锁成功。否则进行判断,是否需要挂起。这样就能保证head之后的一个节点在自旋CAS获取锁,其他线程都已经被挂起或正在被挂起。这样就能最大限度地避免无用的自旋消耗CPU。

finally代码块

在acquireQueued方法中,具有一个finally代码块,那么无论try中发生了什么,finally代码块都会执行的。在acquire独占式不可中断获取锁的方法中,执行finally的只有两种情况:

当前结点(线程)最终获取到了锁,此时会进入finally,而在获取到锁之后会设置failed = false。

在try中发生了异常,此时直接跳到finally中。这里发生异常的情况只可能在tryAcquire或predecessor方法中发生,然后直接进入finally代码块中,此时还没有获得锁,failed=true!

  1. tryAcquire方法是我们自己实现的,抛出什么异常由我们来定,就算抛出异常一般也不会在acquireQueued中抛出,可能在最开始调用tryAcquire时就抛出了。
  2. predecessor方法中,会检查如果前驱结点为null则抛出NullPointerException。但是注释中又说这个检查无代码层面的意义,或许是这个异常永远不会抛出?

finally代码块中的逻辑为:

如果failed = true,表示没有获取锁而进行finally,即发生了异常。那么执行cancelAcquire方法取消当前结点线程获取锁的请求,acquireQueued方法结束,然后抛出异常。

如果failed = false,表示已经获取到了锁,那么实际上finally中什么都不会执行。acquireQueued方法结束,返回interrupted—是否被中断标志。

综上所述,在acquire独占式不可中断获取锁的方法中,大部分情况在finally中都是什么也不干就返回了,或者说抛出异常的情况基本没有,因此cancelAcquire方法基本不考虑。

但是在可中断获取锁或者超时获取锁的方法中,执行到cancelAcquire方法的情况还是比较常见的。因此将cancelAcquire方法的源码分析放到可中断获取锁方法的源码分析部分!

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

selfInterruptacquire中最后可能调用的一个方法,顾名思义,用于安全的中断,什么意思呢,就是根据!tryAcquireacquireQueued返回值判断是否需要设置中断标志位。

只有tryAcquire尝试失败,并且acquireQueued方法true时,才表示该线程是被中断过了的,但是在parkAndCheckInterrupt里面判断中断标志位之后又重置的中断标志位(interrupted方法会重置中断标志位)。

虽然看起来没啥用,但是本着负责的态度,还是将中断标志位记录下来。那么此时重新设置该线程的中断标志位为true。

/*** 中断当前线程,由于此时当前线程出于运行态,因此只会设置中断标志位,并不会抛出异常*/
static void selfInterrupt() {Thread.currentThread().interrupt();
}

release独占式锁释放

当前线程获取到锁并执行了相应逻辑之后,就需要释放锁,使得后续结点能够继续获取锁。通过调用AQS的release(int arg)模版方法可以独占式的释放锁。

大概步骤如下:

尝试使用tryRelease(arg)释放锁,该方法在最开始我们就讲过,是自己实现的方法,通常来说就是将state值为0或者减少、清除当前获得锁的线程等等,如果符合自己的逻辑,锁释放成功则返回true,否则返回false;

如果tryRelease释放成功返回true,判断如果head不为null且head的状态不为0,那么尝试调用unparkSuccessor方法唤醒头结点之后的一个非取消状态(非CANCELLED状态)的后继结点,让其可以进行锁获取。返回true,方法结束;

如果tryRelease释放失败,那么返回false,方法结束。

/*** 独占式的释放同步状态** @param arg 参数* @return 释放成功返回true, 否则返回false*/
public final boolean release(int arg) {/*tryRelease释放同步状态,该方法是自己重写实现的方法释放成功将返回true,否则返回false或者自己实现的逻辑*/if (tryRelease(arg)) {//获取头结点Node h = head;//如果头结点不为null并且状态不等于0if (h != null && h.waitStatus != 0)/*那么唤醒头结点的一个出于等待锁状态的后继结点* 该方法在acquire中已经讲过了* */unparkSuccessor(h);return true;}return false;
}

独占模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果彻底释放了(即state=0),它会唤醒等待队列里的其他线程来获取资源。

unparkSuccessor

unparkSuccessor用于唤醒参数结点的某个非取消的后继结点,该方法在很多地方法都被调用。

  • 大概步骤:

如果当前结点的状态小于0,那么CAS设置为0,表示后继结点可以继续尝试获取锁。

如果当前结点的后继s为null或者状态为取消CANCELLED,则将s先指向null;然后从tail开始到node之间倒序向前查找,找到离tail最近的非取消结点赋给s。需要从后向前遍历,因为同步队列只保证结点前驱关系的正确性。

如果s不为null,那么状态肯定不是取消CANCELLED,则直接唤醒s的线程,调用LockSupport.unpark方法唤醒,被唤醒的结点将从被park的位置继续执行!

/*** 唤醒指定结点的后继结点** @param node 指定结点*/
private void unparkSuccessor(Node node) {int ws = node.waitStatus;/** 1)  如果当前结点的状态小于0,那么CAS设置为0,表示后继结点线程可以先尝试获锁,而不是直接挂起。* */if (ws < 0)compareAndSetWaitStatus(node, ws, 0);//先获取node的直接后继Node s = node.next;/** 2)  如果s为null或者状态为取消CANCELLED,则从tail开始到node之间倒序向前查找,找到离tail最近的非取消结点赋给s。* */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;}/** 3)如果s不为null,那么状态肯定不是取消CANCELLED,则直接唤醒s的线程,调用LockSupport.unpark方法唤醒,被唤醒的结点将从被park的位置向后执行!* */if (s != null)LockSupport.unpark(s.thread);
}

acquirelnterruptibly独占式可中断获取锁

上面分析的独占式获取锁的方法acquire,是不会响应中断的。但是AQS提供了另外一个acquireInterruptibly模版方法,调用该方法的线程在等待获取锁时,如果当前线程被中断,会立刻返回,并抛出InterruptedException

public final void acquireInterruptibly(int arg)throws InterruptedException {//如果当前线程被中断,直接抛出异常if (Thread.interrupted())throw new InterruptedException();//尝试获取锁if (!tryAcquire(arg))//如果没获取到,那么调用AQS 可被中断的方法doAcquireInterruptibly(arg);
}
doAcquireInterruptibly

doAcquireInterruptibly会首先判断线程是否是中断状态,如果是则直接返回并抛出异常,其他不步骤和独占式不可中断获取锁基本原理一致。还有一点的区别就是在后续挂起的线程因为线程被中断而返回时的处理方式不一样:

  1. 独占式不可中断获取锁仅仅是记录该状态,interrupted = true,紧接着又继续循环获取锁;
  2. 独占式可中断获取锁则直接抛出异常,因此会直接跳出循环去执行finally代码块。
/*** 独占可中断式的锁获取** @param arg 参数*/
private void doAcquireInterruptibly(int arg)throws InterruptedException {//同样调用addWaiter将当前线程构造成结点加入到同步队列尾部final Node node = addWaiter(Node.EXCLUSIVE);//获取锁失败标志,默认为trueboolean failed = true;try {/*和独占式不可中断方法acquireQueued一样,循环获取锁*/for (; ; ) {final Node p = node.predecessor();if (p == head && tryAcquire(arg)) {setHead(node);p.next = null; // help GCfailed = false;return;}if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())/** 这里就是区别所在,独占不可中断式方法acquireQueued中* 如果线程被中断,此处仅仅会记录该状态,interrupted = true,紧接着又继续循环获取锁** 但是在该独占可中断式的锁获取方法中* 如果线程被中断,此处直接抛出异常,因此会直接跳出循环去执行finally代码块* */throw new InterruptedException();}}/*获取到锁或者抛出异常都会执行finally代码块*/ finally {/*如果获取锁失败。可能就是线程被中断了,那么执行cancelAcquire方法取消该结点对锁的请求,该线程结束*/if (failed)cancelAcquire(node);}
}
finally代码块

在doAcquireInterruptibly方法中,具有一个finally代码块,那么无论try中发生了什么,finally代码块都会执行的。在acquireInterruptibly独占式可中断获取锁的方法中,执行finally的只有两种情况:

当前结点(线程)最终获取到了锁,此时会进入finally,而在获取到锁之后会设置failed = false。

在try中发生了异常,此时直接跳到finally中,这里发生异常的情况可能在tryAcquirepredecessor方法中,更加有可能的原因是因为线程被中断而抛出InterruptedException异常,然后直接进入finally代码块中,此时还没有获得锁,failed=true。

  1. tryAcquire方法是我们自己实现的,抛出什么异常由我们来定,就算抛出异常一般也不会在doAcquireInterruptibly中抛出,可能在最开始调用tryAcquire时就抛出了。
  2. predecessor方法中,会检查如果前驱结点为null则抛出NullPointerException。但是注释中又说这个检查无代码层面的意义,或许是这个异常永远不会抛出?
  3. 根据doAcquireInterruptibly逻辑,如果线程在挂起过程中被中断,那么将主动抛出InterruptedException异常,这也是被称为"可中断"的逻辑

finally代码块中的逻辑为:

  1. 如果failed = true,表示没有获取锁而进行finally,即发生了异常。那么执行cancelAcquire方法取消当前结点线程获取锁的请求,doAcquireInterruptibly方法结束,抛出异常!
  2. 如果failed = false,表示已经获取到了锁,那么实际上finally中什么都不会执行,doAcquireInterruptibly方法结束。
cancelAcquire取消获取锁请求

由于独占式可中断获取锁的方法中,线程被中断而抛出异常的情况比较常见,因此这里分析finally中cancelAcquire的源码。cancelAcquire方法用于取消结点获取锁的请求,参数为需要取消的结点node。

  • 大概步骤为:

node记录的线程thread置为null

跳过已取消的前置结点。由node向前查找,直到找到一个状态小于等于0的结点pred (即找一个没有取消的结点),更新node.prev为找到的pred。

node的等待状态waitStatus置为CANCELLED,即取消请求锁。

如果node是尾结点,那么尝试CAS更新tail指向pred,成功之后继续CAS设置pred.next为null。

否则,说明node不是尾结点或者CAS失败(可能存在对尾结点的并发操作):

  1. 如果node不是head的后继 并且 (pred的状态为SIGNAL或者将pred的waitStatus置为SIGNAL成功) 并且 pred记录的线程不为null。那么设置pred.next指向node.next。最后node.next指向node自己。
  2. 否则,说明node是head的后继 或者pred状态设置失败 或者 pred记录的线程为null。那么调用unparkSuccessor唤醒node的一个没取消的后继结点。最后node.next指向node自己。
/*** 取消指定结点获取锁的请求** @param node 指定结点*/
private void cancelAcquire(Node node) {// Ignore if node doesn't existif (node == null)return;/*1 node记录的线程thread置为null*/node.thread = null;/*2 类似于shouldParkAfterFailedAcquire方法中查找有效前驱的代码:do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);pred.next = node;这里同样由node向前查找,直到找到一个状态小于等于0的结点(即没有被取消的结点),作为前驱但是这里只更新了node.prev,没有更新pred.next*/Node pred = node.prev;while (pred.waitStatus > 0)node.prev = pred = pred.prev;//predNext记录pred的后继,后续CAS会用到。Node predNext = pred.next;/*3 node的等待状态设置为CANCELLED,即取消请求锁*/node.waitStatus = Node.CANCELLED;/*4 如果当前结点是尾结点,那么尝试CAS更新tail指向pred,成功之后继续CAS设置pred.next为null。*/if (node == tail && compareAndSetTail(node, pred)) {//新尾结点pred的next结点设置为null,即使失败了也没关系,说明有其它新入队线程或者其它取消线程更新掉了。compareAndSetNext(pred, predNext, null);}/*5 否则,说明node不是尾结点或者CAS失败(可能存在对尾结点的并发操作),这种情况要做的事情是把pred和node的后继非取消结点拼起来。*/else {int ws;/*5.1 如果node不是head的后继 并且 (pred的状态为SIGNAL或者将pred的waitStatus置为SIGNAL成功) 并且 pred记录的线程不为null。那么设置pred.next指向node.next。这里没有设置prev,但是没关系。此时pred的后继变成了node的后继—next,后续next结点如果获取到锁,那么在shouldParkAfterFailedAcquire方法中查找有效前驱时,也会找到这个没取消的pred,同时将next.prev指向pred,也就设置了prev关系了。*/if (pred != head &&((ws = pred.waitStatus) == Node.SIGNAL ||(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&pred.thread != null) {//获取next结点Node next = node.next;//如果next结点存在且未被取消if (next != null && next.waitStatus <= 0)//那么CAS设置perd.next指向node.nextcompareAndSetNext(pred, predNext, next);}/*5.2 否则,说明node是head的后继 或者pred状态设置失败 或者 pred记录的线程为null。** 此时需要调用unparkSuccessor方法尝试唤醒node结点的后继结点,因为node作为head的后继结点是唯一有资格取尝试获取锁的结点。* 如果外部线程A释放锁,但是还没有调用unpark唤醒node的时候,此时node被中断或者发生异常,这时node将会调用cancelAcquire取消,结点内部的记录线程变成null,* 此时就是算A线程的unpark方法执行,也只是LockSupport.unpark(null)而已,也就不会唤醒任何结点了* 那么node后面的结点也不会被唤醒了,队列就失活了;如果在这种情况下,在node将会调用cancelAcquire取消的代码中* 调用一次unparkSuccessor,那么将唤醒被取消结点的后继结点,让后继结点可以尝试获取锁,从而保证队列活性!** 前面对node进行取消的代码中,并没有将node彻底移除队列,* 而被唤醒的结点会尝试获取锁,而在在获取到锁之后,在* setHead(node);* p.next = null; // help GC* 部分,可能将这些被取消的结点清除* */else {unparkSuccessor(node);}/*最后node.next指向node自身,方便后续GC时直接销毁无效结点同时也是为了Condition的isOnSyncQueue方法,判断一个原先属于条件队列的结点是否转移到了同步队列。因为同步队列中会用到结点的next域,取消结点的next也有值的话,可以断言next域有值的结点一定在同步队列上。这里也能看出来,遍历的时候应该采用倒序遍历,否则采用正序遍历可能出现死循环*/node.next = node;}
}
cancelAcquire案例演示

假设一个同步队列结构如下,有ABCDE五个线程调用acquireInterruptibly方法争夺锁,并且BCDE线程都是因为获取不到锁而导致的阻塞。

如果此时线程D被中断,那么抛出异常进入finally代码块,属于node不是尾结点,node不是head的后继的情况,如下图:

在cancelAcquire方法之后的结构如下:

如果此时线程E被中断,那么抛出异常进入finally代码块,属于node是尾结点的情况,如下图:

在cancelAcquire方法之后的结构如下:

如果此时进来了两个新线程F、G,并且又都被挂起了,那么此时同步队列结构如下图:

可以看到,实际上该队列出现了分叉,这种情况在同步队列中是很常见的,因为被取消的结点并没有主动去除自己的prev引用。那么这部分被取消的结点无法被删除吗,其实是可以的,只不过需要满足一定的条件结构!如果此时线程B被中断,那么抛出异常进入finally代码块,属于node不是尾结点,node是head的后继的情况,如下图:

在cancelAcquire方法之后的结构如下:

注意在这种情况下,node还会调用unparkSuccessor方法唤醒后继结点C,让C尝试获取锁,如果假设此时线程A的锁还没有使用完毕,那么此时C肯定不能获取到锁。

但是C也不是什么都没做,C在被唤醒之后获得CPU执行权的那段时间里,在doAcquireInterruptibly方法的for循环中,改变了一些引用关系。

它会判断自己是否可以被挂起,此时它的前驱被取消了waitStatus=1,明显不能,因此会继续向前寻找有效的前驱,最终C被挂起之后的结构如下:

可以看到C最终和head结点直接链接了起来,但是此时被取消的B由于具有prev引用,因此还没有被GC,不要急,这是因为还没到指定结构,到了就自然会被GC了。

如果此时线程A的资源使用完毕,那么首先释放锁,然后会尝试唤醒一个没有取消的后继线程,明显选择C。

如果在A释放锁之后,调用LockSupport.unpark方法唤醒C之前,C被先一步因中断而唤醒了。此时C抛出异常,不会再去获得锁,而是去finally执行cancelAcquire方法去了,此时还是属于node不是尾结点,node是head的后继的情况,如下图:

那么在C执行完cancelAcquire方法之后的结构如下:

如果此时线程A又获取到了CPU的执行权,执行LockSupport.unpark,但此时结点C因为被中断而取消,其内部记录的线程变量变成了null,LockSupport.unpark(null),将会什么也不做。那么这时队列岂不是失活了?其实并没有!

此时,cancelAcquire方法中的"node"不是尾结点,node是head的后继”这种情况下的unparkSuccessor方法就非常关键了。该方法用于唤醒被取消结点C的一个没被取消的后继结点F,让其尝试获取锁,这样就能保证队列不失活。

F被唤醒之后,会判断是否能够休眠,明显不能,因为前驱node的状态为1,此时经过循环中一系列方法的操作,会变成如下结构:

明显结点F是head的直接后继,可以获取锁。

在获取锁成功之后,F会将自己设置为新的head,此时又会改变一些引用关系,即将F与前驱结点的prev和next关系都移除:

setHead(node);
p.next = null; // help GC

引用关系改变之后的结构下:

可以看到,到这一步,才会真正的将哪些无效结点删除,被GC回收。那么,需要真正删除一个结点需要有什么条件?条件就是:如果某个结点获取到了锁,那么该结点的前驱以及和该结点前驱相关的结点都将会被删除!

但是,在上面的分析中,我们认为只要有节点引用关联就不会被GC回收,然而实际上现代Java虚拟机采用可达性分析算法来分析垃圾,因此,在上面的队列中,对于那些“分叉”,即那些被取消的、只剩下prev引用的、最重要的是不能通过head和tail的引用链到达也没有外部引用可达的节点,将会在可达性分析算法中被标记为垃圾并在一次GC中被直接回收!

比如此时F线程执行完了,下一个就是G,那么G获得锁之后,F将会被删除,最终结构如下:

tryAcquireNanos独占式超时获取锁

独占式超时获取锁tryAcquireNanos模版方法可以被视作响应中断获取锁acquireInterruptibly方法的“增强版”,支持中断,支持超时时间!

/*** 独占式超时获取锁,支持中断** @param arg          参数* @param nanosTimeout 超时时间,纳秒* @return 是否获取锁成功* @throws InterruptedException 如果被中断,则抛出InterruptedException异常*/
public final boolean tryAcquireNanos(int arg, long nanosTimeout)throws InterruptedException {//如果当前线程被中断,直接抛出异常if (Thread.interrupted())throw new InterruptedException();//同样调用tryAcquire尝试获取锁,如果获取成功则直接返回true//否则调用doAcquireNanos方法挂起指定一段时间,该短时间内获取到了锁则返回true,超时还未获取到锁则返回falsereturn tryAcquire(arg) ||doAcquireNanos(arg, nanosTimeout);
}
doAcquireNanos

doAcquireNanos方法在支持响应中断的基础上, 增加了超时获取的特性。

该方法在自旋过程中,当结点的前驱结点为头结点时尝试获取锁,如果获取成功则从该方法返回,这个过程和独占式同步获取的过程类似,但是在锁获取失败的处理上有所不同。

如果当前线程获取锁失败,则判断是否超时(nanosTimeout小于等于0表示已经超时),如果没有超时,重新计算超时间隔nanosTimeout,然后使当前线程等待nanosTimeout纳秒(当已到设置的超时时间,该线程会从LockSupport.parkNanos(Objectblocker,long nanos)方法返回)。

如果nanosTimeout小于等于spinForTimeoutThreshold(1000纳秒)时,将不会使该线程进行超时等待,而是进入快速的自旋过程。原因在于,非常短的超时等待无法做到十分精确,如果这时再进行超时等待,相反会让nanosTimeout的超时从整体上表现得反而不精确。

因此,在超时非常短的场景下,AQS会进入无条件的快速自旋而不是挂起线程。

static final long spinForTimeoutThreshold = 1000L;/*** 独占式超时获取锁** @param arg          参数* @param nanosTimeout 剩余超时时间,纳秒* @return true 成功 ;false 失败* @throws InterruptedException 如果被中断,则抛出InterruptedException异常*/
private boolean doAcquireNanos(int arg, long nanosTimeout)throws InterruptedException {//获取当前的纳秒时间long lastTime = System.nanoTime();//同样调用addWaiter将当前线程构造成结点加入到同步队列尾部final Node node = addWaiter(Node.EXCLUSIVE);boolean failed = true;try {/*和独占式不可中断方法acquireQueued一样,循环获取锁*/for (; ; ) {final Node p = node.predecessor();if (p == head && tryAcquire(arg)) {setHead(node);p.next = null; // help GCfailed = false;return true;}/*这里就是区别所在*///如果剩余超时时间小于0,则退出循环,返回false,表示没获取到锁if (nanosTimeout <= 0)return false;//如果需要挂起 并且 剩余nanosTimeout大于spinForTimeoutThreshold,即大于1000纳秒if (shouldParkAfterFailedAcquire(p, node)&& nanosTimeout > spinForTimeoutThreshold)//那么调用LockSupport.parkNanos方法将当前线程挂起nanosTimeoutLockSupport.parkNanos(this, nanosTimeout);//获取当前纳秒,走到这一步可能是线程中途被唤醒了long now = System.nanoTime();//计算 新的剩余超时时间:原剩余超时时间 - (当前时间now - 上一次计算时的时间lastTime)nanosTimeout -= now - lastTime;//lastIme赋值为本次计算时的时间lastTime = now;//如果线程被中断了,那么直接抛出异常if (Thread.interrupted())throw new InterruptedException();}}/*获取到锁、超时时间到了、抛出异常都会执行finally代码块*/finally {/*如果获取锁失败。可能就是线程被中断了,那么执行cancelAcquire方法取消该结点对锁的请求,该线程结束* 或者是超时时间到了,那么执行cancelAcquire方法取消该结点对锁的请求,将返回false* */if (failed)cancelAcquire(node);}
}
finally代码块

在doAcquireNanos方法中,具有一个finally代码块,那么无论try中发生了什么,finally代码块都会执行的。在tryAcquireNanos独占式超时获取锁的方法中,执行finally的只有三种情况:

当前结点(线程)最终获取到了锁,此时会进入finally,而在获取到锁之后会设置failed = false。

在try中发生了异常,此时直接跳到finally中,这里发生异常的情况可能在tryAcquire、predecessor方法中,更加有可能的原因是因为线程被中断而抛出InterruptedException异常,然后直接进入finally代码块中,此时还没有获得锁,failed=true!

  1. tryAcquire方法是我们自己实现的,据抛出什么异常由我们来定,就算抛出异常一般也不会在doAcquireNanos中抛出,可能在最开始调用tryAcquire时就抛出了。
  2. predecessor方法中,会检查如果前驱结点为null则抛出NullPointerException。但是注释中又说这个检查无代码层面的意义,或许是这个异常永远不会抛出?
  3. 根据doAcquireNanos逻辑,如果线程在挂起过程中被中断,那么将主动抛出InterruptedException异常,这也是被称为“可中断”的逻辑。

方法的超时时间到了,当前线程还没有获取到锁,那么,将会跳出循环,直接进入finally代码块中,此时还没有获得锁,failed=true!

finally代码块中的逻辑为:

  1. 如果failed = true,表示没有获取锁而进行finally,可能发生了异常 或者 超时时间到了。那么执行cancelAcquire方法取消当前结点线程获取锁的请求,doAcquireNanos方法结束,抛出异常 或者返回 false。
  2. 如果failed = false,表示已经获取到了锁,那么实际上finally中什么都不会执行,doAcquireNanos方法结束,返回true。

独占式获取/释放锁总结

独占式的获取锁和释放锁的方法中,我们需要重写tryAcquire 和tryRelease 方法。

独占式的获取锁和释放锁时,需要在tryAcquire方法中记录到底是哪一个线程获取了锁。一般使用exclusiveOwnerThread字段(setExclusiveOwnerThread方法)记录,在tryRelease 方法释放锁成功之后清楚该字段的值。

acquire流程图

release流程图

共享式分析

共享式:同一时刻能够有多个线程获取到锁。

在独占锁的实现中会使用一个exclusiveOwnerThread属性,用来记录当前持有锁的线程。当独占锁已经被某个线程持有时,其他线程只能等待它被释放后,才能去争锁,并且同一时刻只有一个线程能争锁成功。

对于共享锁来说,如果一个线程成功获取了共享锁,那么其他等待在这个共享锁上的线程就也可以尝试去获取锁,并且极有可能获取成功。基于共享式实现的组件有CountDownLatch、Semaphore等。

acquireShared共享锁获取

acquireShared(int)是共享锁模式下线程获取共享资源的入口方法,它会获取指定量的资源,获取成功则直接返回,获取失败则进入等待队列,直到获取到资源为止,整个过程无法响应中断

  • 大概步骤如下:

首先使用tryAcquireShared尝试获取锁,获取成功(返回值大于等于0)则直接返回;

否则,调用doAcquireShared将当前线程封装为Node.SHARED模式的Node 结点后加入到AQS 同步队列的尾部,然后"自旋"尝试获取锁,如果还是获取不到,那么最终使用park方法挂起自己等待被唤醒。

/*** 共享式获取锁的模版方法,不响应中断** @param arg 参数*/
public final void acquireShared(int arg) {//尝试调用tryAcquireShared方法获取锁//获取成功(返回值大于等于0)则直接返回;if (tryAcquireShared(arg) < 0)//失败则调用doAcquireShared方法将当前线程封装为Node.SHARED类型的Node 结点后加入到AQS 同步队列的尾部,//然后"自旋"尝试获取同步状态,如果还是获取不到,那么最终使用park方法挂起自己。doAcquireShared(arg);
}

tryAcquireShared尝试获取共享锁

tryAcquireShared该方法是AQS的子类即我们自己实现的,用于尝试获取共享锁,一般来说就是对state的改变、或者重入锁的检查等等,不同的锁有自己相应的逻辑判断。

protected int tryAcquireShared(int arg)

返回int类型的值(比如返回剩余的state状态值->资源数量),一般的理解为:

如果返回值小于0,表示当前线程共享锁失败;

如果返回值大于0,表示当前线程共享锁成功,并且接下来其他线程尝试获取共享锁的行为很可能成功;

如果返回值等于0,表示当前线程共享锁成功,但是接下来其他线程尝试获取共享锁的行为会失败。

实际上在AQS的实际实现中,即使某时刻返回值等于0,接下来其他线程尝试获取共享锁的行为也可能会成功。即某线程获取锁并且返回值等于0之后,马上又有线程释放了锁,导致实际上可获取锁数量大于0,此时后继还是可以尝试获取锁的。

protected int tryAcquireShared(int arg) {throw new UnsupportedOperationException();
}

doAcquireShared自旋获取共享锁

首次调用tryAcquireShared方法获取锁失败之后,会调用doAcquireShared方法。类似于独占式获取锁acquire方法中的addWaiter和acquireQueued方法的组合版本!此方法用于将当前线程加入等待队列尾部休息,直到其他线程释放资源唤醒自己,自己成功拿到相应量的资源后才返回。

  • 大概步骤如下:

调用addWaiter方法,将当前线程封装为Node.SHARED模式的Node结点后加入到AQS 同步队列的尾部,即表示共享模式。

后面就是类似于acquireQueued方法的逻辑,结点自旋尝试获取共享锁。如果还是获取不到,那么最终使用park方法挂起自己等待被唤醒。

每个结点可以尝试获取锁的要求是前驱结点是头结点,那么它本身就是整个队列中的第二个结点,每个获得锁的结点都一定是成为过头结点。那么如果某第二个结点因为不满足条件没有获取到共享锁而被挂起,那么即使后续结点满足条件也一定不能获取到共享锁。

/*** 自旋尝试共享式获取锁,一段时间后可能会挂起* 和独占式获取的区别:* 1 以共享模式Node.SHARED添加结点* 2 获取到锁之后,修改当前的头结点,并将信息传播到后续的结点队列中** @param arg 参数*/
private void doAcquireShared(int arg) {/*1 addWaiter方法逻辑,和独占式获取的区别1 :以共享模式Node.SHARED添加结点*/final Node node = addWaiter(Node.SHARED);/*2 下面就是类似于acquireQueued方法的逻辑* 区别在于获取到锁之后acquireQueued调用setHead方法,这里调用setHeadAndPropagate方法*  *///当前线程获取锁失败的标志boolean failed = true;try {//当前线程的中断标志boolean interrupted = false;for (; ; ) {//获取前驱结点final Node p = node.predecessor();/*当前驱结点是头结点的时候就会以共享的方式去尝试获取锁*/if (p == head) {int r = tryAcquireShared(arg);/*返回值如果大于等于0,则表示获取到了锁*/if (r >= 0) {/*和独占式获取的区别2 :修改当前的头结点,根据传播状态判断是否要唤醒后继结点。*/setHeadAndPropagate(node, r);// 释放掉已经获取到锁的前驱结点p.next = null;/*检查设置中断标志*/if (interrupted)selfInterrupt();failed = false;return;}}/*判断是否应该挂起,以及挂起的方法,和acquireQueued方法的逻辑完全一致,不会响应中断*/if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);}
}

从源码可以看出,和独占式获取的主要区别为:

  1. addWaiter以共享模式Node.SHARED添加结点。
  2. 获取到锁之后,调用setHeadAndPropagate设置行head结点,然后根据传播状态判断是否要唤醒后继结点。
setHeadAndPropagat设置结点并传播信息

在结点线程获取共享锁成功之后会调用setHeadAndPropagat方法,相比于setHead方法,在设置head之后多执行了一步propagate操作:

  1. 和setHead方法一样设置新head结点信息
  2. 根据传播状态判断是否要唤醒后继结点。

这里说的传播其实说的是 propagate > 0 的情况,道理也很简单,当前线程获取同步状态成功了,还有剩余的同步状态可用于其他线程获取,那就要通知在等待队列的线程,让他们尝试获取剩余的同步状态。

/*** 入参,node: 当前节点* 入参,propagate:获取同步状态的结果值,即上面方法中的变量 r*/
private void setHeadAndPropagate(Node node, int propagate) {// 记录旧的头部节点,用于下面的checkNode h = head;//将当前节点设置为头节点,head指向自己setHead(node);// 通过 propagate 的值和 waitStatus 的值来判断,如果还有剩余量,继续唤醒下一个邻居线程if (propagate > 0 || h == null || h.waitStatus < 0) {Node s = node.next;// 如果后继节点为空或者后继节点为共享类型,则进行唤醒后继节点// 这里后继节点为空意思是只剩下当前头节点了if (s == null || s.isShared())// 唤醒操作doReleaseShared();}
}
doReleaseShared唤醒后继结点

doReleaseShared用于在共享模式下唤醒后继结点。

/*** 共享式获取锁的核心方法,尝试唤醒一个后继线程,被唤醒的线程会尝试获取共享锁,如果成功之后,则又会有可能调用setHeadAndPropagate,将唤醒传播下去。* 独占锁只有在一个线程释放所之后才会唤醒下一个线程,而共享锁在一个线程在获取到锁和释放掉锁锁之后,都可能会调用这个方法唤醒下一个线程* 因为在共享锁模式下,锁可以被多个线程所共同持有,既然当前线程已经拿到共享锁了,那么就可以直接通知后继结点来获取锁,而不必等待锁被释放的时候再通知。*/
private void doReleaseShared() {/*一个死循环,跳出循环的条件就是最下面的break*/for (; ; ) {//获取当前的head,每次循环读取最新的headNode h = head;//如果h不为null且h不为tail,表示队列至少有两个结点,那么尝试唤醒head后继结点线程if (h != null && h != tail) {int ws = h.waitStatus;//如果头结点的状态为SIGNAL,那么表示后继结点需要被唤醒if (ws == Node.SIGNAL) {//尝试CAS设置h的状态从Node.SIGNAL变成0//可能存在多线程操作,但是只会有一条成功if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))//失败的线程结束本次循环,继续下一次循环continue;            // loop to recheck cases//成功的那一条线程会调用unparkSuccessor方法唤醒head的一个没有取消的后继结点//对于一个head,只需要一条线程去唤醒该head的后继就行了。上面的CAS就是保证unparkSuccessor方法对于一个head只执行一次unparkSuccessor(h);}/** 如果h状态为0,那说明后继结点线程已经是唤醒状态了或者将会被唤醒,不需要该线程来唤醒* 那么尝试设置h状态从0变成PROPAGATE,如果失败则继续下一次循环,此时设置PROPAGATE状态能保证唤醒操作能够传播下去* 因为后继结点成为头结点时,在setHeadAndPropagate方法中能够读取到原head结点的PROPAGATE状态<0,从而让它可以尝试唤醒后继结点(如果存在)* */else if (ws == 0 &&!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))//失败的线程结束本次循环,继续下一次循环continue;                // loop on failed CAS}// 执行到这一步说明在上面的判断中队列可能只有一个结点,或者unparkSuccessor方法调用完毕,或h状态为PROPAGATE(不需要继续唤醒后继)// 再次检查h是否仍然是最新的head,如果不是的话需要再进行循环;如果是的话说明head没有变化,退出循环if (h == head)                   // loop if head changedbreak;}
}

reaseShared共享式释放锁

releaseShared(int)是共享模式下线程释放共享资源的入口,它会释放指定量的资源,如果成功释放且允许唤醒等待线程,它会唤醒等待队列里的其他线程来获取资源。

  • 大概步骤为:

调用tryReleaseShared尝试释放共享锁,这里必须实现为线程安全。

如果释放了锁,那么调用doReleaseShared方法唤醒后继结点,实现唤醒的传播。

对于支持共享式的同步组件(即多个线程同时访问),它们和独占式的主要区别就是tryReleaseShared方法必须确保锁的释放是线程安全的(因为既然是多个线程能够访问,那么释放的时候也会是多个线程的,就需要保证释放时候的线程安全)。

由于tryReleaseShared方法也是我们自己实现的,因此需要我们自己实现线程安全,所以常常采用CAS的方式来释放同步状态。

/*** 共享模式下释放锁的模版方法。* ,如果成功释放则会调用*/
public final boolean releaseShared(int arg) {//tryReleaseShared释放锁资源,该方法由子类自己实现if (tryReleaseShared(arg)) {//释放成功,必定调用doReleaseShared尝试唤醒后继结点doReleaseShared(); return true;}return false;
}

acquireSharedInterruptibly共享式可中断获取锁

acquireShared方法是不会响应中断的。但是AQS提供了另外一个acquireSharedInterruptibly模版方法,调用该方法的线程在等待获取锁时,如果当前线程被中断,会立刻返回,并抛出InterruptedException。

/*** 共享式可中断获取锁模版方法** @param arg 参数* @throws InterruptedException 线程处于中断状态,抛出此异常*/
public final void acquireSharedInterruptibly(int arg)throws InterruptedException {//最开始就检查一次,如果当前线程是被中断状态,则清除已中断状态,并抛出异常if (Thread.interrupted())throw new InterruptedException();//尝试获取锁if (tryAcquireShared(arg) < 0)//获取不到就执行doAcquireSharedInterruptibly方法doAcquireSharedInterruptibly(arg);
}
doAcquireSharedInterruptibly共享式可中断获取锁

该方法内部操作和doAcquireShared差不多,都是自旋获取共享锁,有些许区别,就是在后续挂起的线程因为线程被中断而返回时的处理方式不一样。

共享式不可中断获取锁仅仅是记录该状态,interrupted = true,紧接着又继续循环获取锁;共享式可中断获取锁则直接抛出异常,因此会直接跳出循环去执行finally代码块。

/*** 以共享可中断模式获取。** @param arg 参数*/
private void doAcquireSharedInterruptibly(int arg)throws InterruptedException {/*内部操作和doAcquireShared差不多,都是自旋获取共享锁,有些许区别*/final Node node = addWaiter(Node.SHARED);boolean failed = true;try {for (; ; ) {final Node p = node.predecessor();if (p == head) {int r = tryAcquireShared(arg);if (r >= 0) {setHeadAndPropagate(node, r);p.next = null; // help GCfailed = false;return;}}if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())/** 这里就是区别所在,共享不可中断式方法doAcquireShared中* 如果线程被中断,此处仅仅会记录该状态,interrupted = true,紧接着又继续循环获取锁** 但是在该共享可中断式的锁获取方法中* 如果线程被中断,此处直接抛出异常,因此会直接跳出循环去执行finally代码块* */throw new InterruptedException();}}/*获取到锁或者抛出异常都会执行finally代码块*/finally {/*如果获取锁失败。那么是发生异常的情况,可能就是线程被中断了,执行cancelAcquire方法取消该结点对锁的请求,该线程结束*/if (failed)cancelAcquire(node);}
}

tryAcquireSharedNanos共享式超时获取锁

共享式超时获取锁tryAcquireSharedNanos模版方法可以被视作共享式响应中断获取锁acquireSharedInterruptibly方法的“增强版”,支持中断,支持超时时间!

/*** 共享式超时获取锁,支持中断** @param arg          参数* @param nanosTimeout 超时时间,纳秒* @return 是否获取锁成功* @throws InterruptedException 如果被中断,则抛出InterruptedException异常*/public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)throws InterruptedException {//最开始就检查一次,如果当前线程是被中断状态,则清除已中断状态,并抛出异常if (Thread.interrupted())throw new InterruptedException();//下面是一个||运算进行短路连接的代码//tryAcquireShared尝试获取锁,获取到了直接返回true//获取不到(左边表达式为false) 就执行doAcquireSharedNanos方法return tryAcquireShared(arg) >= 0 ||doAcquireSharedNanos(arg, nanosTimeout);
}
doAcquireSharedNanos共享式超时获取锁

doAcquireSharedNanos(int arg,long nanosTimeout)方法在支持响应中断的基础上,增加了超时获取的特性。

该方法在自旋过程中,当结点的前驱结点为头结点时尝试获取锁,如果获取成功则从该方法返回,这个过程和共享式式同步获取的过程类似,但是在锁获取失败的处理上有所不同。

如果当前线程获取锁失败,则判断是否超时(nanosTimeout小于等于0表示已经超时),如果没有超时,重新计算超时间隔nanosTimeout,然后使当前线程等待nanosTimeout纳秒(当已到设置的超时时间,该线程会从LockSupport.parkNanos(Objectblocker,long nanos)方法返回)。

如果nanosTimeout小于等于spinForTimeoutThreshold(1000纳秒)时,将不会使该线程进行超时等待,而是进入快速的自旋过程。原因在于,非常短的超时等待无法做到十分精确,如果这时再进行超时等待,相反会让nanosTimeout的超时从整体上表现得反而不精确。

因此,在超时非常短的场景下,AQS会进入无条件的快速自旋而不是挂起线程。

static final long spinForTimeoutThreshold = 1000L;/*** 以共享超时模式获取。** @param arg          参数* @param nanosTimeout 剩余超时时间,纳秒* @return true 成功 ;false 失败* @throws InterruptedException 如果被中断,则抛出InterruptedException异常*/
private boolean doAcquireSharedNanos(int arg, long nanosTimeout)throws InterruptedException {//剩余超时时间小于等于0的,直接返回if (nanosTimeout <= 0L)return false;//能够等待获取的最后纳秒时间final long deadline = System.nanoTime() + nanosTimeout;//同样调用addWaiter将当前线程构造成结点加入到同步队列尾部final Node node = addWaiter(Node.SHARED);boolean failed = true;try {/*和共享式式不可中断方法doAcquireShared一样,自旋获取锁*/for (; ; ) {final Node p = node.predecessor();if (p == head) {int r = tryAcquireShared(arg);if (r >= 0) {setHeadAndPropagate(node, r);p.next = null; // help GCfailed = false;return true;}}/*这里就是区别所在*///如果新的剩余超时时间小于0,则退出循环,返回false,表示没获取到锁nanosTimeout = deadline - System.nanoTime();if (nanosTimeout <= 0L)return false;//如果需要挂起 并且 剩余nanosTimeout大于spinForTimeoutThreshold,即大于1000纳秒if (shouldParkAfterFailedAcquire(p, node) &&nanosTimeout > spinForTimeoutThreshold)//那么调用LockSupport.parkNanos方法将当前线程挂起nanosTimeoutLockSupport.parkNanos(this, nanosTimeout);//如果线程被中断了,那么直接抛出异常if (Thread.interrupted())throw new InterruptedException();}}/*获取到锁、超时时间到了、抛出异常都会执行finally代码块*/finally {/*如果获取锁失败。可能就是线程被中断了,那么执行cancelAcquire方法取消该结点对锁的请求,该线程结束* 或者是超时时间到了,那么执行cancelAcquire方法取消该结点对锁的请求,将返回false* */if (failed)cancelAcquire(node);}
}

共享式获取/释放锁总结

我们可以调用acquireShared 模版方法来获取不可中断的共享锁,可以调用acquireSharedInterruptibly模版方法来可中断的获取共享锁,可以调用tryAcquireSharedNanos模版方法来可中断可超时的获取共享锁,在此之前需要重写tryAcquireShared方法;还可以调用releaseShared模版方法来释放共享锁,在此之前需要重写tryReleaseShared方法。

对于共享锁来说,由于锁是可以多个线程同时获取的。那么如果一个线程成功获取了共享锁,那么其他等待在这个共享锁上的线程就也可以尝试去获取锁,并且极有可能获取成功。因此在一个结点线程释放共享锁成功时,必定调用doReleaseShared尝试唤醒后继结点,而在一个结点线程获取共享锁成功时,也可能会调用doReleaseShared尝试唤醒后继结点。

基于共享式实现的组件有CountDownLatch、Semaphore、ReentrantReadWriteLock等。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/826173.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

高级语言程序设计第5次作业

高级语言程序设计课程第四次个人作业 这个作业属于哪个课程:https://edu.cnblogs.com/campus/fzu/2024C/ 这个作业要求在哪里:https://edu.cnblogs.com/campus/fzu/2024C/homework/13298 学号:102300303 姓名:梁佳 第八章第1题第2题第3题第4题第5题第6题第7题第8题第九章第…

利用前馈神经网络(FNN)进行气温预测任务

一、前馈神经网络 前馈神经网络(Feedforward Neural Networks, FNN)是人工神经网络中的一种,它的信息流动是单向的,从输入层到隐藏层,再到输入层,没有反向的连接。其中,隐藏层可以有多个,用于处理输入层的数据,且每一个隐藏层通常配合一个非线性的激活函数来进行训练。…

CoSeR桥接图像和语言以实现认知超分辨率

CoSeR桥接图像和语言以实现认知超分辨率 6.10.1 CoSeR桥接图像和语言以实现认知超分辨率概述 现有的超分辨率(SR)模型主要侧重于恢复局部纹理细节,往往忽略了场景中的全局语义信息。这种疏忽可能会导致在恢复过程中遗漏关键的语义细节或引入不准确的纹理。 引入了认知超分辨…

综合、诊断和优化:迈向精细视觉语言理解

综合、诊断和优化:迈向精细视觉语言理解 6.8.1 综合、诊断和优化:迈向精细视觉语言理解概述视觉语言模型(VLM)在各种下游任务中表现出了卓越的性能。然而,理解细粒度的视觉语言概念,如属性和对象间关系,仍然是一个重大的挑战。虽然有几个基准旨在以更精细的粒度评估VLM,…

Nuxt.js 应用中的 nitro:init 事件钩子详解

title: Nuxt.js 应用中的 nitro:init 事件钩子详解 date: 2024/11/3 updated: 2024/11/3 author: cmdragon excerpt: nitro:init 是 Nuxt 3 中的一个生命周期钩子,在 Nitro 初始化完成后被调用。这个钩子允许开发者注册 Nitro 钩子,并直接与 Nitro 进行交互。这种灵活性使…

【人脸伪造检测后门攻击】Imperceptible Face Forgery Attack via Adversarial Semantic Mask

原文Github地址:https://github.com/clawerO-O/ASMA一、研究动机 ​ 目前的后门攻击模型是基于数字像素上的操作,例如增加噪声,从而使得深度模型在推理阶段表现为不正常,但这种attack隐蔽性很差,可以被人眼所观察到。因为这些模型是在整个面部区域增加对抗性扰动,增加了许…

MTR: 网络排查神器 / 网络诊断工具介绍

原创 晓致知 电脑知识MTR(My Traceroute)是一款功能全面且高效的网络诊断工具,它巧妙地将traceroute和ping的功能融为一体。通过MTR,用户可以实时追踪数据包在网络中的传输路径,清晰地看到数据包从源地址到目标地址所经过的所有节点。同时,MTR还能提供详细的网络性能指标…

随想

日复又一日,重复再重复。

相册

国风版,相册 好看的你

锋利的在线诊断工具——Arthas

导航前言 火线告警,CPU飚了 服务重启,迅速救火 黑盒:无尽的猜测和不安 Arthas:锋利的Java诊断工具 在线追踪Cpu占比高的代码段 代码重构,星夜上线,稳了 结语 参考肮脏的代码必须重构,但漂亮的代码也需要很多重构。前言 有些代码在当初编写的时候是非常稳健的,但是随着数…

制作一个ai丛雨(附Python代码)

绫,再一次,再一次创造一个有你的世界😭开一个随笔记录一下我的第一版ai老婆,目前只有普通对话和切换背景的功能(后面可能会加一个选人物功能)先放一个效果图(看起来还行)代码和注意事项都放在了下面,应该没什么大问题,复制粘贴导包就能用了注意事项: 1、代码推荐使…

东方娱乐周刊

学科领域: 人文社科-教育学、文学、艺术、体育、人文社科:其他@目录一、征稿简介二、重要信息三、服务简述四、投稿须知 一、征稿简介二、重要信息期刊官网:https://ais.cn/u/3eEJNv三、服务简述 学科领域: 人文社科-教育学、文学、艺术、体育、人文社科:其他 四、投稿须知…