JUC探险-6、Lock & AQS

18 篇文章 0 订阅

文章目录

一、:Lock初步认识

  锁是用来控制多个线程访问共享资源的方式。一般来说,一个锁能够防止多个线程同时访问共享资源。在Lock接口出现之前,java程序主要是靠synchronized关键字实现锁功能的,而JDK1.5之后,并发包中增加了lock接口,它提供了与synchronized一样的锁功能。虽然它失去了像synchronize关键字隐式加锁解锁的便捷性,但是却拥有了锁获取和释放的可操作性,可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。

  需要注意的是synchronized同步块执行完成或者遇到异常是锁会自动释放,而lock必须调用unlock()方法释放锁,因此一般在finally块中释放锁。

  Lock提供的方法

public interface Lock {
	// 获取锁
	void lock();
	
	// 获取锁(过程中能够响应中断)
	void lockInterruptibly() throws InterruptedException;
	
	// 非阻塞式响应中断能立即返回,获取锁返回true,反之返回fasle
	boolean tryLock();
	
	// 超时获取锁,在超时内或者未中断的情况下能够获取锁
	boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
	
	// 释放锁
	void unlock();
	
	// 获取与lock绑定的等待通知组件,当前线程必须获得了锁才能进行等待,进行等待时会先释放锁,当再次获取锁时才能从等待中返回
	Condition newCondition();
}

  进入大多数锁的实现,我们会发现一个很明显的特点:大多数方法的实现实际上都是调用了其静态内存类Sync中的方法,而Sync类继承了AbstractQueuedSynchronizer(AQS)。可以看出,要想理解Lock原理,关键核心在于对队列同步器AbstractQueuedSynchronizer(简称同步器)的理解。


二、:AQS

  官方注释解释:

