AQS补充——关于LockSupport和acquireQueue()

先关注LockSupport

/**
 * Basic thread blocking primitives for creating locks and other
 * synchronization classes.
 *  ...
 */

一开始这句话能够理解这是一个基本阻塞工具,AQS中实现了队列,但是当一个线程没有获取锁的时候,是如何阻塞的呢,便是基于这个类。

1、LockSupport 重要API

Park

 public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(false, 0L);     //核心在这句,等待unpark
        setBlocker(t, null);
    }
/**
    当前park的线程, parkBlocker对象,类似于sync(o).
    private static void setBlocker(Thread t, Object arg) {
     // Even though volatile, hotspot doesn't need a write barrier here.
      UNSAFE.putObject(t, parkBlockerOffset, arg);
    }
  */

 public static void park() {
        UNSAFE.park(false, 0L);     //核心在这句,等待unpark
    }

大体上就是加锁等待,其他parkNano等方法类似sleep。

unPark

//解除封印
public static void unpark(Thread thread) {
    if (thread != null)
        UNSAFE.unpark(thread);
}

2、 一个互斥锁

基于LockSupport实现,来自于java.util.concurrent.locks.LockSupport.

class FIFOMutex {

    // 标识
    private final AtomicBoolean locked = new AtomicBoolean(false);

    // 等待队列
    private final Queue waiters = new ConcurrentLinkedQueue();

    // 加锁
    public void lock() {
        boolean wasInterrupted = false;
        Thread current = Thread.currentThread();
        // 线程安全的
        waiters.add(current);

        // 头节点是当前线程,且lock为true ,则代表不需要阻塞.循环的目的就是防止用户执行unpark
        while (waiters.peek() != current || !locked.compareAndSet(false, true)) {
            // 执行到这里会阻塞/等待
            LockSupport.park(this);
            if (Thread.interrupted()) // ignore interrupts while waiting
                wasInterrupted = true;
        }

        // 移除头节点,因为当前线程不需要等待.
        waiters.remove();
        if (wasInterrupted)          // reassert interrupt status on exit
            current.interrupt();
    }

    // 解锁
    public void unlock() {
        // 设置,这里由于就一个线程操作, 所以不需要cas操作.
        locked.set(false);
        // unPark下一个等待的线程.
        LockSupport.unpark(waiters.peek());
    }
}

acquireQueue()

我们先从acquire开始分析:

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

先调用tryAcquire(自实现)去获取,失败后加入队列,我们再看addWaiter方法:

private Node addWaiter(Node mode) {
    //以给定模式构造结点。mode有两种:EXCLUSIVE(独占)和SHARED(共享)
    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入队。
    enq(node);
    return node;
}

enq就不贴了,自旋添加入队,重点关注acquireQueue();通过tryAcquire()和addWaiter(),该线程获取资源失败,已经被放入等待队列尾部了。聪明的你立刻应该能想到该线程下一部该干什么了吧:进入等待状态休息,直到其他线程彻底释放资源后唤醒自己,自己再拿到资源,然后就可以去干自己想干的事了。没错,就是这样!是不是跟医院排队拿号有点相似~~acquireQueued()就是干这件事:在等待队列中排队拿号(中间没其它事干可以休息),直到拿到号后再返回

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;//标记是否成功拿到资源
    try {
        boolean interrupted = false;//标记等待过程中是否被中断过
        
        //又是一个“自旋”!
        for (;;) {
            final Node p = node.predecessor();//拿到前驱
            //如果前驱是head,即该结点已成老二,那么便有资格去尝试获取资源(可能是老大释放完资源唤醒自己的,当然也可能被interrupt了)。
            if (p == head && tryAcquire(arg)) {
                setHead(node);//拿到资源后,将head指向该结点。所以head所指的标杆结点,就是当前获取到资源的那个结点或null。
                p.next = null; // setHead中node.prev已置为null,此处再将head.next置为null,就是为了方便GC回收以前的head结点。也就意味着之前拿完资源的结点出队了!
                failed = false; // 成功获取资源
                return interrupted;//返回等待过程中是否被中断过
            }
            
            //如果自己可以休息了,就通过park()进入waiting状态,直到被unpark()。如果不可中断的情况下被中断了,那么会从park()中醒过来,发现拿不到资源,从而继续进入park()等待。
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;//如果等待过程中被中断过,哪怕只有那么一次,就将interrupted标记为true
        }
    } finally {
        if (failed) // 如果等待过程中没有成功获取资源(如timeout,或者可中断的情况下被中断了),那么取消结点在队列中的等待。
            cancelAcquire(node);
    }
}

这里面有两个值得关注的方法,一个是shouldParkAfterFailedAcquire(),另一个是parkAndCheckInterrupt():

前者是用来检测自己是否真的可以去休息了,也就是进入线程的waiting状态,只是一个检查;后者是去休息;

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;//拿到前驱的状态
    if (ws == Node.SIGNAL)
        //如果已经告诉前驱拿完号后通知自己一下,那就可以安心休息了
        return true;
    if (ws > 0) {
        /*
         * 如果前驱放弃了,那就一直往前找,直到找到最近一个正常等待的状态,并排在它的后边。
         * 注意:那些放弃的结点,由于被自己“加塞”到它们前边,它们相当于形成一个无引用链,稍后就会被保安大叔赶走了(GC回收)!
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
         //如果前驱正常,那就把前驱的状态设置成SIGNAL,告诉它拿完号后通知自己一下。有可能失败,人家说不定刚刚释放完呢!
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}


private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);//调用park()使线程进入waiting状态
    return Thread.interrupted();//如果被唤醒,查看自己是不是被中断的。
}

总结下它的流程吧:

  1. 调用自定义同步器的tryAcquire()尝试直接去获取资源,如果成功则直接返回;
  2. 没成功,则addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
  3. acquireQueued()使线程在等待队列中休息,有机会时(轮到自己,会被unpark())会去尝试获取资源。获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
  4. 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。
img

参考原文:Java并发之AQS详解

你可能感兴趣的:(AQS补充——关于LockSupport和acquireQueue())