并发编程(七):AQS框架(实现锁机制的AQS框架你了解多少)

AQS(一种实现锁机制的框架,也可以说一些基类组合构建锁和同步器)

 

一:基本概念

AQS框架:全称AbstractQueuedSynchronizer。(抽象队列同步)

AQS是一个用来构建锁和同步器的(各种基类)框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器。

总结:手撕核心类AbstractQueuedSynchronizer,你会对AQS认识更加深刻,然后Count

 

二:AQS基本原理的详细分析

类图分析(下面会详细说明)

并发编程(七):AQS框架(实现锁机制的AQS框架你了解多少)_第1张图片

 

并发编程(七):AQS框架(实现锁机制的AQS框架你了解多少)_第2张图片

基本锁的特性:

加锁、解锁

以ReentrantLock为例:

可重入锁,公平锁,非公平锁

可重入锁,公平锁,非公平锁概念自行知乎

上述概念结合源码进行分析(以ReentrantLock为例)

private final Sync sync;
// ReentrantLock的加锁方法
public void lock() {
    sync.lock();
}
//ReentrantLock 释放锁
public void unlock() {
    sync.release(1);
}
// 以下代码是ReentrantLock实现公平锁和非公平锁的构造方法
/**
 * Creates an instance of {@code ReentrantLock}.
 * This is equivalent to using {@code ReentrantLock(false)}.
 * 默认非公平锁
 */
public ReentrantLock() {
    sync = new NonfairSync();
}

/**
 * Creates an instance of {@code ReentrantLock} with the
 * given fairness policy.
 * 公平锁
 * @param fair {@code true} if this lock should use a fair ordering policy
 */
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

实现公平,非公平的详细过程后续会详细讲到

 

AbstractQueuedSynchronizer核心类

/**
 * Acquires in exclusive mode, ignoring interrupts.  Implemented
 * by invoking at least once {@link #tryAcquire},
 * returning on success.  Otherwise the thread is queued, possibly
 * repeatedly blocking and unblocking, invoking {@link
 * #tryAcquire} until success.  This method can be used
 * to implement method {@link Lock#lock}.
 *
 * @param arg the acquire argument.  This value is conveyed to
 *        {@link #tryAcquire} but is otherwise uninterpreted and
 *        can represent anything you like.
 */
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
    // 唤醒queue中的线程;addWaiter向队列中添加线程
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
...
// 继承类重写该方法,自定义尝试获取锁操作
protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}
...
/**
 * Acquires in exclusive uninterruptible mode for thread already in
 * queue. Used by condition wait methods as well as acquire.
 * 
 * @param node the node
 * @param arg the acquire argument
 * @return {@code true} if interrupted while waiting
 */
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);
    }
}
...
/**
 * Creates and enqueues node for current thread and given mode.
 * 线程在Node的排队方式
 * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
 * @return the new node
 */
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)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

获取锁:

acquire获取锁方法:

tryAcquire(arg)先尝试获取锁,获取失败则acquireQueued(addWaiter(Node.EXCLUSIVE), arg) 进行排队; addWaiter(Node.EXCLUSIVE)将当前线程放进Node(双向链表)的表尾。

 

那既然是个队列,那线程在队列中如何排队?

 

一个队列来对线程进行排队和管理,要知道以下信息

  1. 线程,肯定要知道我是哪个线程(因为连哪个线程都不知道,你还排啥队,管理个球球?)
  2. 队列中线程状态,既然知道是哪一个线程,肯定还要知道线程当前处在什么状态,是已经取消了“获锁”请求,还是在“”等待中”,或者说“即将得到锁”
  3. 前驱和后继线程,因为是一个等待队列,那么也就需要知道当前线程前面的是哪个线程,当前线程后面的是哪个线程(因为当前线程释放锁以后,理当立马通知后继线程去获取锁)

类图如下:

并发编程(七):AQS框架(实现锁机制的AQS框架你了解多少)_第3张图片

 