Provides a framework for implementing blocking locks and related
synchronizers (semaphores, events, etc) that rely on
first-in-first-out (FIFO) wait queues.  This class is designed to
be a useful basis for most kinds of synchronizers that rely on a
single atomic {@code int} value to represent state. Subclasses
must define the protected methods that change this state, and which
define what that state means in terms of this object being acquired
or released.  Given these, the other methods in this class carry
out all queuing and blocking mechanics. Subclasses can maintain
other state fields, but only the atomically updated {@code int}
value manipulated using methods {@link #getState}, {@link
#setState} and {@link #compareAndSetState} is tracked with respect
to synchronization.

<p>Subclasses should be defined as non-public internal helper
classes that are used to implement the synchronization properties
of their enclosing class.  Class
{@code AbstractQueuedSynchronizer} does not implement any
synchronization interface.  Instead it defines methods such as
{@link #acquireInterruptibly} that can be invoked as
appropriate by concrete locks and related synchronizers to
implement their public methods.

  同步器是用来构建锁和其他同步组件的基础框架,它的实现主要依赖一个int成员变量来表示同步状态以及通过一个FIFO队列(CLH队列)构成等待队列。它的子类必须重写AQS的几个protected修饰的用来改变同步状态的方法,其他方法主要是实现了排队阻塞机制。状态的更新使用getState()、setState()以及compareAndSetState()这三个方法。
  子类被推荐定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态的获取和释放方法来供自定义同步组件的使用,同步器既支持独占式获取同步状态,也可以支持共享式获取同步状态,这样就可以方便的实现不同类型的同步组件。
  同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。可以这样理解二者的关系:锁是面向使用者,它定义了使用者与锁交互的接口,隐藏了实现细节;同步器是面向锁的实现者,它简化了锁的实现方式,屏蔽了同步状态的管理,线程的排队,等待和唤醒等底层操作。锁和同步器很好的隔离了使用者和实现者所需关注的领域。

  AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

  ①AQS的设计模式

    AQS的设计是使用模板方法设计模式,它将一些方法开放给子类进行重写,而同步器给同步组件所提供模板方法又会重新调用被子类所重写的方法。

    举个例子,AQS中需要重写的方法tryAcquire:

protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

    ReentrantLock中NonfairSync(继承AQS)会重写该方法为:

protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}

    而AQS中的模板方法acquire():

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

    这里会调用tryAcquire()方法,而此时当继承AQS的NonfairSync调用模板方法acquire()时就会调用已经被NonfairSync重写的tryAcquire()方法。

    这就是使用AQS的方式,在明确这点后会对lock的实现理解有很大的提升。可以归纳总结为这么几点:
      ●同步组件(这里不仅仅指锁,还包括CountDownLatch等)的实现依赖于同步器AQS,在同步组件实现中,使用AQS的方式被推荐定义继承AQS的静态内存类。
      ●AQS采用模板方法进行设计,AQS的protected修饰的方法需要由继承AQS的子类进行重写实现,当调用AQS的子类的方法时就会调用被重写的方法。
      ●AQS负责同步状态的管理,线程的排队、等待和唤醒这些底层操作,而Lock等同步组件主要专注于实现同步语义。
      ●在重写AQS的方式时,使用AQS提供的getState()、setState()、compareAndSetState()方法进行修改同步状态。

    AQS可重写的方法如下图(摘自《java并发编程的艺术》一书):

    在实现同步组件时AQS提供的模板方法如下图:

    1、AQS提供的模板方法可以分为三类

      ●独占式获取与释放同步状态。
      ●共享式获取与释放同步状态。
      ●查询同步队列中等待线程情况。

    同步组件通过AQS提供的模板方法实现自己的同步语义。

    在同步组件的实现上主要是利用了AQS,而AQS“屏蔽”了同步状态的修改、线程排队等底层实现,通过AQS的模板方法可以很方便的给同步组件的实现者进行调用。而针对用户来说,只需要调用同步组件提供的方法来实现并发编程即可。

    2、在新建一个同步组件时需要把握的两个关键点

      ●实现同步组件时推荐定义继承AQS的静态内存类,并重写需要的protected修饰的方法。
      ●同步组件语义的实现依赖于AQS的模板方法,而AQS模板方法又依赖于被AQS的子类所重写的方法。

    3、同步组件以及AQS的功能实际上被切分成各自的两部分

      ●同步组件实现者的角度:
        通过可重写的方法:独占式: tryAcquire()(独占式获取同步状态),tryRelease()(独占式释放同步状态);共享式 :tryAcquireShared()(共享式获取同步状态),tryReleaseShared()(共享式释放同步状态);告诉AQS怎样判断当前同步状态是否成功获取或者是否成功释放。同步组件专注于对当前同步状态的逻辑判断,从而实现自己的同步语义。
      ●AQS的角度:
        对AQS来说,只需要同步组件返回的true和false即可,因为AQS会对true和false会有不同的操作,true会认为当前线程获取同步组件成功直接返回,而false的话就AQS也会将当前线程插入同步队列等一系列的方法。

    总的来说,同步组件通过重写AQS的方法实现自己想要表达的同步语义,而AQS只需要同步组件表达的true和false即可,AQS会针对true和false不同的情况做不同的处理。

  ②同步队列

    当共享资源被某个线程占有,其他请求该资源的线程将会阻塞,从而进入同步队列。

    就数据结构而言,队列的实现方式无外乎两者一是通过数组的形式,另外一种则是链表的形式。AQS中的同步队列是通过链表方式进行实现的。

    在AQS有一个静态内部类Node,其中有这样一些属性:

static final class Node {
	// 节点状态
	volatile int waitStatus;
	
	// 当前节点/线程的前驱节点
	volatile Node prev;
	
	// 当前节点/线程的后继节点
	volatile Node next;
	
	// 加入同步队列的线程引用
	volatile Thread thread;
	
	// 等待队列中的下一个节点
	Node nextWaiter;
}

    节点有以下这些状态:

static final class Node {
	static final Node SHARED = new Node();
	static final Node EXCLUSIVE = null;
	
	// 节点从同步队列中取消
	static final int CANCELLED =  1;
	
	// 后继节点的线程处于等待状态,如果当前节点释放同步状态会通知后继节点,使得后继节点的线程能够运行
	static final int SIGNAL    = -1;
	
	// 当前节点进入等待队列中
	static final int CONDITION = -2;
	
	// 表示下一次共享式同步状态获取将会无条件传播下去
	static final int PROPAGATE = -3;

	// 还有一个值0,代表初始状态
}

    后文会用到的构造方法:

Node(Thread thread, Node mode) {     // Used by addWaiter
    this.nextWaiter = mode;
    this.thread = thread;
}

    不难看出,每个节点拥有其前驱和后继节点,说明这是一个双向队列

    AQS中还有三个重要的成员变量:

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
	// 头节点
	private transient volatile Node head;
	
	// 尾节点
	private transient volatile Node tail;
	
	// 同步状态,使用volatile修饰保证线程可见性
	private volatile int state;
}

    由此可见:AQS实际上通过头尾指针来管理同步队列,同时实现包括获取锁失败的线程进行入队,释放锁时对同步队列中的线程进行通知等核心方法。

    通过对源码的理解以及做实验的方式,我们可以清楚的知道以下两点:
      ●节点的数据结构,即AQS的静态内部类Node,节点的等待状态等信息。
      ●同步队列是一个双向队列,AQS通过持有头尾指针管理同步队列。

    那么,节点如何进行入队和出队是怎样做的呢?实际上这对应着锁的获取和释放两个操作:获取锁失败进行入队操作,获取锁成功进行出队操作。

  ③独占式锁

    1、独占式锁常用方法

