Java基础知识复习(四)

AQS

AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLock,Semaphore,其他的注入 ReentrantReadWriteLock,SynchronousQueue,FutureTask等等都是基于AQS的,我们也可以利用AQS构造出符合我们自己需求的同步器。

AQS核心思想

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

AQS实现原理

先来看一个AQS原理图:

在这里插入图片描述
AQS维护一个共享资源state,通过内置的FIFO来完成获取资源线程的排队工作。(这个内置的同步队列称为"CLH"队列)。该队列由一个一个的Node结点组成,每个Node结点维护一个prev引用和next引用,分别指向自己的前驱和后继结点。AQS维护两个指针,分别指向队列头部head和尾部tail。

当线程获取资源失败(比如当前资源已经被其他线程占用),会被构造成一个Node节点加入CLH队列中,同时当前线程会被阻塞在队列中(通过LockSupport.park实现,其实是等待态)。当持有锁的线程释放锁时,会唤醒后继结点,然后此结点线程继续加入到对同步状态的争夺中。

AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。

private volatile int state;

状态信息通过procted类型的getStatesetStatecompareAndSetState进行操作

AQS支持两种同步方式:

  • Exclusive

    (独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:

    • 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
    • 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
  • Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。

ReentrantReadWriteLock 可以看成是组合式,因为ReentrantReadWriteLock也就是读写锁允许多个线程同时对某一资源进行读。

不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。

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

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

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

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

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

默认情况下,每个方法都抛出 UnsupportedOperationException。 这些方法的实现必须是内部线程安全的,并且通常应该简短而不是阻塞。AQS类中的其他方法都是final ,所以无法被其他类使用,只有这几个方法可以被其他类使用。

AQS源码分析

Node结点

Node结点是AQS(AbstractQueuedSynchronizer)中的一个静态内部类,我们捡Node的几个重要属性来说一下

static final class Node {
  
    static final Node SHARED = new Node();

    static final Node EXCLUSIVE = null;

 /** waitStatus值,表示线程已被取消(等待超时或者被中断)*/
    static final int CANCELLED =  1;
  /** waitStatus值,表示后继线程需要被唤醒(unpaking)*/
    static final int SIGNAL    = -1;
 /**waitStatus值,表示结点线程等待在condition上,当被signal后,会从等待队列转移到同步到队列中 */
    static final int CONDITION = -2;
  /** waitStatus值,表示下一次共享式同步状态会被无条件地传播下去*/
    static final int PROPAGATE = -3;

   /** 等待状态,初始为0 */
    volatile int waitStatus;

   /**当前结点的前驱结点 */
    volatile Node prev;

    /** 当前结点的后继结点 */
    volatile Node next;

   /** 与当前结点关联的排队中的线程 */
    volatile Thread thread;
独占式(默认非公平锁)

获取锁:acquire()

一般调用Lock.lock()方法会直接代理到acquire()

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

大概的逻辑是:

调用使用者重写的acquire()方法,尝试获取锁,如果成功,返回true,后面的逻辑不再执行;否则,执行下面的方法逻辑。

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

tryAcquire()的一个实现(默认是非公平锁,想要使用公平锁就在创建Lock的时候在构造函数传入一个true来使用公平锁):

@ReservedStackAccess
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState(); // 获取状态
    if (c == 0) { // 如果c为0,说明现在这个资源没有线程占用
        if (compareAndSetState(0, acquires)) { // 通过CAS自旋的方式获取锁
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) { // 如果发现自己已经拥有该锁,简单的自增
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false; // 否则获取失败,返回false
}

如果获取锁失败,会构造一个Node解点,通过addWatiter()将此节点通过CAS自旋的方式,添加到CLH队列的尾部。

private Node addWaiter(Node mode) {
    Node node = new Node(mode);

    for (;;) {
        Node oldTail = tail;
        if (oldTail != null) {
            node.setPrevRelaxed(oldTail);
            if (compareAndSetTail(oldTail, node)) { // 也是通过CAS自旋的方式进行添加,防止被其他线程占用
                oldTail.next = node;
                return node;
            }
        } else {
            initializeSyncQueue(); // 如果当前tail为空或者线程调用CAS设置队尾失败,调用这个方法
        }
    }
}
private final void initializeSyncQueue() {
    Node h;
    if (HEAD.compareAndSet(this, null, (h = new Node()))) // 通过CAS自旋的方式来设置结点
        tail = h;
}

构造Node解点后,会放入队列中:

acquireQueued()内部也是一个死循环,只有前驱结点是头结点的结点,才有机会去tryAcquire();若tryAcquire()成功,表示获取同步状态成功,将此结点设置为头结点;若是非头结点,或者tryAcquire()失败,则进入shouldParkAfterFailedAcquire()去判断判断当前线程是否应该阻塞,若可以,调用parkAndCheckInterrupt()阻塞当前线程,直到被中断或者被前驱结点唤醒。若还不能休息,继续循环。

final boolean acquireQueued(final Node node, int arg) {
    boolean interrupted = false;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                return interrupted;
            }
              // 如果没有获取到同步状态,通过shouldParkAfterFailedAcquire判断是否应该阻塞,parkAndCheckInterrupt用来阻塞线程
            if (shouldParkAfterFailedAcquire(p, node))
                interrupted |= parkAndCheckInterrupt();
        }
    } catch (Throwable t) {
        cancelAcquire(node);
        if (interrupted)
            selfInterrupt();
        throw t;
    }
}

