jdk中cocurrent下的AbstractQueuedSynchronizer理解记录

   以前虽然看过一次AQS的源码实现,但在过一段时间后与同学交流时,发觉自己理解并不够深,印像太浅。需要做一个记录整理,帮助自己消化。

 

AQS中Node的设计: 

 

jdk中cocurrent下的AbstractQueuedSynchronizer理解记录_第1张图片

几个点:

1. Node实现作者: "CLH" (Craig, Landin, and * Hagersten) ,有名的CLH queue

2. 是一个FIFO的链表的实现,对于队列的控制经常要做double-check。

3. Node节点通过一个int waiteStatus代表一些不同意义的状态。

 

  • SIGNAL=-1,代表是需要当前Node节点需要唤起后一个Node节点。在Node节点enqueue时,会设置前一个节点的状态。这样链式的唤醒,完成这样的一个交接棒。
  • CONDITION = -2 , 
4. nextWaiter一个标志位,就是用于表明是采用的共享锁还是排他锁。同时也是其对应condition队列的引用节点。

来看一下Node操作的一个double-check设计

node的equeue操作: 
private Node addWaiter(Node mode) {
        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) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {  // 位置1
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize    // 位置3
                Node h = new Node(); // Dummy header
                h.next = node;
                node.prev = h;
                if (compareAndSetHead(h)) {   // 位置4
                    tail = node;
                    return h;
                }
            }
            else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {   // 位置2
                    t.next = node;
                    return t;
                }
            }
        }
    }
 
代码中标记了4个位置:
1.  node默认的equeue操作都是接在tail节点之后,prev节点指定完成后,进行一个cas设置操作,将当前加入的节点做为tail。 因为会有并发操作,原先的tail节点会有所变化,位置1处会出现失败。这样就进入第2步的check机制
2.  位置2,每次都取一次当前的tail节点,尝试通过cas设置操作,将当前节点做为tail,有并发变化/竞争导致处理失败,继续重复这一动作,直到完成为之。 
3.  在位置1和2处理的一种异常情况,就是tail节点为空,有可能该节点是第一个进行enqueue,也有可能是node节点被所有唤醒。这时需要重新创建队列,创建一个空的Head node节点,将自己做为tail节点。
4.  同样考虑并发因素,位置3在处理时,可能已有线程创建了Head节点,这样就又回到位置2上的处理,将node节点添加在tail节点之后。

位置2,3,4都是基于一种发展的锁机制实现,尝试N次竞争后,总能成功。

node的dequeue操作: 
一个比较巧妙的设计,需要细细品味。
出库分为两个动作: 

动作1: 
private void setHead(Node node) {
        head = node;
        node.thread = null;
        node.prev = null;
    }
轮到自己出库后,进行head节点的gc处理,断开自己node节点和链表的一些应用。同时将自己添加为head。 
说明:刚开始对出库时将自己node设置为head,一直想不明白。后来仔细看了下是否轮到出库时的判断就可以明白了。
final Node p = node.predecessor();   // 位置1
   if (p == head && tryAcquire(arg)) {
   setHead(node);
   p.next = null; // help GC
   return interrupted;
}
轮到出库时的判断:
   在位置1上,每次判断都是获取当前节点的prev节点,判断==head节点。最后就可以这么理解,head节点其实是一个“傀儡”,代表的是上一个出库的节点,因为是一个FIFO队列,如果当前的上一个节点已经出库,那就可以轮到自己

动作2: 
private void unparkSuccessor(Node node) {
        /*
         * Try to clear status in anticipation of signalling.  It is
         * OK if this fails or if status is changed by waiting thread.
         */
        compareAndSetWaitStatus(node, Node.SIGNAL, 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) { //位置1
            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上,当一个节点需要出库后,唤醒下一个非cancel的节点。这里的LockSupport的代码设计也有一点取巧,后续可以再写点。

acquire , release , cancel三个动作设计

按照原先作者的设计: 

 

  Acquire:
      while (!tryAcquire(arg)) {
          enqueue thread if it is not already queued;
          possibly block current thread;
      }
 
  Release:
      if (tryRelease(arg))
         unblock the first queued thread;

 

 预留了5个protected方法,用于client自己实现相关的处理,进行业务行为控制,因为cocurrent很多Lock,Future的实现都是基于此扩展,定义了自己的处理。

具体的一些方法使用,后续再补。

 

acquire动作:

独占锁:

 

  1. public final void acquire(int arg)
  2. public final void acquireInterruptibly(int arg) throws InterruptedException
  3. public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException 

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
说明:
1. tryAcquire自定义的扩展,一般用value值进行控制,类似P/V原语的控制。
2.  addWaiter是一个入库的动作,前面已经介绍。
3. 来看一下accquireQueued方法
final boolean acquireQueued(final Node node, int arg) {
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();   // 位置1
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&  // 位置2
                    parkAndCheckInterrupt())  // 位置3
                    interrupted = true;
            }
        } catch (RuntimeException ex) {
            cancelAcquire(node);  // 位置4
            throw ex;
        }
    }
位置1: 前面已经介绍过,就是对比一下我的上一个节点是否已经出队列,如果已经出队列,就认为当前轮到自己出队列,返回 interrupted的标志。
位置2: 执行了一个动作,就是设置一下当前节点的上一个节点的waitStatus状态为SINGLE,让其在出队列的时候能唤醒自己进行处理。
位置3: 在设置了上一个节点为SINGLE后,当前线程就可以进行park,转到阻塞状态,直到等到被唤醒。 (唤醒条件有2个: 前一个节点的唤醒和 Thread.interupte事件)
位置4: 就是一个cancel动作。