// 独占式获取同步状态,如果获取失败则插入同步队列进行等待
void acquire(int arg)
// 与acquire方法相同,但在同步队列中进行等待的时候可以检测中断
void acquireInterruptibly(int arg)
// 在acquireInterruptibly基础上增加了超时等待功能,在超时时间内没有获得同步状态返回false
boolean tryAcquireNanos(int arg, long nanosTimeout)
// 释放同步状态,该方法会唤醒在同步队列中的下一个节点
boolean release(int arg)

    2、独占锁的获取——acquire()方法

public final void acquire(int arg) {
	// 先看同步状态是否获取成功,如果成功则方法结束返回
	// 若失败则先调用addWaiter()方法再调用acquireQueued()方法
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

      acquire根据当前获得同步状态成功与否做了两件事情:
        ●成功,则方法结束返回。
        ●失败,则先调用addWaiter(),然后再调用acquireQueued()方法。

      Ⅰ、addWaiter()方法
private Node addWaiter(Node mode) {
	// 1.将当前线程构建成Node类型
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    // 2.当前尾节点是否为null
	Node pred = tail;
	// 快速尝试
    if (pred != null) {
    	// 2.2将当前节点尾插入的方式插入同步队列中
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 2.1当前同步队列尾节点为null,说明当前线程是第一个加入同步队列进行等待的线程;或者快速尝试失败
    enq(node);
    return node;
}

        程序的逻辑主要分为两个部分:
          ●当前同步队列的尾节点为null,调用方法enq()插入。
          ●当前同步队列的尾节点不为null,则采用尾插入(compareAndSetTail()方法)的方式入队。(快速尝试)

        这里还有一个小插曲:如果 if (compareAndSetTail(pred, node))执行失败,此时不会返回结果,会继续执行到enq()方法。同时很明显compareAndSetTail是一个CAS操作,通常来说如果CAS操作失败会继续自旋(死循环)进行重试。
        因此,我们猜测,enq()方法可能承担两个任务:
          ●处理当前同步队列尾节点为null时进行入队操作。
          ●如果CAS尾插入节点失败后负责自旋进行尝试。

      ⅰ、enq()方法
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            // 1.构造头结点
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            // 2.尾插入,CAS操作失败自旋尝试
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

        如果尾节点为null进入方法,会在第1步中先创建头结点,说明同步队列是带头结点的链式存储结构。带头结点与不带头结点的队列相比,会在入队和出队的操作中获得更大的便捷性,因此同步队列选择了带头结点的链式存储结构。尾节点为null,说明当前线程是第一次插入同步队列,因此此时带头节点的队列初始化头节点。
        第2步中compareAndSetTail(t, node)方法会利用CAS操作设置尾节点,如果CAS操作失败会在for (;;)for死循环中不断尝试,直至成功return返回为止。

        因此,对enq()方法的操作总结如下:
          ●在当前线程是第一个加入同步队列时,调用compareAndSetHead(new Node())方法,完成链式队列的头结点的初始化
          ●自旋不断尝试CAS尾插入节点直至成功为止。

      Ⅱ、acquireQueued()方法

        这个方法的作用就是排队获取锁的过程:

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            // 1.获得当前节点的先驱节点
            final Node p = node.predecessor();
            // 2.当前节点能否获取独占式锁				
            // 2.1如果当前节点的先驱节点是头结点并且成功获取同步状态,即可以获得独占式锁
            if (p == head && tryAcquire(arg)) {
            	// 队列头指针用指向当前节点
                setHead(node);
            	// 释放前驱节点
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 2.2获取锁失败,线程进入等待状态等待获取独占式锁
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

        整体来看这又是一个自旋的过程,代码首先获取当前节点的先驱节点,如果先驱节点是头结点并且成功获得同步状态的时候,当前节点所指向的线程能够获取锁。反之,获取锁失败进入等待状态。

        其中,获取锁成功,出队的代码是:

// 队列头指针用指向当前节点
setHead(node);
// 释放前驱节点
p.next = null; // help GC
failed = false;
return interrupted;

        这里,setHead()方法的代码是:

private void setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}

        可以看出,此处将当前节点通过setHead()方法设置为队列的头结点,然后将之前的头结点的next域设置为null并且pre域也为null,即与队列断开,无任何引用方便GC时能够将内存进行回收。

        当获取锁失败的时候会调用shouldParkAfterFailedAcquire()方法和parkAndCheckInterrupt()方法。

      ⅰ、shouldParkAfterFailedAcquire()方法
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        // 前驱节点设置为SIGNAL状态,在释放锁的时候会唤醒后继节点,所以后继节点(也就是当前节点)现在可以阻塞自己
        return true;
    if (ws > 0) {
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        do {
        	// 前驱节点状态为CANCELLED,向前遍历,更新当前节点的前驱为往前第一个非CANCELLED节点
        	// 当前线程会之后会再次回到循环并尝试获取锁
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * 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.
         */
        // 节点状态为0或者PROPAGATE时,设置前驱的等待状态为SIGNAL
        // 当前线程会之后会再次回到循环并尝试获取锁
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

        这里的主要逻辑是通过compareAndSetWaitStatus()方法,使用CAS将节点状态由INITIAL设置成SIGNAL,表示当前线程阻塞。当节点状态设置失败则最终方法返回false,然后会在acquireQueued()方法自旋,会继续重试,直至设置节点状态位为SIGNAL时,最终方法返回true,才会执行parkAndCheckInterrupt()方法。

      ⅱ、parkAndCheckInterrupt()方法
private final boolean parkAndCheckInterrupt() {
    // 使得该线程阻塞
    LockSupport.park(this);
    return Thread.interrupted();
}

        这里的主要逻辑是调用LookSupport.park()方法,该方法是用来阻塞当前线程的。

      代码全部走完一遍后,可以总结得到acquireQueued()在自旋过程中主要完成了两件事情:
        ●如果当前节点的前驱节点是头节点,并且能够获得同步状态的话,当前线程能够获得锁该方法执行结束退出。
        ●获取锁失败的话,先将节点状态设置成SIGNAL,然后调用LookSupport.park()方法使得当前线程阻塞。

      经过上面的分析,独占式锁的获取过程也就是acquire()方法的执行流程如下图所示:

    3、独占锁的释放——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()返回true),则会执行if块中的代码,当head指向的头结点不为null,并且该节点的状态值不为0的话才会执行unparkSuccessor()方法。

      Ⅰ、unparkSuccessor()方法
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)
    	// 尝试将Node的状态置为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;
        // 从tail开始向前找到Node之后最近的非CANCELLED节点
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
		// 后继节点不为null时唤醒该线程
        LockSupport.unpark(s.thread);
}

        如果状态值为负数,将其设置为0。然后获取头节点的后继节点。如果后继节点为null或者状态为CANCELLED,倒序寻找符合条件的后继节点。当后继节点不为null的时候会调用LookSupport.unpark()方法,该方法会唤醒该节点的后继节点所包装的线程。因此,每一次锁释放后就会唤醒队列中该节点的后继节点所引用的线程,从而进一步可以佐证获得锁的过程是一个FIFO(先进先出)的过程。

        此处的判断是否多此一举?思考之后比较耐人寻味,猜测可能有以下情况:
          ●node某时刻为tail。此时有新线程通过addWaiter中的if分支或者enq方法添加了自己。然后compareAndSetTail成功。此时这里的Node s = node.next读出来s == null,但事实上node已经不是tail,它有后继了。
        从此处也可以看出jdk代码的严谨。

      经过上面的分析,独占式锁的释放过程也就是release()方法的执行流程如下图所示:

    对于独占式锁的获取和释放的过程以及同步队列,可以做如下总结:
      ●线程获取锁失败,线程被封装成Node进行入队操作,核心方法在于addWaiter()和enq(),同时enq()完成对同步队列的头结点初始化工作以及CAS操作失败的重试。
      ●线程获取锁是一个自旋的过程,当且仅当当前节点的前驱节点是头结点并且成功获得同步状态时,节点出队即该节点引用的线程获得锁,否则,当不满足条件时就会调用LookSupport.park()方法使得线程阻塞。
      ●释放锁的时候会唤醒后继节点。

    总体来说:在获取同步状态时,AQS维护一个同步队列,获取同步状态失败的线程会加入到队列中进行自旋;移除队列(或停止自旋)的条件是前驱节点是头结点并且成功获得了同步状态。在释放同步状态时,同步器会调用unparkSuccessor()方法唤醒后继节点。

    4、独占锁的可中断式获取——acquireInterruptibly()方法

      lock相较于synchronized有一些更方便的特性,比如能响应中断以及超时等待等特性。这里就可以看出其中一些。

public final void acquireInterruptibly(int arg) throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (!tryAcquire(arg))
		// 线程获取锁失败
        doAcquireInterruptibly(arg);
}

      在获取同步状态失败后就会调用doAcquireInterruptibly()方法。

      Ⅰ、doAcquireInterruptibly()方法