shouldParkAfterFailedAcquire()

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
          
            return true;
        if (ws > 0) {
          
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
        }
        return false;
    }

若shouldParkAfterFailedAcquire返回true,也就是当前结点的前驱结点为SIGNAL状态,则意味着当前结点可以放心休息,进入parking状态了。parkAncCheckInterrupt阻塞线程并处理中断。

private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);//使用LockSupport使线程进入阻塞状态,是本地的native方法
        return Thread.interrupted();// 线程是否被中断过
    }

简单总结:

  • 首先tryAcquire获取同步状态,成功则直接返回;否则,进入下一环节;
  • 线程获取同步状态失败,就构造一个结点,加入同步队列中,这个过程使用CAS保证线程安全;
  • 加入队列中的结点线程进入自旋状态,若是头结点,才有机会尝试去获取同步状态;否则,当其前驱结点的状态为SIGNAL,线程便可安心休息,进入阻塞状态,直到被中断或者被前驱结点唤醒。
  • 但是呢,由于默认实现的是非公平锁,即如果锁现在是释放状态,并不是直接由在队列中的线程排对获取锁,也有可能是上一个释放锁的线程继续又获取到锁或者是某一个新加入的线程取到锁,这对处于队列中的线程不公平(非公平锁原理)

释放锁:release()

当前线程执行完自己的方法逻辑,需要释放锁,一般调用Lock.unlock()方法会直接代理到release()

public void unlock() {
    sync.release(1);
}
public final boolean release(int arg) {
    if (tryRelease(arg)) { // 调用使用者重写的tryRelease方法,若成功,唤醒其后继结点,失败则返回false
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

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.compareAndSetWaitStatus(ws, 0); // 还是采用CAS自旋的方式设置状态

    /*
     * 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;
        for (Node p = tail; p != node && p != null; p = p.prev)
            if (p.waitStatus <= 0)
                s = p;
    }
    if (s != null)
        LockSupport.unpark(s.thread);
}

release()的逻辑相对简单,需要找到头结点的后继结点进行唤醒,若后继结点为空或处于CANCEL状态,从后向前遍历找寻一个正常的结点,唤醒其对应线程。

共享式

共享式:共享式地获取同步状态。对于独占式同步组件来讲,同一时刻只有一个线程能获取到同步状态,其他线程都得去排队等待,其待重写的尝试获取同步状态的方法tryAcquire返回值为boolean,这很容易理解;对于共享式同步组件来讲,同一时刻可以有多个线程同时获取到同步状态,这也是“共享”的意义所在。其待重写的尝试获取同步状态的方法tryAcquireShared返回值为int。

 protected int tryAcquireShared(int arg) {
        throw new UnsupportedOperationException();
    }

1.当返回值大于0时,表示获取同步状态成功,同时还有剩余同步状态可供其他线程获取;

2.当返回值等于0时,表示获取同步状态成功,但没有可用同步状态了;

3.当返回值小于0时,表示获取同步状态失败。

获取锁:acquireShared():

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

doAcquiredShared():

private void doAcquireShared(int arg) {
    final Node node = addWaiter(Node.SHARED);//构造一个共享结点,添加到同步队列尾部。若队列初始为空,先添加一个无意义的傀儡结点,再将新节点添加到队列尾部。
    boolean interrupted = false;//线程parking过程中是否被中断过
    try {
        for (;;) {
            final Node p = node.predecessor(); // 找到前驱结点
            if (p == head) {//头结点持有同步状态,只有前驱是头结点,才有机会尝试获取同步状态
                int r = tryAcquireShared(arg);//尝试获取同步装填
                if (r >= 0) {//r>=0,获取成功
                    setHeadAndPropagate(node, r);//获取成功就将当前结点设置为头结点,若还有可用资源,传播下去,也就是继续唤醒后继结点
                    p.next = null; // help GC
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node))//是否需要进入parking状态
                interrupted |= parkAndCheckInterrupt();//阻塞线程
        }
    } catch (Throwable t) {
        cancelAcquire(node);
        throw t;
    } finally {
        if (interrupted)
            selfInterrupt();
    }
}

大体逻辑与独占式的acquireQueued差距不大,只不过由于是共享式,会有多个线程同时获取到线程,也可能同时释放线程,空出很多同步状态,所以当排队中的头结点获取到同步状态,如果还有可用资源,会继续传播下去。

private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; // Record old head for check below
    setHead(node);
    
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

释放锁:releaseShared()

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) { // 尝试释放锁
        doReleaseShared();
        return true;
    }
    return false;
}

doReleaseShared:

private void doReleaseShared() {
    for (;;) {//死循环,共享模式,持有同步状态的线程可能有多个,采用循环CAS保证线程安全
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                if (!h.compareAndSetWaitStatus(Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     !h.compareAndSetWaitStatus(0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值