• 4.锁--Synchronizer Framework Base Class—AbstractQueuedSynchronizer介绍


    1. AQS简单介绍

    AQS是Java并发类库的基础。其提供了一个基于FIFO队列,可以用于构建锁或者其它相关同步装置的基础框架。该同步器(下面简称同步器)利用了一个int来表示状态,期望它可以成为实现大部分同步需求的基础。使用的方法是继承。子类通过继承同步器并须要实现它的方法来管理其状态,管理的方式就是通过类似acquire和release的方式来操纵状态。

    然而多线程环境中对状态的操纵必须确保原子性,因此子类对于状态的把握,须要使用这个同步器提供的下面三个方法对状态进行操作:

    • java.util.concurrent.locks.AbstractQueuedSynchronizer.getState()
    • java.util.concurrent.locks.AbstractQueuedSynchronizer.setState(int)
    • java.util.concurrent.locks.AbstractQueuedSynchronizer.compareAndSetState(int, int)

    子类推荐被定义为自己定义同步装置的内部类。同步器自身没有实现不论什么同步接口。它不过定义了若干acquire之类的方法来供使用。

    该同步器即能够作为排他模式也能够作为共享模式。当它被定义为一个排他模式时,其它线程对其的获取就被阻止,而共享模式对于多个线程获取都能够成功。

    同步器是实现锁的关键。利用同步器将锁的语义实现,然后在锁的实现中聚合同步器。能够这样理解:锁的API是面向使用者的,它定义了与锁交互的公共行为,而每一个锁须要完毕特定的操作也是透过这些行为来完毕的(比方:能够同意两个线程进行加锁,排除两个以上的线程),可是实现是依托给同步器来完毕;同步器面向的是线程訪问和资源控制,它定义了线程对资源能否够获取以及线程的排队等操作。锁和同步器非常好的隔离了二者所须要关注的领域。严格意义上讲,同步器能够适用于除了锁以外的其它同步设施上(包含锁)。

    2. CLH算法

    锁的实现是以CLH算法为基础。

    以下简介一下CLH算法:

    CLH算法构建了隐式的链表,是一种非堵塞算法的实现。CLH队列中的结点QNode中含有一个locked字段,该字段若为true表示该线程须要获取锁,且不释放锁,为false表示线程释放了锁。

    结点之间是通过隐形的链表相连,之所以叫隐形的链表是由于这些结点之间没有明显的next指针。而是通过myPred所指向的结点的变化情况来影响myNode的行为。

    CLHLock上另一个尾指针,始终指向队列的最后一个结点。CLHLock的类图例如以下所看到的:



    当一个线程须要获取锁时。会创建一个新的QNode。将当中的locked设置为true表示须要获取锁,然后线程对tail域调用getAndSet方法。使自己成为队列的尾部,同一时候获取一个指向其前趋的引用myPred,然后该线程就在前趋结点的locked字段上旋转。直到前趋结点释放锁。

    当一个线程须要释放锁时。将当前结点的locked域设置为false。同一时候回收前趋结点。例如以下图所看到的,线程A须要获取锁,其myNode域为true。些时tail指向线程A的结点。然后线程B也增加到线程A后面,tail指向线程B的结点。

    然后线程A和B都在它的myPred域上旋转,一量它的myPred结点的locked字段变为false。它就能够获取锁扫行。明显线程A的myPred locked域为false,此时线程A获取到了锁。


    整个CLH的代码例如以下。当中用到了ThreadLocal类,将QNode绑定到每个线程上。同一时候用到了AtomicReference,对尾指针的改动正是调用它的getAndSet()操作来实现的,它可以保证以原子方式更新对象引用。
    CLH算法的示意代码例如以下:
    [java] view plaincopy在CODE上查看代码片派生到我的代码片
    1. import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;  
    2.   
    3. public class CLHLock {  
    4.     public static class CLHNode {  
    5.         private boolean isLocked = true// 默认是在等待锁  
    6.     }  
    7.   
    8.     @SuppressWarnings("unused" )  
    9.     private volatile CLHNode tail ;  
    10.     private static final AtomicReferenceFieldUpdater<CLHLock, CLHNode> UPDATER = AtomicReferenceFieldUpdater  
    11.                   . newUpdater(CLHLock.class, CLHNode .class , "tail" );  
    12.   
    13.     public void lock(CLHNode currentThread) {  
    14.         CLHNode preNode = UPDATER.getAndSet( this, currentThread);  
    15.         if(preNode != null) {//已有线程占用了锁,进入自旋  
    16.             while(preNode.isLocked ) {  
    17.             }  
    18.         }  
    19.     }  
    20.   
    21.     public void unlock(CLHNode currentThread) {  
    22.         // 假设队列里仅仅有当前线程,则释放对当前线程的引用(for GC)。  
    23.         if (!UPDATER .compareAndSet(this, currentThread, null)) {  
    24.             // 还有兴许线程  
    25.             currentThread. isLocked = false ;// 改变状态,让兴许线程结束自旋  
    26.         }  
    27.     }  
    28. }  
    至于AQS的实现,和CLH略有不同,同步器的開始提到了事实上现依赖于一个FIFO队列。那么队列中的元素Node就是保存着线程引用和线程状态的容器,每一个线程对同步器的訪问。都能够看做是队列中的一个节点。Node的主要包括下面成员变量:
    [java] view plaincopy在CODE上查看代码片派生到我的代码片
    1. Node {  
    2.     int waitStatus;  
    3.     Node prev;  
    4.     Node next;  
    5.     Node nextWaiter;  
    6.     Thread thread;  
    7. }  

    成员变量主要负责保存该节点的线程引用,同步等待队列(下面简称sync队列)的前驱和后继节点。同一时候也包括了同步状态。

    节点成为sync队列和condition队列构建的基础,在同步器中就包括了sync队列。同步器拥有三个成员变量:sync队列的头结点head、sync队列的尾节点tail和状态state。

    对于锁的获取,请求形成节点,将其挂载在尾部。而锁资源的转移(释放再获取)是从头部開始向后进行。对于同步器维护的状态state,多个线程对其的获取将会产生一个链式的结构。



    3.AQS实现分析

    3.1 概述

    同步器的设计包括获取和释放两个操作:
    获取操作步骤例如以下:
    if(尝试获取成功){
        return;
     }else{
         增加等待队列;park自己
    }

    释放操作:
    if(尝试释放成功){
        unpark等待队列中第一个节点
    }else{
        return false
    }

    要满足以上两个操作。须要下面3点来支持:
    1、原子操作同步状态;
    2、堵塞或者唤醒一个线程;

    3、内部应该维护一个队列。

    AQS的实现採用了模板设计模式,在AbstractQueuedSynchronizer类中,定义了

    [java] view plaincopy在CODE上查看代码片派生到我的代码片
    1. protected boolean tryAcquire(int arg);  
    2. protected int tryAcquireShared(int arg);  
    3. protected boolean tryRelease(int arg);  
    4. protected boolean tryReleaseShared(int arg)。  
    等未详细实现的方法。子类须要实现这些方法。来完毕不同的同步器实现。

    3.2 获取、释放锁操作

    3.2.1 获取操作

    获取锁操作的代码例如以下:

    [java] view plaincopy在CODE上查看代码片派生到我的代码片
    1. public final void acquire(int arg) {  
    2.         if (!tryAcquire(arg) &&     
    3.             acquireQueued(addWaiter(Node.EXCLUSIVE), arg))  
    4.             selfInterrupt(); //假设获取锁的过程中有中断,则在获取操作完毕后,响应中断。  
    5.     }  

    上述逻辑主要包含:
    1. 尝试获取(调用tryAcquire更改状态,须要保证原子性);
    在tryAcquire方法中使用了同步器提供的对state操作的方法,利用compareAndSet保证仅仅有一个线程可以对状态进行成功改动,而没有成功改动的线程将进入sync队列排队(通过调用addWaiter方法)

    addWaiter方法例如以下:

    [java] view plaincopy在CODE上查看代码片派生到我的代码片
    1. private Node addWaiter(Node mode) {  
    2.         Node node = new Node(Thread.currentThread(), mode);  
    3.         // Try the fast path of enq; backup to full enq on failure 首先在尾部高速加入。失败后再调用enq方法  
    4.         Node pred = tail;  
    5.         if (pred != null) {  
    6.             node.prev = pred;  
    7.             if (compareAndSetTail(pred, node)) {  //通过CAS操作,来进行入队操作  
    8.                 pred.next = node;  
    9.                 return node;  
    10.             }  
    11.         }  
    12.         enq(node);  
    13.         return node;  
    14.     }  

    2. 假设获取不到,将当前线程构造成节点Node并增加sync队列;
    进入队列的每一个线程都是一个节点Node,从而形成了一个双向队列。类似CLH队列,这样做的目的是线程间的通信会被限制在较小规模(也就是两个节点左右)。
    3. 再次尝试获取(调用acquireQueued方法),假设没有获取到那么将当前线程从线程调度器上摘下。进入等待状态。


    acquireQueued代码例如以下:

    [java] view plaincopy在CODE上查看代码片派生到我的代码片
    1. final boolean acquireQueued(final Node node, int arg) {  
    2.         boolean failed = true;  
    3.         try {  
    4.             boolean interrupted = false;  
    5.             for (;;) {  
    6.                 final Node p = node.predecessor();  
    7.                 if (p == head && tryAcquire(arg)) { //假设为头结点。且获取锁成功,则退出, Note:head事实上保存的是已经获取锁的节点。是哑节点  
    8.                     setHead(node);  
    9.                     p.next = null// help GC  
    10.                     failed = false;  
    11.                     return interrupted;  
    12.                 }  
    13.                 if (shouldParkAfterFailedAcquire(p, node) &&  
    14.                     parkAndCheckInterrupt()) //<span style="font-family: Arial;">parkAndCheckInterrupt的实现会调用park方法。使当前线程进入等待状态</span>  
    15.                     interrupted = true;  
    16.             }  
    17.         } finally {  
    18.             if (failed)  
    19.                 cancelAcquire(node);  
    20.         }  
    21.     }  
    上述逻辑主要包含:
    1. 获取当前节点的前驱节点;
    须要获取当前节点的前驱节点,而头结点所相应的含义是当前站有锁且正在执行。


    2. 当前驱节点是头结点而且可以获取状态。代表该当前节点占有锁。
    假设满足上述条件,那么代表可以占有锁。依据节点对锁占有的含义,设置头结点为当前节点。
    3. 否则进入等待状态。
    假设没有轮到当前节点执行,那么将当前线程从线程调度器上摘下。也就是进入等待状态。


    须要注意的是。acquire在运行过程中。并不能及时的对外界中断进行对应,必须等待运行完成之后,假设由外部中断。则进行中断响应。与acquire方法类似,acquireInterruptibly方法提供了获取状态能力,当然在无法获取状态的情况下会进入sync队列进行排队,这类似acquire。可是和acquire不同的地方在于它可以在外界对当前线程进行中断的时候提前结束获取状态的操作。换句话说,就是在类似synchronized获取锁时。外界可以对当前线程进行中断,而且获取锁的这个操作可以响应中断并提前返回。

    一个线程处于synchronized块中或者进行同步I/O操作时。对该线程进行中断操作,这时该线程的中断标识位被设置为true,可是线程依然继续运行。


    3.2.2 释放操作

    释放操作代码例如以下:
    [java] view plaincopy在CODE上查看代码片派生到我的代码片
    1. public final boolean release(int arg) {  
    2.         if (tryRelease(arg)) {  
    3.             Node h = head;  
    4.             if (h != null && h.waitStatus != 0)  
    5.                 unparkSuccessor(h);  
    6.             return true;  
    7.         }  
    8.         return false;  
    9.     }  
    上述逻辑主要包含:
    1. 尝试释放状态;
    tryRelease可以保证原子化的将状态设置回去,当然须要使用compareAndSet来保证。

    假设释放状态成功过之后。将会进入后继节点的唤醒过程。


    2. 唤醒当前节点的后继节点所包括的线程。


    通过LockSupport的unpark方法将休眠中的线程唤醒,让其继续acquire状态。

  • 相关阅读:
    [LeetCode]Linked List Cycle
    ACM 整数划分(四)
    ACM 子串和
    ACM 阶乘之和
    ACM 组合数
    ACM 阶乘的0
    ACM 比大小
    ACM 擅长排列的小明
    ACM 重建二叉树
    cocos2dx 魔塔项目总结(一)
  • 原文地址:https://www.cnblogs.com/zfyouxi/p/5135149.html
Copyright © 2020-2023  润新知