private void doAcquireInterruptibly(int arg) throws InterruptedException {
    // 将节点插入到同步队列中
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            // 获取锁出队
			if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return;
            }
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
				// 线程中断抛异常
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

        可以看到,代码与acquireQueued()方法逻辑几乎一致,唯一的区别是当parkAndCheckInterrupt()返回true时即线程阻塞时,该线程被中断,代码抛出被中断异常。

    5、独占锁的超时等待式获取——tryAcquireNanos()方法

      通过调用lock.tryLock(timeout,TimeUnit)方式达到超时等待获取锁的效果,该方法会在三种情况下才会返回:
        ●在超时时间内,当前线程成功获取了锁。
        ●当前线程在超时时间内被中断。
        ●超时时间结束,仍未获得锁返回false。

public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    return tryAcquire(arg) ||
    	// 实现超时等待的效果
        doAcquireNanos(arg, nanosTimeout);
}

      可以看出,核心逻辑在doAcquireNanos()方法中。

      Ⅰ、doAcquireNanos()方法
private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
    if (nanosTimeout <= 0L)
        return false;
    //1.根据超时时间和当前时间计算出截止时间
    final long deadline = System.nanoTime() + nanosTimeout;
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
			//2.当前线程获得锁出队列
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return true;
            }
			// 3.1重新计算超时时间
            nanosTimeout = deadline - System.nanoTime();
            // 3.2已经超时返回false
			if (nanosTimeout <= 0L)
                return false;
			// 3.3线程阻塞等待 
            if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold)
                LockSupport.parkNanos(this, nanosTimeout);
            // 3.4线程被中断抛出被中断异常
			if (Thread.interrupted())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

        程序逻辑同独占锁可响应中断式获取基本一致,唯一的不同在于获取锁失败后,对超时时间的处理上:在第1步会先计算出按照现在时间和超时时间计算出理论上的截止时间,比如当前时间是9h0min,超时时间是10min,那么根据deadline = System.nanoTime() + nanosTimeout计算出刚好达到超时时间时的系统时间就是9h0min+10min = 9h10min。然后根据deadline - System.nanoTime()就可以判断是否已经超时了。比如,当前系统时间是9h30min很明显已经超过了理论上的系统时间9h10min,deadline - System.nanoTime()计算出来就是一个负数,自然而然会在3.2步中的If判断之间返回false。如果还没有超时即3.2步中的if判断为true时就会继续执行3.3步通过LockSupport.parkNanos使得当前线程阻塞,同时在3.4步增加了对中断的检测,若检测出被中断直接抛出被中断异常。

        经过上面的分析,doAcquireNanos()方法的执行流程如下图所示:

  ④共享式锁

    1、共享式锁常用方法