和accquire方法的区别,就是针对Thread.interrupt会响应一个InterruptedException异常,立马返回。而accquire会一直等待直到自己可以出队列。

 

看一下其核心方法:
private void doAcquireInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.EXCLUSIVE);
        try {
            for (;;) {
                final Node p = node.predecessor();  // 位置1
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    return;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&  // 位置2
                    parkAndCheckInterrupt())
                    break;   // 位置3
            }
        } catch (RuntimeException ex) {
            cancelAcquire(node);
            throw ex;
        }
        // Arrive here only if interrupted
        cancelAcquire(node);
        throw new InterruptedException();   // 位置4
    }
 位置1,2和accquire一样,唯一的区别小点就在 位置3上直接进行了break,跳出响应位置4了 InterruptedException。

和accquire, accquireInterruptibly方法的区别,就在于在支持 Interrupted响应的基础上,还支持了Timeout超时的控制,在指定时间内无法获取对应锁,抛出对应的TimeoutException
来看一下核心方法:
private boolean doAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
        long lastTime = System.nanoTime();
        final Node node = addWaiter(Node.EXCLUSIVE);
        try {
            for (;;) {
                final Node p = node.predecessor();   // 位置1
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    return true;
                }
                if (nanosTimeout <= 0) {   // 位置2
                    cancelAcquire(node);
                    return false;
                }
                if (nanosTimeout > spinForTimeoutThreshold &&  // 位置3
                    shouldParkAfterFailedAcquire(p, node))   // 位置4
                    LockSupport.parkNanos(this, nanosTimeout);  // 位置5
                long now = System.nanoTime();   // 位置6
                nanosTimeout -= now - lastTime;
                lastTime = now;
                if (Thread.interrupted())   // 位置7
                    break;
            }
        } catch (RuntimeException ex) {
            cancelAcquire(node);
            throw ex;
        }
        // Arrive here only if interrupted
        cancelAcquire(node);
        throw new InterruptedException();  // 位置8
    }
位置1: 和先前的accquire一样
位置2:6都是对超时时间的处理,nanosTimeout为当前还需要等待的时间,每次检查一下nanosTimeout时间,并在循环过程中减去对应的处理时间。
位置3:一个自旋锁的优化判断
位置4:和先前的accquire一样,设置上一个Node节点的waitStatus状态。
位置5:和先前accquire有点不同,使用 LockSupport指定时间的park方法,完成对应的时间控制。

共享锁:

 

public final void acquireShared(int arg)

和独占锁处理方式基本类似,来看一下核心代码:

 

private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED);
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor(); 
                if (p == head) {
                    int r = tryAcquireShared(arg); 
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);   // 位置1
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } catch (RuntimeException ex) {
            cancelAcquire(node);
            throw ex;
        }
    }

这里设计上有点小技巧,原先思考一个共享锁的典型场景:读写锁。 一旦写锁释放,应该是唤起所有的读锁。而原先在看setHeadAndPropagate,并没有一个循环释放锁的过程。后来思考了下,采用的是一个链式释放的过程,前一个shared的锁对象释放下一个,在释放的时候继续进行tryAccquireShared控制。

 

一点感悟:在写并发程序时,一些传统编程的思路要有所改变。

 

public final void acquireSharedInterruptibly(int arg) throws InterruptedException

public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout) throws InterruptedException

这两个实现上和独占锁类似,也就是setHeadAndPropagate处理上的不同点而已。

 

 

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;

没啥特别好将的,一看基本也就明白了,出队列的时候,同时唤醒下一个Node。

 

cancel动作:

private void cancelAcquire(Node node) 

 

 

代码就不贴了,几个处理:

1. 从链表上删除cancel节点

2. 如果cancel节点是head,则尝试唤醒cancel节点的下一个节点。

 

 

ConditionObject的理解

几个主要方法:

 

  • public final void await() throws InterruptedException
  • public final void awaitUninterruptibly()
  • public final long awaitNanos(long nanosTimeout) throws InterruptedException
  • public final boolean awaitUntil(Date deadline) throws InterruptedException
  • public final boolean await(long time, TimeUnit unit) throws InterruptedException
  • public final void signal()
  • public final void signalAll()

先理解一下ConditionObject的应用场景,和Objectd的wait,single方法使用场景的区别。
就拿生产者/消费者程序看,假想用object.wait和single实现: 

伪代码如下:
Array queue;
Object empty = new Object();
Object full = new Object();

// 生产者
if(queue 是否满了)
   full.wait() //阻塞等待
else 
   put(queue , data) //放入数据
   empty.single(); // 已经放了一个,通知一下


// 消费者
if(queue 是否空了)
  empty.wait() // 阻塞等待
else 
  data = get(queue);
  full.single() //  已经消费了,通知一下
 
存在的问题: 
1. 如何保证put和get数据是满足是线程安全的? CAS设计 or 使用同步原语? 

很明显,CAS的设计满足不了 queue的两个操作,第一数据入库,第二下标+1,存在安全隐患。所以需要使用对queue进行同步控制。但这样会引出思索的问题,拿到queue的锁后,一直在等待empty或者full通知。
这里ConditionObject就能很好的解决这个问题,不存在死的问题。它的执行一个条件的await操作时,会首先释放当前所持有的Lock,让其他的线程可以进行生产/消费处理。说白了2个object是基于同一个Node等待队列链表。

 

整体概念

在整个AQS存在两种链表。 一个链表就是整个Sync Node链表,横向链表。另一种链表就是Condition的wait Node链表,相对于Sync node,它属于node节点的一个纵向链表。当纵向列表被single通知后,会进入对应的Sync Node进行排队处理。

 

通过这样的纵横队列,实现了ConditionObject共享lock锁数据。

 

你可能感兴趣的:(jdk,thread,编程)