/** 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;
// Node类静态常量---线程状态
/** 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;

// volatile修饰保证其在多线程情况下的可见性
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;

Node nextWaiter;

 

概念介绍:

线程的2种等待模式:

  • SHARED:表示线程以共享的模式等待锁(如ReadLock)
  • EXCLUSIVE:表示线程以互斥的模式等待锁(如ReentrantLock),互斥就是一把锁只能由一个线程持有,不能同时存在多个线程使用同一个锁

 

线程在队列中的状态:

  • CANCELLED:值为1,表示线程的获锁请求已经“取消”
  • SIGNAL:值为-1,表示该线程一切都准备好了,就等待锁空闲出来给我(阻塞)
  • CONDITION:值为-2,表示线程等待某一个条件(Condition)被满足
  • PROPAGATE:值为-3,当线程处在“SHARED”模式时,该字段才会被使用上

 

成员变量:

  • waitStatus:该int变量表示线程在队列中的状态,其值就是上述提到的CANCELLED、SIGNAL、CONDITION、PROPAGATE
  • prev:该变量类型为Node对象,表示该节点的前一个Node节点(前驱)
  • next:该变量类型为Node对象,表示该节点的后一个Node节点(后继)
  • thread:该变量类型为Thread对象,表示该节点的代表的线程
  • nextWaiter:该变量类型为Node对象,表示等待condition条件的Node节点(暂时不用管它,不影响我们理解主要知识点)

 

等待中的线程如何感知到锁空闲并获得锁?

 

上文提到的acquireQueued方法就是把放入队列中的这个线程不断进行“获锁”,直到它“成功获锁”或者“不再需要锁(如被中断)“。

并发编程(七):AQS框架(实现锁机制的AQS框架你了解多少)_第4张图片

注:盗张网上的图

上图解释说明:(1)获取当前Node的前驱Node -->(2)前驱Node是否是head节点,并且是否获锁成功--->(3)是则删除head节点,将当前节点设置为head节点,不是则判断是否要阻塞当前node,如果不阻塞继续执行(1)操作。

 

“不管公平还是非公平模式下,ReentrantLock对于排队中的线程都能保证,排在前面的一定比排在后面的线程优先获得锁”但是,非公平模式不保证“队列中的第一个线程一定就比新来的(未加入到队列)的线程优先获锁”因为队列中的第一个线程尝试获得锁时,可能刚好来了一个线程也要获取锁,而这个刚来的线程都还未加入到等待队列,此时两个线程同时随机竞争,很有可能,队列中的第一个线程竞争失败(而该线程等待的时间其实比这个刚来的线程等待时间要久)。

 

细看上图会发现:“如果就是那么不恰巧,就是不符合这个唯一跳出循环的条件(p是head节点,并且当前线程获锁成功)”,那就一直在循环里面空跑了吗!那CPU使用率不就会飙升?!

答:将线程阻塞,就不会出现死循环了。

 

什么时候线程需要被阻塞呢?

 

并发编程(七):AQS框架(实现锁机制的AQS框架你了解多少)_第5张图片

关键在于CAS会设置pred节点的状态为SIGNAL;则第二次循环时线程就会被ws == SIGNAL判断成功而阻塞掉。

 

释放锁时“唤醒”被阻塞着的线程去“拿锁”

 

// ReentrantLock类释放锁
public void unlock() {
    sync.release(1);
}
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

调用的是AbstractQueuedSynchronizer的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)
        // CAS机制
        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.
     */
     //如果head节点的下一个节点它是null或者已经被cancelled了(status>0)
    //那么就从队列的尾巴往前找,找到一个最前面的并且状态不是cancelled的线程
    //至于为什么要从后往前找,不是从前往后找,谁能跟我说一下,这点我也不知道为什么
    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;
    }
    // 如果node存在则唤醒线程
    if (s != null)
        LockSupport.unpark(s.thread);
}

/**
 * CAS waitStatus field of a node.
 * 多线程下保证waitStatus的原子性
 */
private static final boolean compareAndSetWaitStatus(Node node,
                                                     int expect,
                                                     int update) {
    return unsafe.compareAndSwapInt(node, waitStatusOffset,
                                    expect, update);
}
// 唤醒线程 unsafe类
public static void unpark(Thread thread) {
    if (thread != null)
        UNSAFE.unpark(thread);
}

 

如何实现可重入

并发编程(七):AQS框架(实现锁机制的AQS框架你了解多少)_第6张图片

 

三:AQS底层使用了模板方法模式

 

同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用):

  1. 使用者继承AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对于共享资源state的获取和释放)
  2. 将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。

 

自定义同步器时需要重写下面几个AQS提供的模板方法:

isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。 tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。 tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。 tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。 tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。

 

四:AQS 组件总结

 

  • Semaphore(信号量)-允许多个线程同时访问: synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。
  • CountDownLatch (倒计时器): CountDownLatch是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。
  • CyclicBarrier(循环栅栏): CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await()方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。

 

今天被docker部署分布式项目弄得心态爆炸,老婆镇楼,开心下,明天继续搬砖

并发编程(七):AQS框架(实现锁机制的AQS框架你了解多少)_第7张图片

你可能感兴趣的:(线程安全,并发编程,多线程)