// 共享式获取同步状态,与独占式的区别在于同一时刻有多个线程获取同步状态
void acquireShared(int arg)
// 在acquireShared方法基础上增加了能响应中断的功能
void acquireSharedInterruptibly(int arg)
// 在acquireSharedInterruptibly基础上增加了超时等待的功能
boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
// 共享式释放同步状态
boolean releaseShared(int arg)

    2、共享锁的获取——acquireShared()方法

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

      首先会调用tryAcquireShared()方法,tryAcquireShared()返回值是一个int类型。当返回值为大于等于0的时候方法结束说明获得成功获取锁,否则小于0时,表明获取同步状态失败即所引用的线程获取锁失败,失败后会执行doAcquireShared()方法,加入等待队列。

      Ⅰ、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);
    }
}

        逻辑几乎和独占式锁的获取一模一样,这里的自旋过程中能够退出的条件是:当前节点的前驱节点是头结点并且tryAcquireShared(arg)返回值大于等于0即能成功获得同步状态。此时会设置新的头结点,并且唤醒后继线程。

      ⅰ、setHeadAndPropagate()方法
private void setHeadAndPropagate(Node node, int propagate) {
	// 把当前的head记录下来,用于下面的条件检查
	Node h = head; // Record old head for check below
	setHead(node);
	// propagate是tryAcquireShared()的返回值,这是决定是否唤醒后继线程的依据之一
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        // 根据node的下一个节点共享来决定是否唤醒后继线程
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

        这里会根据tryAcquireShared()的返回结果、后继节点的共享状态等,决定是否执行唤醒后继线程的操作。

        这里的判断条件其实也很值得思考,可否只根据propagate > 0这一个条件进行判断呢?如果这样的话,其实就用不到PROPAGATE这一状态了。再来回顾PROPAGATE的引入,其实不是一开始就有的,而是某次迭代修复bug时加入的。PROPAGATE的引入是为了解决共享锁并发释放导致的线程挂起问题。如果没有这些后续判断,可能会出现以下情况:
          ●当releaseShared()方法有竞争的情况下,可能会有队列中处于等待状态的节点因为第一个线程完成释放被唤醒,第二个线程获取到锁,但还没设置好head,又有新线程释放锁。此时读到老的head状态为0导致释放但不唤醒,最终后一个等待线程既没有被释放线程唤醒,也没有被持锁线程唤醒。由此可见,仅仅靠tryAcquireShared()的返回值来决定是否要将唤醒传递下去是不充分的。

      ⅱ、doReleaseShared()方法

        这是共享锁中的核心唤醒函数,主要做的事情就是唤醒下一个线程或者设置PROPAGATE状态。当后继线程被唤醒后,会尝试获取共享锁,如果成功之后,则又会调用setHeadAndPropagate()方法,将唤醒操作传播下去。这个方法的作用是保障在acquire()和release()存在竞争的情况下,保证队列中处于等待状态的节点能够有办法被唤醒。

private void doReleaseShared() {
    for (;;) {
        Node h = head;
        // 判断队列中是否存在后继线程
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                unparkSuccessor(h);
            }
            // 如果h节点的状态为0,需要设置为PROPAGATE用以保证唤醒的传播
            else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        // 检查h是否仍然是head,如果不是的话需要再进行循环
        if (h == head)                   // loop if head changed
            break;
    }
}

        根据代码,将doReleaseShared()方法功能总结如下:
          ●在队列存在后继线程的情况下,唤醒后继线程。
          ●如果多线程同时释放共享锁,由于处在中间过程,读到head节点等待状态为0时,不会执行unparkSuccessor()方法,但为了保证唤醒能够正确稳固传递下去,设置节点状态为PROPAGATE
          ●最后再次检查头节点,如果头节点发生了变更,则继续循环。

    3、共享锁的释放——releaseShared()方法

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

      这里和独享锁类似,也是先尝试释放锁,成功后唤醒后继线程。使用的doReleaseShared()方法在上面也提到过。

    4、共享锁的可中断获取——acquireSharedInterruptibly()方法 & 超时等待获取——tryAcquireSharedNanos()方法

      此处与独占锁基本类似,可以参照独占锁代码和思想学习。

系列文章传送门:

JUC探险-1、初识概貌
JUC探险-2、synchronized
JUC探险-3、volatile
JUC探险-4、final
JUC探险-5、原子类
JUC探险-6、Lock & AQS
JUC探险-7、ReentrantLock
JUC探险-8、ReentrantReadWriteLock
JUC探险-9、Condition
JUC探险-10、常见工具、数据结构
JUC探险-11、ConcurrentHashMap
JUC探险-12、CopyOnWriteArrayList
JUC探险-13、ConcurrentLinkedQueue
JUC探险-14、ConcurrentSkipListMap
JUC探险-15、BlockingQueue
JUC探险-16、ThreadLocal
JUC探险-17、线程池

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值