2.AQS的CLH同步队列

161 阅读6分钟

AQS的CLH同步队列

同步队列是双向。

与CLH同步锁异同

CLH队列锁提过,AQS里面的CLH队列是CLH同步锁的一种变形, 其主要从两方面进行了改造:节点的结构与节点等待机制。

1.结构上引入了头结点和尾节点。

他们分别指向队列的头和尾,尝试获取锁、入队列、释放锁等实现都与头尾节点相关,并且每个节点都引入前驱节点和后后续节点的引用。

2.等待机制上由原来的自旋改成阻塞唤醒。

知道其结构了,我们再看看他的实现。在线程获取锁时会调用AQS的acquire()方法,该方法第一次尝试获取锁如果失败,会将该线程加入到CLH队列中:

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

addWaiter:添加到等待队列

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

CLH队列节点Node源码

static final class Node {
        /** Marker to indicate a node is waiting in shared mode */
        static final Node SHARED = new Node();
        /** Marker to indicate a node is waiting in exclusive mode */
        static final Node EXCLUSIVE = null;
    /***
        NEW(0):新结点入队时的默认状态
        
        CANCELLED(1):表示当前结点已取消调度。
        当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
        
        SIGNAL(-1):
        表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL即-1。
        
        CONDITION(-2):
        表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后
        CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
        
        PROPAGATE(-3):
        共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
    ***/
        static final int CANCELLED =  1;
        static final int SIGNAL    = -1;
        static final int CONDITION = -2;
        static final int PROPAGATE = -3;
        volatile int waitStatus;
 
        volatile Node prev;
 
        volatile Node next;
 
        volatile Thread thread;
 
        Node nextWaiter;
​
        /**
         * Returns true if node is waiting in shared mode.
         */
        final boolean isShared() {
            return nextWaiter == SHARED;
        }
 
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }
​
        Node() {    
        }
​
        Node(Thread thread, Node mode) {
            this.nextWaiter = mode;
            this.thread = thread;
        }
​
        Node(Thread thread, int waitStatus) {
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

入队源码

private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            //第一轮:如果队列中还没有元素 tail 为 null
            if (t == null) {
                // 第一轮:使用cas 将 head 从 null 变为 new Node()
                // 假设新创建的这个空节点A 
                // head节点的属性:thread=null
                if (
                    compareAndSetHead(new Node()))
                    //第一轮:将head赋值为tail
                    //第一轮:此时head和tail是同一个节点 是新创建的节点A
                    //第一轮:进入第二轮循环
                    tail = head;
            } else {
                // 第二轮:将原来的尾结点tail即A赋值给新的node即B的prev
                // 即新节点B的prev指向头结点A  
                node.prev = t;
                // 第二轮:将新节点B作为尾节点
                if (compareAndSetTail(t, node)) {
                    // 第二轮:将头结点A的nex指向B
                    t.next = node;
                    return t;
                }
            }
        }
    }
//第一次循环
//head = yummy节点
//tail = yummy节点//第二次循环
//新节点的前置节点指向head
//尾节点被设置为新节点
//头结点的后置节点指向新节点
//此时
//head ← 新节点
//tail = 新节点
//head → 新节点
//因为尾节点就是新节点
//所以就是
//head→tail(新节点)
//head←tail(新节点)

head是yummy节点,head后面的节点都是正常节点。

入队代码测试

