AQS exclusive模式整理

xiaoxiao2022-06-11  34

等待队列节点类整体描述: 等待队列是一个自旋锁(CLH)的变体。在这里我们用它来进行同步器的阻塞。在每个节点中的一个status域追踪者一个线程是否需要阻塞。当一个线程的前任节点被释放后,它就会被唤醒。队列中每个节点被当作一个特定的监视器来持有一个等待线程。status域并不控制线程是否被加锁。当一个线程第一次进入等待队列中,它会尝试获取锁。但是作为第一个节点并不一定保证获取锁成功,它只是得到了竞争锁的权力,所以当前已释放的竞争线程(如要重新获锁)需重新等待。 等待队列是一个双向链表结构。每个节点的prev域指向它的前任,并且prev的主要作用就是用来处理节点的取消(cancellation)。如果一个节点已经被取消,它的后继节点的prev应该链上一个非取消的节点。 我们使用next域来实现阻塞技术。每个线程的id被保存在线程所属的节点中,所以一个节点的后继也就示意着遍历下一个节点来决定它是哪个线程并且唤醒它。判断后继的过程必须要避免新线程将当前节点设置为它们的前任这个过程的发生,因为会产生竞争。

CLH队列的基本操作:尾插和头出 尾插:简单的原子操作,尾插就行了 头出:需要判断当前结点的前任,目地是为了处理可能的由于过时和中断造成的取消 CLH队列需要一个虚设的头结点来使用,但我们不会在构造时创建它们因为如果没有线程竞争这将会造成无谓的浪费。所以在第一次线程竞争的时候这个节点才会被构造。

内容 1、节点Node类 2、Node节点以外的AQS内部属性和方法 3、非中断独享模式下的线程必须先调用aquire方法来进行资源的获取 4、独享模式下释放资源掉用的是release方法

1、节点Node类

产生竞争时每个线程都会被封装在这个类中,也就是将线程包装成节点,节点中有着这样一些属性来帮我们标识线程的阻塞、唤醒和取消。

int waitStatus

它可以有如下取值: 1:表明后继结点已经或者将会被阻塞(通过park方法),并且当前结点在释放或取消时需要唤醒它 0:初始状态 -1:表明线程由于过时或中断已被取消

官方对waitStatus 的解释:在每个节点中waitStatus 域追踪者一个线程是否需要阻塞。当一个线程的前任节点被释放后,它就会被唤醒。waitStatus 域并不控制线程是否被加锁。 这里要注意:waitStatus域并不控制线程是否被加锁 ,它只是标识着每个节点所处的状态(阻塞、释放和取消等)。

static final Node EXCLUSIVE = null;

这是一个空Node类型的指针,代表了节点的EXCLUSIVE 模式

Node prev,next :

代表每个节点的前任和后继

Thread thread :

当前线程

final Node predecessor() throws NullPointerException { Node p = prev; if (p == null) throw new NullPointerException(); else return p; }

上述代码用来返回一个节点的前任,若存在前任,则返回,若无,返回NullPointerException 还有三个构造器,就不说了

2、Node节点以外的AQS内部属性和方法

Node head,tail:

链表的头、尾节点

private volatile int state:

同步状态,由AQS的子类来定义来改变这个状态值的protected方法,并且定义这个状态值的意义,用来获取或释放对象。

protected final int getState() { return state; } protected final void setState(int newState) { state = newState; } protected final boolean compareAndSetState(int expect, int update) { return unsafe.compareAndSwapInt(this, stateOffset, expect, update); }

上面三个方法分别用来获取、设置和替换state值,一般由AQS的子类来调用

3、非中断独享模式下的线程必须先调用aquire方法来进行资源的获取

调用链: 1、acquire调用tryAcquire,成功则获取资源 2、tryAcquire失败,调用addWaiter将当前节点加入等待队列 3、tryAcquire调用acquireQueued,在等待队列中尝试获得资源,若成功,则退出for循环 4、若3过程失败,调用shouldParkAfterFailedAcquire判断当前线程是否应该被阻塞,若是则进行阻塞并停止for循环;不是,继续for循环,即重新开始第3步

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

1、首先调用tryAcquire方法,若其返回true则证明获取资源成功,于是&&左边变为false,直接跳出if判断,并成功获取资源 2、若左边为tryAcquire返回false证明失败,调用addWaiter方法将当前线程加入等待队列,并将当前线程标记为EXCLUSIVE 3、调用acquireQueued方法一直尝试获取资源,直到成功返回 4、若acquireQueued返回true则证明线程被中断过了,因为acquireQueued并不处理中断,只是对中断做标记,所以最后会调用selfInterrupt来对当前线程做自我中断;返回false证明无中断

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

