JUC之AQS

JUCL下的锁和synchronized提供的锁的区别

1、锁的获取和释放是显示的靠程序员用代码来控制的,增加了灵活性,可以实现更加复杂的应用场景

2、尝试非堵塞式的获取锁

3、可中断的获取锁

4、可超时的获取锁

5、等待队列可按条件分类(Condition),这样可以实现更加精确的按组唤醒操作

AQS提供的能力

1、使用了一个int成员变量来表示同步状态

2、提供了一个FIFO队列来支持竞争线程的排队工作

3、通过模板方法设计模式来对外提供能力(子类覆盖某些步骤抽象方法)

4、定义三个方法来修改 同步状态值(getState()、setState(int newState)、compareAndSetState(int expect,int update))

5、支持独占式的获取同步状态,也支持共享式的获取同步状态

6、支持条件等待队列

AQS是什么

AQS = AbstractQueuedSynchronizer 队列同步器
AQS是JDK5.0 引入的一个抽象类,它对常见的lock场景进行了抽象,目的是对各种场景的lock提供基础支持,使锁实现起来更加容易(排队与唤醒功能)。
AQS位于java.util.concurrent.locks包中,可以看出它就是为lock服务的,ReentrantLock(独占式可重入锁)、Semaphore(共享式锁)、ReentrantReadWriteLock(混合式锁)、等都是基于它来实现的,还有CountDownLatch和ThreadPoolExecutor.Worker中也有AQS的影子。

工作过程

AQS通过内置的FIFO同步队列来完成资源获取线程的排队工作,如果当前线程获取同步状态(锁)失败时,AQS则会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,则会把节点中的线程唤醒,使其再次尝试获取同步状态。

AQS提供的方法

  • getState():返回同步状态的当前值;
  • setState(int newState):设置当前同步状态;
  • compareAndSetState(int expect, int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性;
  • tryAcquire(int arg):独占式获取同步状态,获取同步状态成功后,其他线程需要等待该线程释放同步状态才能获取同步状态;
  • tryRelease(int arg):独占式释放同步状态;
  • tryAcquireShared(int arg):共享式获取同步状态,返回值大于等于0则表示获取成功,否则获取失败;
  • tryReleaseShared(int arg):共享式释放同步状态;
  • isHeldExclusively():当前同步器是否在独占式模式下被线程占用,一般该方法表示是否被当前线程所独占;
  • acquire(int arg):独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则,将会进入同步队列等待,该方法将会调用可重写的tryAcquire(int arg)方法;
  • acquireInterruptibly(int arg):与acquire(int arg)相同,但是该方法响应中断,当前线程为获取到同步状态而进入到同步队列中,如果当前线程被中断,则该方法会抛出InterruptedException异常并返回;
  • tryAcquireNanos(int arg,long nanos):超时获取同步状态,如果当前线程在nanos时间内没有获取到同步状态,那么将会返回false,已经获取则返回true;
  • acquireShared(int arg):共享式获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待,与独占式的主要区别是在同一时刻可以有多个线程获取到同步状态;
  • acquireSharedInterruptibly(int arg):共享式获取同步状态,响应中断;
  • tryAcquireSharedNanos(int arg, long nanosTimeout):共享式获取同步状态,增加超时限制;
  • release(int arg):独占式释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒;
  • releaseShared(int arg):共享式释放同步状态;

通过对API的分析可以知道,AQS主要提供了:

1、独占式的获取与释放同步状态

2、共享式的获取与释放同步状态

3、查询同步队列中的线程排队情况

独占式锁:同一时刻只能由一个线程获取到,其它线程只能进入队列排队,等待。

共享式锁:同一时刻可以由多个线程持有。

混合式锁:当有读锁被其它线程持有期间,写锁线程只能堵塞等待前面所有的读锁释放,写锁堵塞之后进来的读锁只能排队到写锁线程后面,当读锁释放完毕后写锁获取成功,写锁释放完毕后,排在写锁后面的读锁可以同时获取到读锁。

AQS的原理

在基于AQS构建的同步器中,只能在一个时刻发生阻塞,从而降低上下文切换的开销,提高了吞吐量。同时在设计AQS时充分考虑了可伸缩性,因此J.U.C中所有基于AQS构建的同步器均可以获得这个优势。AQS的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态。AQS使用一个int类型的成员变量state来表示同步状态,当state>0时表示已经获取了锁,当state = 0时表示释放了锁。它提供了三个方法(getState()、setState(int newState)、compareAndSetState(int expect,int update))来对同步状态state进行操作,当然AQS可以确保对state的操作是安全的。

定义(源码分析)

public abstract class AbstractQueuedSynchronizer extends
    AbstractOwnableSynchronizer implements java.io.Serializable { 
    //等待队列的头节点
    private transient volatile Node head; //注:被transient修饰的变量不能序列化
    //等待队列的尾节点
    private transient volatile Node tail;
    //同步状态
    private volatile int state;
    protected final int getState() { return state;}
    protected final void setState(int newState) { state = newState;}
    ...
}

队列同步器AQS是用来构建锁或其他同步组件的基础框架,内部使用一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作,其中内部状态state,等待队列的头节点head和尾节点head,都是通过volatile修饰,保证了多线程之间的可见。在深入实现原理之前,我们先看看内部的FIFO队列是如何实现的。

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;
    //等待状态,这个和state是不一样的:有1,0,-1,-2,-3五个值
    volatile int waitStatus;
    volatile Node prev;//前驱节点
    volatile Node next;//后继节点
    volatile Thread thread;//等待锁的线程
    Node nextWaiter;//和节点是否共享有关
    ...
    }