package AQS;
​
​
import static AQS.AqsNode.Node.enter;
​
public class AqsNode {
​
    @Override
    public String toString() {
        return "AqsNode{" +
                "head=" + head.name +
                ", tail=" + tail.name +
                ", state=" + state +
                '}';
    }
​
    private static transient volatile Node head;
    private static transient volatile Node tail;
    private volatile int state;
​
    private static Node enq(final Node node) {
        for (; ; ) {
            System.out.println(head);
            Node t = tail;
            if (t == null) {  
                Node node1 = new Node();
                node1.name = "YUMMY";
                if (compareAndSetHead(node1))
                    tail = head;
            } else {
                //关键点:这里要把t当做头结点来看
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
            System.out.println(head);
        }
    }
​
    private static boolean compareAndSetTail(Node t, Node node) {
         tail = node;
        return true;
    }
​
    private static boolean compareAndSetHead(Node node) {
        head = node;
        return true;
    }
​
    static final class Node {
​
        public String name;
        volatile int waitStatus;
        volatile Node prev;
        volatile Node next;
        volatile Thread thread;
        Node nextWaiter;
​
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }
​
        Node() {
        }
​
        @Override
        public String toString() {
            return "Node{" +
                    "name='" + name + ''' +
                    '}';
        }
​
        public static void enter() {
            AqsNode.Node node1 = new AqsNode.Node();
            node1.name = "node1";
            AqsNode.Node node2 = new AqsNode.Node();
            node2.name = "node2";
            enq(node1);
            enq(node2);
        }
​
    }
​
    public static void main(String[] args) {
        enter();
    }
}
​

入队代码图解

第一个节点入队前:头节点和尾节点没有连接,且头尾节点的前置节点和后置节点为空。 image.png

第一个节点入队后:头节点yummy的后置节点指向尾节点node1,尾节点的前置节点指向头结点yummy。头结点的前置节点和尾节点的后置节点为空。

image.png

第二个节点入队后:头结点的前置节点和尾节点的后置节点为空。

image.png

出队代码

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                //线程2从死循环中醒来 发现自己的前置节点是头结点 获取锁成功
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

假设t1加锁成功 t2在同步队列中。

t1释放锁的时候,t2会被unpark,然后执行死循环发现t2的前置节点是头结点,然后t2获取锁成功,进入判断:

/***
执行出队操作。
为什么说是执行出队操作呢?
因为本来节点是 Head → t2
现在把Head移除了  t2作为新的Head
***/
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
    //将当前节点设置为头结点
    setHead(node);
    //将原来头结点的next指针指向空
    p.next = null;//help GC
    failed = false;
    // 还是需要获得锁后, 才能返回打断状态
    return interrupted;
}
private void setHead(Node node) {
        head = node;
        node.thread = null;
        node.prev = null;
    }

Node状态:waitStatus

Node结点是对每一个等待获取资源的线程的封装,其包含了需要同步的线程本身及其等待状态,如是否被阻塞、是否等待唤醒、是否已经被取消等。

变量waitStatus则表示当前Node结点的等待状态,共有5种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE、0。

  • CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。

    因为超时或者中断,节点会被设置为取消状态,被取消的节点时不会参与到竞争中的,他会一直保持取消状态不会转变为其他状态。

  • SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL即-1。

  • CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。

  • PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。

  • 0:新结点入队时的默认状态。

新建节点一般都为0,负值表示结点处于有效等待状态,而正值表示结点已被取消。

所以源码中很多地方用>0、<0来判断结点的状态是否正常,0是新结点入队时的默认状态。

已取消

线程因为超时或者中断涉及到取消的操作,如果某个节点被取消了,那个该节点就不会参与到锁竞争当中,它会等待GC回收。

取消的主要过程是将取消状态的节点移除掉,移除的过程还是比较简单的。先将其状态设置为CANCELLED,然后将其前驱节点指向其后继节点。

假设当前队列结构如下:A------>B----->C

现在要取消B节点 即将B的前置节点A指向B的后置节点C

A------>C

当然这个过程仍然会是一个CAS操作:

node.waitStatus = Node.CANCELLED;
Node pred = node.prev;
Node predNext = pred.next;
Node next = node.next;

Node指针

volatile Node prev;//同步队列中节点的前置节点
volatile Node next;//同步队列中节点的后置节点

image.png

Node nextWaiter;//等待队列中节点的后置节点

同步队列是双向,条件等待队列是单向。