AQS中的tryAcquire只会抛出异常,就像官方文档所说的,AQS只提供操作模板,它的实现还得子类来完成,所以acquire方法调用的一般是子类重写的tryAcquire方法,也就是尝试获锁的过程在子类中得到实现

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; }

如等待队列中有节点,就直接插入当前节点;若没有,就调用enq方法将此节点插入

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; } } } }

尾插,一个自旋操作,队列里有节点就直接插入;若没有就创建一个新的虚设的头结点,并在下一个循环中将节点插入到头节点后,然后return当前节点,跳出循环

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)) { setHead(node); p.next = null; failed = false; return interrupted; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }

获取在独享非中断模式下已经存在于队列中的节点 1、标记中断位为false 2、获取当前节点的前任节点,若前任是头节点并且调用tryAcquire方法成功获取,则将当前节点设为头节点,并释放前任,然后跳出循环,返回中断标记 3、若前任不是头节点或调用tryAcquire方法没有获取到资源,则进入shouldParkAfterFailedAcquire方法进行判断,若返回true则证明当前节点应该阻塞,然后调用parkAndCheckInterrupt方法对其阻塞并进行中断的核查,停止尝试获取资源的自旋操作;若返回false则证明不需要阻塞,再次进入自旋操作中尝试获取资源。 4、failed的作用是标记是否成功获取资源。若没有成功获取资源但却跳出了自旋操作,会在finally中进行判断,然后取消获取资源的操作。

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

将当前节点设置为队列的头节点,并将节点的thread和prev置空

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; if (ws == Node.SIGNAL) return true; if (ws > 0) { do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }

1、前任节点状态已为-1,表明后继结点已经或者将会被阻塞,返回true 2、当前线程的前任被取消,则从前任开始往前搜,直到一个waitStatus不大于0,即非取消的节点,然后将当前节点的prev设置为它,返回false 3、若前任waitStatus为0或-3,表明我们需要将后继其阻塞,但还未执行阻塞,所以利用原子操作将前任的waitStatus设为-1,返回false

static void selfInterrupt() { Thread.currentThread().interrupt(); }

调用底层的interrupt方法对当前线程进行阻塞

private void cancelAcquire(Node node) { if (node == null) return; node.thread = null; Node pred = node.prev; while (pred.waitStatus > 0) node.prev = pred = pred.prev; Node predNext = pred.next; node.waitStatus = Node.CANCELLED; if (node == tail && compareAndSetTail(node, pred)) { compareAndSetNext(pred, predNext, null); } else { int ws; if (pred != head && ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && pred.thread != null) { Node next = node.next; if (next != null && next.waitStatus <= 0) compareAndSetNext(pred, predNext, next); } else { unparkSuccessor(node); } node.next = node; } }

取消操作中包括标记和删除操作 首先是标记操作: 1、如果节点不存在则忽略它 2、将当前节点的线程置空 3、删除当前节点之前所有waitStatus大于0(已取消)的节点 4、将当前节点的waitStatus置为1(即取消当前节点) 删除操作: 1、若当前节点为tail则直接删除,并将其前任设为tail 2、当前节点不为tail,并且前任节点不为头节点并且前任的后继需要被唤醒(前任的waitStatus为-1),则将前任的next指向当前节点的后继 3、否则唤醒当点节点的的后继 最后是一个打结操作:node.next = node来帮助GC 中断模式下的方法与非中断方法大同小异,这里就不具体分析了

4、独享模式下释放资源掉用的是release方法

调用链: 1、release调用tryRelease方法,若tryRelease成功则成功释放资源,并且判断是否需要唤醒后继,使用unparkSuccessor来唤醒 2、tryRelease方法失败证明释放资源失败

public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }

1、调用tryRelease方法,若返回true证明释放资源成功,然后判断若等待队列中有节点并且队列中第一个节点的waitStatus 不等于0,则使用unparkSuccessor方法来唤醒第二个节点 2、若tryRelease方法返回false证明获取资源失败

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

释放资源,和tryAquire一样要由AQS子类来实现

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; 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、首先若当前节点的waitStatus为-1,也就表明当前节点的后继需要唤醒,于是就利用原子操作将其waitStatus置为0 2、如果当前节点的后继为空,则跳出该方法 3、否则,从队列的最后一个节点一直往前,找到离当前节点最近的非取消节点,然后调用LockSupport的unpark方法将其阻塞

/*判断当前节点是否是队列中的第一个节点,是就返回false,不是则返回true*/ public final boolean hasQueuedPredecessors() { /*1、若当前队列为空,则h==t,返回false 2、若当前队列只有一个节点,则返回false 3、否则若当前线程所属节点不为第二个节点,返回true;若等于,返回false*/ Node t = tail; Node h = head; Node s; return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); }
转载请注明原文地址: https://www.6miu.com/read-4930544.html

最新回复(0)