Java多线程-五

前言

Object管理monitor的方法,功能比较有限,自JDK1.5后,出现了Condition,可以实现更多的功能。同时Java里的锁,最基本的实现都用到了同步队列,在JDK里这个关键的类是AbstractQueuedSynchronizer。此篇文章主要讲述Condition框架以及AbstractQueuedSynchronizer(AQS)

Condition

Condition是用于实现等待/通知的接口,比Object类的wait / notify 具备更丰富的功能。Condition必须与锁配合使用,一个Condition与一个Lock绑定,但一个锁可以创建多个Condition个,即创建多个monitor。好处是可以唤醒指定线程,而notify只能随机唤醒一个。

image-20200428054455285

一般由Lock的newCondition方法创建:(以ReentrantLock源码为例)

final ConditionObject newCondition() {
    return new ConditionObject();
}

使用Condition构建的生产者-消费者模型(同样是用长度有限的ArrayList来模拟):

public class Test2 {
    public List<Integer> list = new ArrayList<>();
    public static final int MAX = 5;
    public int size = 0;
    public Lock lock = new ReentrantLock();
    public Condition consumer = lock.newCondition();
    public Condition producer = lock.newCondition();

    public void add(int i) {
        lock.lock();
        try {
            if (size >= MAX) {
                consumer.await();
            }
            list.add(i);
            size++;
            producer.signal();
            System.out.println(Thread.currentThread().getName() + " After add " + list);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void delete(int i) {
        lock.lock();
        try {
            if (size == 0) {
                producer.await();
            }
            list.remove(i);
            size--;
            consumer.signal();
            System.out.println(Thread.currentThread().getName() + "After delete " + list);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        Test2 t = new Test2();
        for (int i = 0; i < 5; i++) {
            Thread thread1 = new Thread(() -> {
                while (true) {
                    t.add(1);
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            thread1.start();
        }
        for (int i = 0; i < 3; i++) {
            Thread thread2 = new Thread(() -> {
                while (true) {
                    t.delete(0);
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            thread2.start();
        }
    }
}

Question:Object的monitor方法与condition有何区别?

Ans:Object只支持一个等待队列,而Condition支持多个(创建多个Condition对象)。Condition还支持中断线程,设置等待时间等功能。

newCondition方法里的ConditionObject就是Condition接口的实现类,同时它还是AbstractQueuedSynchronizer的一个内部类(AQS)。因为锁的实现原理就依赖AQS,AQS内部维护了一个同步队列,如果是独占锁,所有获取锁失败的线程都会加入到同步队列中。(阻塞的线程肯定要等待,所以有一个存储它们的结构,这个结构就是AQS,不仅存储,还要同步

AbstractQueuedSynchronizer

概念

image-20200428055058192

AQS是用来构建锁和其他同步组件的基本框架,通过一个原子int和FIFO队列构建的等待队列。底层通过CAS实现原子性,利用队列实现锁竞争。

image-20200428055146048

AQS是实现锁或其他同步组件的关键,ReentrantLock,ThreadPoolExecutor,Semaphore以及Condition都要用到。

AQS与锁的关系:锁是面向使用者,定义了使用者和锁交互的接口,隐藏了实现细节。而同步器是面向锁的实现者,它简化了锁的实现方式,屏蔽了同步状态的管理,线程的派对,等待和唤醒等底层操作。

image-20200428055328470

这5个方法,AQS里没有实现逻辑,但它的其他常用方法却会调用这几个方法。所以子类如果需要,那么就要去对这些方法进行重写。(模板方法设计模式 )

image-20200428055441302

image-20200428055445401

AQS方法总结:

①独占式的获取与释放同步状态

②共享式的获取与释放同步状态

③查询同步队列中等待线程的情况

例子

JDK上的例子:

public class Mutex implements Lock, java.io.Serializable {
    // Our internal helper class
    // 继承AQS的静态内存类
    // 重写方法
    private static class Sync extends AbstractQueuedSynchronizer {
        // Reports whether in locked state
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }

        // Acquires the lock if state is zero
        public boolean tryAcquire(int acquires) {
            assert acquires == 1; // Otherwise unused
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        // Releases the lock by setting state to zero
        protected boolean tryRelease(int releases) {
            assert releases == 1; // Otherwise unused
            if (getState() == 0) throw new IllegalMonitorStateException();
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        // Provides a Condition
        Condition newCondition() {
            return new ConditionObject();
        }

        // Deserializes properly
        private void readObject(ObjectInputStream s)
            throws IOException, ClassNotFoundException {
            s.defaultReadObject();
            setState(0); // reset to unlocked state
        }
    }

    // The sync object does all the hard work. We just forward to it.
    private final Sync sync = new Sync();
    //使用同步器的模板方法实现自己的同步语义
    public void lock() {
        sync.acquire(1);
    }

    public boolean tryLock() {
        return sync.tryAcquire(1);
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }

    public void unlock() {
        sync.release(1);
    }

    public Condition newCondition() {
        return sync.newCondition();
    }

    public boolean isLocked() {
        return sync.isHeldExclusively();
    }

    public boolean hasQueuedThreads() {
        return sync.hasQueuedThreads();
    }

    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    private static Mutex mutex = new Mutex();

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(() -> {
                mutex.lock();
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    mutex.unlock();
                }
            });
            thread.start();
        }
    }
}

执行结果:

image-20200428055649433

image-20200428055656964

Mutex是Lock的一个实现类(同ReentrantLock),提供了lock,unlock,tryLock等方法。但其内部类都是调用了AQS的模板方法。这里只重写了独占锁的实现tryAcquire和tryRelease,调试时确实也只允许一个线程在运行。

对于同步组件Lock,专注于同步状态的判断,实现同步语义(如上面的tryRelease)。而对于AQS,只需要同步组件返回的true / false,根据返回值会有不同的操作。

AQS原理

①同步队列。当共享资源被某个线程占有,其他请求该资源的线程将会阻塞,从而进入同步队列。AQS的同步队列使用的是链式方法去实现,而且是一个双向队列。

AQS里有一个静态内部类Node:

/**
     * Wait queue node class.
     *
     * <p>The wait queue is a variant of a "CLH" (Craig, Landin, and
     * Hagersten) lock queue. CLH locks are normally used for
     * spinlocks.  We instead use them for blocking synchronizers, but
     * use the same basic tactic of holding some of the control
     * information about a thread in the predecessor of its node.  A
     * "status" field in each node keeps track of whether a thread
     * should block.  A node is signalled when its predecessor
     * releases.  Each node of the queue otherwise serves as a
     * specific-notification-style monitor holding a single waiting
     * thread. The status field does NOT control whether threads are
     * granted locks etc though.  A thread may try to acquire if it is
     * first in the queue. But being first does not guarantee success;
     * it only gives the right to contend.  So the currently released
     * contender thread may need to rewait.
     */
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;

        /** waitStatus value to indicate thread has cancelled */
        static final int CANCELLED =  1;
        /** waitStatus value to indicate successor's thread needs unparking */
        static final int SIGNAL    = -1;
        /** waitStatus value to indicate thread is waiting on condition */
        static final int CONDITION = -2;
        /**
         * waitStatus value to indicate the next acquireShared should
         * unconditionally propagate
         */
        static final int PROPAGATE = -3;

        /**
         * Status field, taking on only the values:
         *   SIGNAL:     The successor of this node is (or will soon be)
         *               blocked (via park), so the current node must
         *               unpark its successor when it releases or
         *               cancels. To avoid races, acquire methods must
         *               first indicate they need a signal,
         *               then retry the atomic acquire, and then,
         *               on failure, block.
         *   CANCELLED:  This node is cancelled due to timeout or interrupt.
         *               Nodes never leave this state. In particular,
         *               a thread with cancelled node never again blocks.
         *   CONDITION:  This node is currently on a condition queue.
         *               It will not be used as a sync queue node
         *               until transferred, at which time the status
         *               will be set to 0. (Use of this value here has
         *               nothing to do with the other uses of the
         *               field, but simplifies mechanics.)
         *   PROPAGATE:  A releaseShared should be propagated to other
         *               nodes. This is set (for head node only) in
         *               doReleaseShared to ensure propagation
         *               continues, even if other operations have
         *               since intervened.
         *   0:          None of the above
         *
         * The values are arranged numerically to simplify use.
         * Non-negative values mean that a node doesn't need to
         * signal. So, most code doesn't need to check for particular
         * values, just for sign.
         *
         * The field is initialized to 0 for normal sync nodes, and
         * CONDITION for condition nodes.  It is modified using CAS
         * (or when possible, unconditional volatile writes).
         */
        volatile int waitStatus;

        /**
         * Link to predecessor node that current node/thread relies on
         * for checking waitStatus. Assigned during enqueuing, and nulled
         * out (for sake of GC) only upon dequeuing.  Also, upon
         * cancellation of a predecessor, we short-circuit while
         * finding a non-cancelled one, which will always exist
         * because the head node is never cancelled: A node becomes
         * head only as a result of successful acquire. A
         * cancelled thread never succeeds in acquiring, and a thread only
         * cancels itself, not any other node.
         */
        volatile Node prev;

        /**
         * Link to the successor node that the current node/thread
         * unparks upon release. Assigned during enqueuing, adjusted
         * when bypassing cancelled predecessors, and nulled out (for
         * sake of GC) when dequeued.  The enq operation does not
         * assign next field of a predecessor until after attachment,
         * so seeing a null next field does not necessarily mean that
         * node is at end of queue. However, if a next field appears
         * to be null, we can scan prev's from the tail to
         * double-check.  The next field of cancelled nodes is set to
         * point to the node itself instead of null, to make life
         * easier for isOnSyncQueue.
         */
        volatile Node next;

        /**
         * The thread that enqueued this node.  Initialized on
         * construction and nulled out after use.
         */
        volatile Thread thread;

        /**
         * Link to next node waiting on condition, or the special
         * value SHARED.  Because condition queues are accessed only
         * when holding in exclusive mode, we just need a simple
         * linked queue to hold nodes while they are waiting on
         * conditions. They are then transferred to the queue to
         * re-acquire. And because conditions can only be exclusive,
         * we save a field by using special value to indicate shared
         * mode.
         */
        Node nextWaiter;
}

waitStatus:节点的状态

prev:当前节点 / 线程的前驱节点

next:后继节点

thread;加入到同步队列的线程引用

nextWaiter:等待队列中的下一个节点

节点的状态:

CANCELLED:节点从等待队列中取消

SIGNAL:后继节点处于等待状态,如果当前节点释放同步状态,将会通知后继节点,使得后继节点的线程能够运行

CONDITION:当前节点处于等待队列,节点在Condition里等待。当其他线程对此Condition调用singnal方法,Condition状态的节点将从等待队列转移到同步队列中,等待获取同步锁。

PROPAGATE:表示下一次共享式同步状态获取将会无条件传播下去。用于共享模式,此时前继节点不仅会唤醒后继节点,还可能会唤醒后继节点的后继节点(传播)。

INITIAL,值为0

SHARED / EXCLUSIVE : 共享锁 / 独占锁

CLH锁

知道了Node的成员变量之后,再看类前的注释,可以知道,Node使用的是一种名为CLH锁的东西,一般用于改进自旋锁.

Question:什么是CLH锁,为什么要有CLH锁,CLH锁为什么要使用双向队列?

Ans:

预备知识:CPU有三种架构,SMP,NUMA,MPP

SMP:Symmetric Multi Processing,即所有的CPU都是对等的,通过总线连接共享一块物理内存,因而所有的CPU都共享全部资源。操作系统只管理一个队列,每个处理器依次处理队列中的进程。常见的PC都是这种架构,拓展性较差,因为随着CPU数量的增加,内存访问冲突将迅速增加,最终导致CPU资源的浪费,使CPU性能的有效性大大降低。(SMP副武器的CPU利用率,最好的情况是2~4个CPU)

NUMA:Non-Uniform Memory Access。NUMA是SMP的一种改进,它的基本特征是具有多个CPU模块,每个CPU模块由多个CPU(如4个)组成,并且具有独立的本地内存,IO槽口等。由于其节点之间可以通过互联模块进行连接和信息的交互,因此每个CPU可以访问整个系统的内存(与MPP的主要区别)。同时,访问本地内存的速度将远远高于访问远地内存(系统内其他节点的内存)的速度,这也是非一致存储访问NUMA的由来。由于这个特点,为了更好地发挥系统性能,开发应用程序时需要尽量减少不同CPU模块之间的信息交互。利用NUMA,可以较好地解决SMP的拓展问题,在一个服务器可以支持上百个CPU。但是在CPU数量增加到一定程度的时候,由于访问远地内存的速度较慢,这时候增加CPU带来的提升会降低。

MPP:Massive Parallel Processing。MPP提供了另外一种进行系统拓展的方式,它由多个SMP服务器通过一定的节点互联网进行连接,协同工作,完成相同的任务。其基本特征是由多个SMP服务器(一个SMP服务器称为一个节点)通过节点互联网络连接而成,每个节点只访问自己的本地资源,是一个完全无共享的结构,因而其拓展能力最好,理论上可以无限制拓展。也就是通过节点网络传输信息,这个过程称为数据重分配Data Redistribution。

三者都有不同的优势和缺点。SMP适合存储中心集中型的数据,比如中心数据库。NUMA适合OLTP事务处理环境,MPP适合决策和数据挖掘。

回到CLH锁,它的作用是改良自旋锁。之前我们提到过,自旋锁在多处理器下读写同一个变量,每次操作都在多个处理器缓存之间进行缓存同步,这会导致繁重的系统总线和内存的流量,大大降低系统整体的性能。解决这个问题有两种方法,一种是MCS锁,另一种是CLH锁。

①MCS锁。MCS Spinlock 是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,直接前驱负责通知其结束自旋,从而极大地减少了不必要的处理器缓存同步的次数,降低了总线和内存的开销。

import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

public class MCSLock {
    public static class MCSNode {
        volatile MCSNode next;
        volatile boolean isBlock = true; // 默认是在等待锁
    }

    volatile MCSNode queue;// 指向最后一个申请锁的MCSNode
    private static final AtomicReferenceFieldUpdater UPDATER = AtomicReferenceFieldUpdater
        .newUpdater(MCSLock.class, MCSNode.class, "queue");

    public void lock(MCSNode currentThread) {
        MCSNode predecessor = UPDATER.getAndSet(this, currentThread);// step 1
        if (predecessor != null) {
            predecessor.next = currentThread;// step 2

            while (currentThread.isBlock) {// step 3
            }
        }else { // 只有一个线程在使用锁,没有前驱来通知它,所以得自己标记自己为非阻塞
            currentThread. isBlock = false;
        }
    }

    public void unlock(MCSNode currentThread) {
        if (currentThread.isBlock) {// 锁拥有者进行释放锁才有意义
            return;
        }

        if (currentThread.next == null) {// 检查是否有人排在自己后面
            if (UPDATER.compareAndSet(this, currentThread, null)) {// step 4
                // compareAndSet返回true表示确实没有人排在自己后面
                return;
            } else {
                // 突然有人排在自己后面了,可能还不知道是谁,下面是等待后续者
                // 这里之所以要忙等是因为:step 1执行完后,step 2可能还没执行完
                while (currentThread.next == null) { // step 5
                }
            }
        }

        currentThread.next.isBlock = false;
        currentThread.next = null;// for GC
    }
}

②CLH锁。CLH锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋。

import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

public class CLHLock {
    public static class CLHNode {
        private volatile boolean isLocked = true; // 默认是在等待锁
    }

    @SuppressWarnings("unused" )
    private volatile CLHNode tail ;
    private static final AtomicReferenceFieldUpdater<CLHLock, CLHNode> UPDATER = AtomicReferenceFieldUpdater
        . newUpdater(CLHLock.class, CLHNode .class , "tail" );

    public void lock(CLHNode currentThread) {
        CLHNode preNode = UPDATER.getAndSet( this, currentThread);
        if(preNode != null) {//已有线程占用了锁,进入自旋
            while(preNode.isLocked ) {
            }
        }
    }

    public void unlock(CLHNode currentThread) {
        // 如果队列里只有当前线程,则释放对当前线程的引用(for GC)。
        if (!UPDATER .compareAndSet(this, currentThread, null)) {
            // 还有后续线程
            currentThread. isLocked = false ;// 改变状态,让后续线程结束自旋
        }
    }
}

二者图示比较:

CLH-MCS-SpinLock

差距:

①CLH是在前驱节点的属性上自旋,而MCS是在本地属性变量上自旋

②CLH锁释放时,只需要改变自己的属性,而MCS锁释放则需要改变后继节点的属性。

③从链表队列来看,CLH的队列是隐式的,CLH的Node并不实际持有下一个节点,而MCS的队列是物理存在的。

显然,根据我们前面所说的CPU架构,MCS适合NUMA架构,避免读取其他内存块时的巨大开销。而CLH锁无须考虑这个层次,一般都是SMP架构。于是AQS的实现使用CLH锁,改进了自旋锁,并且无须考虑NUMA架构的情况下改用MCS锁的情况,代码更简单。CLH需要记录前继节点和后继节点,因而,CLH锁使用双向队列。

AQS通过头尾指针来管理同步队列,同时实现包括获取锁失败的线程进行入队,释放锁时对同步队列中的线程进行通知等核心方法。示意图如下:

image-20200428164924120

节点的入队和出队,就是对应着锁的获取和释放两个操作。获取锁失败进行入队操作,获取锁成功进行出队操作。

常用方法

AQS的模板方法:同步队列,锁的获取和释放,锁的等待,对线程同步状态的管理,对阻塞队列进行派对,等待通知。

常用方法:acquire,release,acquireInterruptibly,acquireShared,releaseShared等

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

当锁调用lock方法时,表明它要获取一个独占锁。如果获取失败,就将当前线程加入到同步队列,如果成功直接执行。而lock方法实际上会调用AQS的acquire方法。首先是通过!tryAcquire(arg)看同步状态是否获取成功,如果获取成功就直接结果并返回了。如果失败,就执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

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; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&	// 获取锁失败,判断是否要阻塞
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

大概就是一个无限循环,一直在尝试获取锁tryAcquire,如果获取成功就直接返回该线程,然后让该线程去执行。如果获取锁失败,此时会调用shouldParkAfterFailedAcquire方法,主要是靠前驱节点来判断节点当前线程是否应该被阻塞。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;		// 前驱节点的状态
    if (ws == Node.SIGNAL)	// 前驱节点为SIGNAL,表示当前线程处于等待状态,直接返回true
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true;
    if (ws > 0) {	// 大于0表示状态为CANCELLED,表示该节点已经超时或者被中断了,需要从同步队列中取消
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {	// 前驱节点为Condition / Propagate,此时线程需要等待SIGNAL信号,但还没有停止
        /*
         * 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;
}

这段代码主要检查当前线程是否需要被阻塞,具体规则如下:

1.如果当前线程的前驱节点状态为SIGNAL,则表明当前线程需要被阻塞,调用unpark方法唤醒,直接返回true,当前线程被阻塞。

2.如果当前线程的前驱节点状态为CANCELLED,则表明该线程的前驱节点已经等待超时或者被中断了,此时需要从CLH队列中将该前驱节点删除,直接回溯到前驱节点状态 <= 0,返回false

3.如果前驱节点的状态既不是SIGNAL,也不是CANCELLED,则通过CAS的方式将其前驱节点设置其SIGNAL,返回false

如果shouldParkAfterFailedAcquire返回true,此时将调用parkAndCheckInterrupt方法将当前线程挂起,从而阻塞线程的调用栈,并同时返回当前线程的中断状态。

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

最后调用的是LockSupport工具类进行创建锁和实现线程阻塞,LockSupport里调用的也是Unsafe类。

这时候我们理解了acquireQueued(addWaiter(Node.EXCLUSIVE), arg)中的acquireQueued方法,那么addWaiter又是什么方法?从acquireQueued的参数列表就可以看出来,addWaiter方法返回一个Node,而它的方法名表示它会添加一个节点到同步等待队列中,Node.EXCLUSIVE表明加入的是独占锁。看源码:

private Node addWaiter(Node mode) {
    // 将当前线程构建成Node类型
    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) {			// 判断尾结点是否为null
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {	// 尾结点不为null,采用尾插法
            pred.next = node;				// 原先的tail的next指向node
            return node;
        }
    }
    enq(node);		// 尾结点为null,说明同步队列为空,直接加入即可
    return node;
}

可以看到,addWaiter的逻辑:先判断尾结点是否为null,如果为null,直接调用enq方法插入。如果不为null,则通过CAS操作compareAndSetTail方法,使得当前同步队列的tail为node,并且原先的tail的next指向node。可是这里的CAS操作并没有放在循环里,也就是无法自旋。那么在CAS操作出错,返回false之后,仍然会执行到enq方法。因此addWaiter只尝试了一次快速的入队操作,如果失败了再进入到enq方法,这也是JDK注释所说的“Try the fast path of enq; backup to full enq on failure”。可以预知enq里仍然有初始化的时候,也有CAS自旋代码。enq方法源码:

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))	// 队列为空,创建head节点,tail和head指向同一个
                tail = head;
        } else {
            node.prev = t;			// 尾插法
            if (compareAndSetTail(t, node)) {	// CAS操作,如果失败就自旋
                t.next = node;
                return t;
            }
        }
    }
}

enq方法只有在第一个线程加入同步队列时,调用compareAndSetHead方法,完成链式队列的头结点初始化,之后通过自旋,不断尝试CAS尾插法,直到线程插入到同步队列为止。

acquire流程图:

image-20200429124413834

②release

acquire是独占锁的获取过程,因为获取失败的时候还要加入到同步队列中,因此逻辑还是比较复杂的。而独占锁的释放就相对而言简单一点,需要调用的是release方法:

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

先进行同步状态释放(tryRelease),如果释放成功,则会执行if块中的代码。当head指向的头结点不为null,且该节点的状态值不为0的话(如果为0,表示节点还在运行中,不release,也不释放锁),执行unparkSuccessor方法,如下:

/**
     * Wakes up node's successor, if one exists.
     *
     * @param node the node
     */
    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)
            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);
    }

首先判断当前节点的状态,如果小于0,因为等一下我们就要释放它的后继节点了,所以这时候就把它的状态改为0compareAndSetWaitStatus(node, ws, 0);。唤醒后继线程的逻辑有点意思,先判断是否为null,或者是否已经cancelled,如果是,那么将进入循环体,s = null;意义不明。然后循环是从tail开始的,从队列结尾,一直往前面找,直到找到最前面的一个节点。然后对该节点调用LockSupport.unpark(s.thread);,解除阻塞。

Question: unparkSuccessor为什么是从后往前寻找第一个非Cancelled的节点?

Ans:主要是enq方法里的compareAndSetTail方法,以及t.next = node;,这两个步骤并不是原子性。如果在执行CAS,在重定向next指针之前,这时候执行了unparkSuccessor方法,此时next还没有指向正确的节点,导致无法从前往后找。此外,当一个节点的状态转为cancelled时,先断开的是next指针,prev指针并未断开。因而,入队的非原子性操作,以及cancelled节点的产生过程中断开的next指针的操作,会让此时从前往后遍历,无法遍历所有的节点。而从后往前遍历则避免了这个情况。·

③cancelAcquire

在acquire的最后,finally语句可能会调用cancelAcquire方法,用于生成CANCELLED状态节点。该方法逻辑:

private void cancelAcquire(Node node) {
    // Ignore if node doesn't exist
    if (node == null)		// 将无效节点过滤
        return;

    node.thread = null;		// 设置该节点不关联任何线程,也就是虚节点

    // Skip cancelled predecessors
    Node pred = node.prev;			
    while (pred.waitStatus > 0)       // 跳过前驱节点,跳过取消状态的node
        node.prev = pred = pred.prev;

    // predNext is the apparent node to unsplice. CASes below will
    // fail if not, in which case, we lost race vs another cancel
    // or signal, so no further action is necessary.
    Node predNext = pred.next;		// 获取过滤后的前驱节点的后继节点

    // Can use unconditional write instead of CAS here.
    // After this atomic step, other Nodes can skip past us.
    // Before, we are free of interference from other threads.
    node.waitStatus = Node.CANCELLED;	// 设置node的状态为CANCELLED

    // If we are the tail, remove ourselves.
    if (node == tail && compareAndSetTail(node, pred)) {
        compareAndSetNext(pred, predNext, null);
    } else {				// 从后往前找第一个非CANCELLED状态的节点为尾结点
        // If successor needs signal, try to set pred's next-link
        // so it will get one. Otherwise wake it up to propagate.
        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; // help GC
    }
}

流程:

①获取当前节点的前驱节点,如果前驱节点的状态是CANCELLED,那就一直往前遍历,找到第一个waitStatus <= 0的节点,将找到的pred节点和当前Node相关联,并且把node的状态设置为CANCELLED

②根据当前节点的位置,分成三种情况。当前节点是头结点head的后继节点,尾结点,中间节点

(1)当前节点是尾结点(断开pred的next,然后修改tail为pred)

img

(2)当前节点是head的后继节点(当前节点的next指向自己,断开了next指针,prev没有改变)

img

(3)既不是head的后继节点,也不是尾结点(pred.next指向pred.next.next,然后当前节点的next转为指向自己)

img

可以看到,所有的变化都是对next指针进行操作,而没有对prev指针操作。

Question:为什么所有的cancel变化都是对对next操作,而没有对prev指针进行操作?什么情况下操作prev?

Ans:(1)执行cancelAcquire的时候,当前节点的前置节点可能已经从队列中出去了(已经执行过Try代码块中的shouldParkAfterFailedAcquire方法了),如果此时修改Prev指针,有可能会导致Prev指向另一个已经移除队列的Node,因此这块变化Prev指针不安全。

(2)shouldParkAfterFailedAcquire方法中,会执行

do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);

其实就是在处理Prev指针。shouldParkAfterFailedAcquire是获取锁失败的情况下才会执行,进入该方法后,说明共享资源已被获取,当前节点之前的节点都不会出现变化,因此这个时候变更Prev指针比较安全。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值