java多线程-AbstractQueuedSynchronizer

大纲:

  1. AbstractQueuedSynchronizer简介
  2. aqs基本结构
  3. lock流程
  4. unlock流程
  5. signal、await流程
  6. Semaphore.acquire

 

一、AbstractQueuedSynchronizer简介

AbstractQueuedSynchronizer(抽象队列同步器)简介:AbstractQueuedSynchronizer以下简称(aqs)是一个基于先进先出队列,用于构建锁及其他同步装置的基础框架。子类通过继承aqs实现同步的需求。

 

二、aqs基本结构

aqs的数据结构是一个双向链表,aqs的主要成员变量是头尾节点,还有一个state(线程的同步状态)

    //头尾节点
    private volatile Node head;
    private volatile Node tail;
    //同步状态
    private volatile int state;

 

节点是一个aqs类中的嵌套类,看下节点的结构:

static final class Node {
        //节点类型
        static final Node EXCLUSIVE = null;
        static final Node SHARED = new Node();

        static final int CANCELLED = 1;
        static final int SIGNAL = -1;//标识这个结点有义务唤醒后续结点
        static final int CONDITION = -2;//放入condition中await的状态
        static final int PROPAGATE = -3;

//前后节点 volatile Node prev; volatile Node next; //节点中存储的线程 volatile Thread thread; Node nextWaite; //等待状态CANCELLED/SIGNAL/CONDITION/PROPAGATE 结点刚被new出来是0 volatile int waitStatus; // Used to establish initial head or SHARED marker Node() {} // Used by Condition public Node(Thread thread, int waitStatus) { this.thread = thread; this.waitStatus = waitStatus; } // Used by addWaiter public Node(Thread thread, Node nextWaite) { this.thread = thread; this.nextWaite = nextWaite; } }

没有获取到资源的线程被包装成为一个节点,每个节点有一个等待状态。

 

三、lock流程

继承关系:NonfairSync->Sync->AbstractQueuedSynchronizer

lock步骤:

1.cas将state变成1,如果成功,把exclusiveOwnerThread设置成当前线程,如果失败调用acquire方法

tryAcquire:重试一下cas把state置为1

addWaiter:把线程包装成node放到队尾,第一次添加node结点时,整个链表为空,需要添加一个dummy结点,然后再把当前结点放到队尾,之后再添加的时候直接添加到队尾

acquireQueued:这个方法首先判断如果当前节点的前驱节点为头结点可以再尝试获取一次锁,如果成功则将该节点设置为头结点。否则进入阻塞阶段:找到一个waitstatus<=0的前驱结点,并把这个前驱结点的waitStaus设置为-1,然后当前线程挂起

lock:

static final class NonfairSync extends Sync {
        //非公平锁的lock方法
        final void lock() {
            //cas修改state状态成功则表示获取锁成功
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                //acquire是aqs的方法
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            //调用Sync的nonfairTryAcquire方法,获取锁成功返回true失败false
            return nonfairTryAcquire(acquires);
        }
    }

tryAcquire:

  //aqs的acquire
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
                acquireQueued(addWaiter(Node.EXCLUSIVE), arg))  //Node.EXCLUSIVE是独占模式
            selfInterrupt();
    }

addWaiter:

private Node addWaiter(Node mode) {
        //将当前线程封装成node
        Node node = new Node(Thread.currentThread(), mode);
        Node pred = tail;
        //队列里原来有值,将node插到队尾
        if (pred != null) {
            node.prev = tail;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        //如果队列空,或者cas插入队尾失败执行enq方法
        enq(node);
        return node;
    }

    private Node enq(final Node node) {
        for (; ; ) {
            //获取尾节点
            Node t = tail;
            if (t == null) {//初始化,队列为空
                //初始化一个空的dummy结点,帮助唤醒它的后继结点
                if (compareAndSetHead(new Node()))
                    tail = head;
            }
            //队列里原来有值,将node插到队尾
            else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

这里for循环就是个自旋,由于是并发插入队尾,乐观锁操作cas就有可能失败,所以不断尝试插入队尾直到成功。

 

acquireQueued:

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true; //获得锁是否失败
        try {
            boolean interrupted = false;//获取锁过程中被interrupt
            for (;;) {
                //获取当前节点的前驱节点
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {//当前驱节点是头节点且获取锁成功
                    //将当前节点设为头结点(头结点表示持有锁的节点)
                    setHead(node);
                    p.next = null; // help GC,原来的头结点(要么是dummy结点,要么是已经释放锁的结点)已经没用了
                    failed = false;
                    //这里线程还没被挂起,无法被interrupt
                    return interrupted;
                }
                //找到有效前驱节点,设置前驱节点的waitstatus为Node.SIGNAL,之后park挂起线程,等带着被unpark()或interrupt()
                if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

 

shouldParkAfterFailedAcquire:

找到一个不是取消状态的前驱结点,把它的waitStatus置为-1

这里首先判断前驱节点的waitState,如果是Node.SIGNAL表示线程可以被挂起了,返回成功;如果>0则表示结点是取消状态,继续向前遍历找到一个<=0的前驱结点把它的waitState改成Node.SIGNAL,当waitState为Node.SIGNAL时说明这个结点要唤醒后继结点。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        //当前节点的前驱的waitStatus
        int ws = pred.waitStatus;
        //当前驱的waitStatus为Node.SIGNAL表示该线程可以阻塞返回true
        if (ws == Node.SIGNAL) {
            return true;
        }
        //当前驱的waitStatus为-1表示前驱节点状态为Node.CANCELLED,跳过前驱,直到找到状态不是Node.CANCELLED的节点,将该节点作为当前节点的前驱
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        }
        //将前驱节点的waitStatus改为Node.SIGNAL
        else {
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

 

 parkAndCheckInterrupt

线程被挂起,等待其他线程唤醒后,检查是否被打断

    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);//调用本地方法将线程挂起
        return Thread.interrupted();//被唤醒时检查是否被打断
    }

 当parkAndCheckInterrupt返回true则调用Thread.currentThread().interrupt()。

 

 

四、unlock流程

unlock步骤:

  1. state--,当state=0时,setExclusiveOwnerThread=null。
  2. 唤醒头结点的下一个有效节点中的线程。
  3. 被唤醒的线程在acquireQueued中继续争抢锁。

release

    public final boolean release(int arg) {
        if (tryRelease(arg)) {//是否成功释放资源
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);//唤醒队列中的线程
            return true;
        }
        return false;
    }

tryRelease

    protected final boolean tryRelease(int releases) {
        int c = getState() - releases;
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        //c>0则有锁重入情况,c==0将释放锁
        if (c == 0) {
            free = true;
            setExclusiveOwnerThread(null);//清除占有锁的线程
        }
        setState(c);
        return free;
    }

unparkSuccessor

    private void unparkSuccessor(Node 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)//waitStatus为0 和负数都是有效节点
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);//唤醒这个节点的线程
    }

 

五、signal、await流程

每个lock对象都可以建立多个条件变量

await大致流程:持有锁的线程可以调用await将放入条件队列,条件队列和aqs中队列一样也是一个双向链表,进入条件队列的node结点会把自生状态改成-2并添加到队尾,然后将持有锁的state置0,断开原有aqs链表并唤醒head结点的下一个有效结点中的线程。然后当前线程挂起。

signal大致流程:持有锁的线程可以唤醒之前await进入条件队列的线程,将队头结点从队列移除,重新加入aqs等待队列,当前结点的waitStatus置0并把前置结点waitStatus置-1。

 

六、Semaphore.acquire方法

Lock的lock方法是调用aqs的acquire方法,该方法是独占模式获取资源的,而Semaphore的acquire调用的是aqs的acquireShared方法,该方法是共享模式获取资源。

    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0) //tryAcquireShared方法子类各自实现,Semaphore.Sync中实现如下
            doAcquireShared(arg);
    }

Semaphore.Sync实现的tryAcquireShared

    protected int tryAcquireShared(int acquires //需要获取的资源数) {
        for (;;) {
            if (hasQueuedPredecessors())
                return -1;
            int available = getState(); //剩余的资源数
            int remaining = available - acquires; //remaining获取资源后剩余的资源数
            if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                return remaining;
    }
    

aqs中doAcquireShared

    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) {
                        setHeadAndPropagate(node, r);//将当前节点设置为头结点,唤醒下一个有效节点
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

和独占模式的流程如出一辙,入队、挂起 的流程一样,依然是唤醒头结点的下一个有效节点。这里需要注意即使是共享模式,唤醒依然是按照入队顺序来的,但资源数不够第二个节点的获取数时也不会唤醒后续节点(即使资源数满足后续节点的获取数)。

唯一不同的是在资源还有剩余的情况下,会在设置头节点的同时继续唤醒当前节点下一个有效节点

   private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        setHead(node);
        //propagate > 0(如果剩余的资源数>0的话,尝试唤醒下一个有效节点)
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
                (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }

小结:

  1. 两种模式获取资源方式差不多,共享模式多了如果资源数>0继续唤醒节点的操作。
  2. 两种模式释放资源的方式也基本一样,这里不再赘述。
posted @ 2019-05-22 18:58  扶不起的刘阿斗  阅读(623)  评论(0编辑  收藏  举报