在这里插入图片描述
黄色节点是默认head节点,其实是一个空节点,我觉得可以理解成代表当前持有锁的线程,每当有线程竞争失败,都是插入到队列的尾节点,tail节点始终指向队列中的最后一个元素。
每个节点中, 除了存储了当前线程,前后节点的引用以外,还有一个waitStatus变量,用于描述节点当前的状态。多线程并发执行时,队列中会有多个节点存在,这个waitStatus其实代表对应线程的状态:有的线程可能获取锁因为某些原因放弃竞争;有的线程在等待满足条件,满足之后才能执行等等。
一共有4种状态:

  1. CANCELLED = 1 取消状态该节点的线程可能由于超时或被中断而处于被取消(作废)状态,一旦处于这个状态,节点状态将一直处于CANCELLED(作废),因此应该从队列中移除.
  2. SIGNAL = -1 等待触发状态当前节点为SIGNAL时,后继节点会被挂起,因此在当前节点释放锁或被取消之后必须被唤醒(unparking)其后继结点.
  3. CONDITION = -2 等待条件状态该节点的线程处于等待条件状态,不会被当作是同步队列上的节点,直到被唤醒(signal),设置其值为0,重新进入阻塞状态
  4. PROPAGATE 状态需要向后传播等待队列是FIFO先进先出,只有前一个节点的状态为SIGNAL时,当前节点的线程才能被挂起

实现原理

子类重写tryAcquire和tryRelease方法通过CAS指令修改状态变量state。

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

线程获取锁过程

下列步骤中线程A和B进行竞争。

  1. 线程A执行CAS执行成功,state值被修改并返回true,线程A继续执行。
  2. 线程A执行CAS指令失败,说明线程B也在执行CAS指令且成功,这种情况下线程A会执行步骤3。
  3. 生成新Node节点node,并通过CAS指令插入到等待队列的队尾(同一时刻可能会有多个Node节点插入到等待队列中),如果tail节点为空,则将head节点指向一个空节点(代表线程B),具体实现如下:
private Node addWaiter(Node mode) {
    //把当前线程包装为node,设为独占模式
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    //如果tail不为空,把node插入末尾
    if (pred != null) {
        node.prev = pred;
        //此时可能有其他线程插入,所以重新判断tail
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        //此时可能有其他线程插入,所以重新判断tail是否为空
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}
  1. node插入到队尾后,该线程不会立马挂起,会进行自旋操作。因为在node的插入过程,线程B(即之前没有阻塞的线程)可能已经执行完成,所以要判断该node的前一个节点pred是否为head节点(代表线程B),如果pred == head,表明当前节点是队列中第一个“有效的”节点,因此再次尝试tryAcquire获取锁,
    a. 如果成功获取到锁,表明线程B已经执行完成,线程A不需要挂起。
    b. 如果获取失败,表示线程B还未完成,至少还未修改state值。进行步骤5。
  2. 前面我们已经说过只有前一个节点pred的线程状态为SIGNAL时,当前节点的线程才能被挂起。
    a. 如果pred的waitStatus == 0,则通过CAS指令修改waitStatus为Node.SIGNAL。
    b. 如果pred的waitStatus > 0,表明pred的线程状态CANCELLED,需从队列中删除。
    c. 如果pred的waitStatus为Node.SIGNAL,则通过LockSupport.park()方法把线程A挂起,并等待被唤醒,被唤醒后进入步骤6。
    具体实现如下:
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.
         */
        return true;
    if (ws > 0) {
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        do {
            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.
         */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}
  1. 线程每次被唤醒时,都要进行中断检测,如果发现当前线程被中断,那么抛出InterruptedException并退出循环。从无限循环的代码可以看出,并不是被唤醒的线程一定能获得锁,必须调用tryAccquire重新竞争,因为锁是非公平的,有可能被新加入的线程获得,从而导致刚被唤醒的线程再次被阻塞,这个细节充分体现了“非公平”的精髓。

线程释放锁过程

  1. 如果头结点head的waitStatus值为-1,则用CAS指令重置为0;
  2. 找到waitStatus值小于0的节点s,通过LockSupport.unpark(s.thread)唤醒线程。
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的后继节点不为空且不是作废状态,则唤醒这个后继节点,否则
从末尾开始寻找合适的节点,如果找到,则唤醒
private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    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;
    }
    if (s != null)
        LockSupport.unpark(s.thread);
}

总结

对获取独占式锁过程总结

AQS的模板方法acquire通过调用子类自定义实现的tryAcquire获取同步状态失败后
->将线程构造成Node节点(addWaiter)
->将Node节点添加到同步队列对尾(addWaiter)
->节点以自旋的方法获取同步状态(acquirQueued)。在节点自旋获取同步状态时,只有其前驱节点是头节点的时候才会尝试获取同步状态,如果该节点的前驱不是头节点或者该节点的前驱节点是头节点单获取同步状态失败,则判断当前线程需要阻塞,如果需要阻塞则需要被唤醒过后才返回。

释放锁过程总结

首先调用子类的tryRelease()方法释放锁,然后唤醒后继节点,在唤醒的过程中,需要判断后继节点是否满足情况,如果后继节点不为且不是作废状态,则唤醒这个后继节点,否则从tail节点向前寻找合适的节点,如果找到,则唤醒.

总结

1、AQS提供一个int变量作为 同步状态(锁的本质就是一个内存变量),提供了CAS的方式安全修改state变量;

2、AQS提供了排队能力,对没有获取到锁的线程提供一个FIFO队列,来管理这些线程,它通过Unsafe提供的CAS能力来实现多线程下无锁的入队和出队,达到一个高性能的目的;

3、AQS对于入队成功的线程采取 LockSupport.park(thread)提供的能力 使某个线程休眠,降低CPU消耗,在合适的时机再通过 LockSupport.unpark(thread)操作唤醒休眠的线程来继续运行;

4、AQS提供了条件队列的支持(仅支持独占模式),根据条件对象的不同来将等待的线程分类管理(多队列),这样的好处是可以更精细化的管理这些线程,避免不必要的唤醒,减少对CPU的消耗。

5、AQS对独占锁提供线程锁定来支持可重入的特性;

6、公平锁模式下如果前面有排队的就加入到队尾,按先后顺序获取锁;非公平锁模式下不管前面有没有排队的,都会先尝试获取锁,失败后再入队尾;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值