并发编程(中)

6、AQS(AbstractQueuedSynchronizer)

1、简介

AQS ,AbstractQueuedSynchronizer ,即抽象队列同步器。它是构建锁或者其他同步组件的基础框架(如 ReentrantLock、ReentrantReadWriteLock、Semaphore 等),J.U.C 并发包的作者(Doug Lea)期望它能够成为实现大部分同步需求的基础。

它是 J.U.C 并发包中的核心基础组件。

2、优势

AQS 解决了在实现同步器时涉及到的大量细节问题,例如获取同步状态、FIFO 同步队列。基于 AQS 来构建同步器可以带来很多好处。它不仅能够极大地减少实现工作,而且也不必处理在多个位置上发生的竞争问题。

在基于 AQS 构建的同步器中,只能在一个时刻发生阻塞,从而降低上下文切换的开销,提高了吞吐量。同时在设计 AQS 时充分考虑了可伸缩性,因此 J.U.C 中,所有基于 AQS 构建的同步器均可以获得这个优势。

3、同步状态

AQS 的主要使用方式是继承,子类通过继承同步器,并实现它的抽象方法来管理同步状态。

AQS 使用一个 int 类型的成员变量 state 来表示同步状态:

  • 当 state > 0 时,表示已经获取了锁。
  • 当 state = 0 时,表示释放了锁。

它提供了三个方法,来对同步状态 state 进行操作,并且 AQS 可以确保对 state 的操作是安全的:

  • #getState()
  • #setState(int newState)
  • #compareAndSetState(int expect, int update)

AQS的设计模式采用的是模板方法模式。子类通过继承的方式,实现它的抽象方法来管理同步状态。对于子类而言,它并没有太多的活要做,AQS已经提供了大量的模板方法来实现同步,主要是分为三类:
1、独占式获取和释放同步状态
2、共享式获取和释放同步状态
3、查询同步队列中的等待线程情况。

独占式

独占式,同一时刻,仅有一个线程持有同步状态。

独占式同步状态获取

#acquire(int arg) 方法,为 AQS 提供的模板方法。该方法为独占式获取同步状态,但是该方法对中断不敏感。也就是说,由于线程获取同步状态失败而加入到 CLH 同步队列中,后续对该线程进行中断操作时,线程不会从 CLH 同步队列中移除。代码如下:

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

调用 #tryAcquire(int arg) 方法,去尝试获取同步状态,获取成功则设置锁状态并返回 true ,否则获取失败,返回 false 。若获取成功,#acquire(int arg) 方法,直接返回,不用线程阻塞,自旋直到获得同步状态成功。

如果 #tryAcquire(int arg) 方法返回 false ,即获取同步状态失败,则调用 #addWaiter(Node mode) 方法,将当前线程加入到 CLH 同步队列尾部。并且, mode 方法参数为 Node.EXCLUSIVE ,表示独占模式。

调用 boolean #acquireQueued(Node node, int arg) 方法,自旋直到获得同步状态成功。
另外,该方法的返回值类型为 boolean ,当返回 true 时,表示在这个过程中,发生过线程中断。但是呢,这个方法又会清理线程中断的标识,所以在种情况下,需要调用 #selfInterrupt() 方法,恢复线程中断的标识,代码如下:

static void selfInterrupt() {
    Thread.currentThread().interrupt();
}

acquireQueued
boolean #acquireQueued(Node node, int arg) 方法,为一个自旋的过程,也就是说,当前线程(Node)进入同步队列后,就会进入一个自旋的过程,每个节点都会自省地观察,当条件满足,获取到同步状态后,就可以从这个自旋过程中退出,否则会一直执行下去。
在这里插入图片描述

 1final boolean acquireQueued(final Node node, int arg) {
 2:     // 记录是否获取同步状态成功
 3:     boolean failed = true;
 4:     try {
 5:         // 记录过程中,是否发生线程中断
 6:         boolean interrupted = false;
 7:         /*
 8:          * 自旋过程,其实就是一个死循环而已
 9:          */
10:         for (;;) {
11:             // 当前线程的前驱节点
12:             final Node p = node.predecessor();
13:             // 当前线程的前驱节点是头结点,且同步状态成功
14:             if (p == head && tryAcquire(arg)) {
15:                 setHead(node);
16:                 p.next = null; // help GC
17:                 failed = false;
18:                 return interrupted;
19:             }
20:             // 获取失败,线程等待--具体后面介绍
21:             if (shouldParkAfterFailedAcquire(p, node) &&
22:                     parkAndCheckInterrupt())
23:                 interrupted = true;
24:         }
25:     } finally {
26:         // 获取同步状态发生异常,取消获取。
27:         if (failed)
28:             cancelAcquire(node);
29:     }
30: }
独占式获取响应中断

AQS 提供了acquire(int arg) 方法,以供独占式获取同步状态,但是该方法对中断不响应,对线程进行中断操作后,该线程会依然位于CLH同步队列中,等待着获取同步状态。为了响应中断,AQS 提供了 #acquireInterruptibly(int arg) 方法。该方法在等待获取同步状态时,如果当前线程被中断了,会立刻响应中断,并抛出 InterruptedException 异常。

public final void acquireInterruptibly(int arg) throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (!tryAcquire(arg))
        doAcquireInterruptibly(arg);
}

它与 #acquire(int arg) 方法仅有两个差别

1、方法声明抛出 InterruptedException 异常。
2、在中断方法处不再是使用 interrupted 标志,而是直接抛出 InterruptedException 异常。

独占式超时获取

AQS 除了提供上面两个方法外,还提供了一个增强版的方法 #tryAcquireNanos(int arg, long nanos) 。该方法为 #acquireInterruptibly(int arg) 方法的进一步增强,它除了响应中断外,还有超时控制。即如果当前线程没有在指定时间内获取同步状态,则会返回 false ,否则返回 true 。
在这里插入图片描述

public final boolean tryAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    return tryAcquire(arg) ||
        doAcquireNanos(arg, nanosTimeout);
}
独占式同步状态释放

当线程获取同步状态后,执行完相应逻辑后,就需要释放同步状态。AQS 提供了#release(int arg)方法,释放同步状态。代码如下:

public final boolean release(int arg) {
     if (tryRelease(arg)) {
       Node h = head;
       if (h != null && h.waitStatus != 0)
           unparkSuccessor(h);
        return true;
    }
   return false;
}
总结

在 AQS 中维护着一个 FIFO 的同步队列。

  • 当线程获取同步状态失败后,则会加入到这个 CLH 同步队列的对尾,并一直保持着自旋。
  • 在 CLH 同步队列中的线程在自旋时,会判断其前驱节点是否为首节点,如果为首节点则不断尝试获取同步状态,获取成功则退出CLH同步队列。
  • 当线程执行完逻辑后,会释放同步状态,释放后会唤醒其后继节点。
共享式

共享式与独占式的最主要区别在于,同一时刻:

  • 独占式只能有一个线程获取同步状态。
  • 共享式可以有多个线程获取同步状态。
    例如,读操作可以有多个线程同时进行,而写操作同一时刻只能有一个线程进行写操作,其他操作都会被阻塞。参见 ReentrantReadWriteLock 。
共享式同步状态获取

AQS 提供 #acquireShared(int arg) 方法,共享式获取同步状态。代码如下:

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

 1: private void doAcquireShared(int arg) {
 2:     // 共享式节点
 3:     final Node node = addWaiter(Node.SHARED);
 4:     boolean failed = true;
 5:     try {
 6:         boolean interrupted = false;
 7:         for (;;) {
 8:             // 前驱节点
 9:             final Node p = node.predecessor();
10:             // 如果其前驱节点,获取同步状态
11:             if (p == head) {
12:                 // 尝试获取同步
13:                 int r = tryAcquireShared(arg);
14:                 if (r >= 0) {
15:                     setHeadAndPropagate(node, r);
16:                     p.next = null; // help GC
17:                     if (interrupted)
18:                         selfInterrupt();
19:                     failed = false;
20:                     return;
21:                 }
22:             }
23:             if (shouldParkAfterFailedAcquire(p, node) &&
24:                     parkAndCheckInterrupt())
25:                 interrupted = true;
26:         }
27:     } finally {
28:         if (failed)
29:             cancelAcquire(node);
30:     }
31: }

调用 #tryAcquireShared(int arg) 方法,尝试获取同步状态,获取成功则设置锁状态并返回大于等于 0 ,否则获取失败,返回小于 0 。若获取成功,直接返回,不用线程阻塞,自旋直到获得同步状态成功。

4、同步队列

AQS 通过内置的 FIFO 同步队列来完成资源获取线程的排队工作:

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

在CLH同步队列中,一个节点(Node),代表一个线程,它保存着线程的引用(thread)、状态(waitstatus)、前驱节点(prev)、后继节点(next)。其定义如下:
Node 是 AbstractQueuedSynchronizer 的内部静态类。

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;
    /**
     * 节点在等待队列中,节点线程等待在Condition上,当其他线程对Condition调用了signal()后,该节点将会从等待队列中转移到同步队列中,加入到同步状态的获取中
     */
    static final int CONDITION = -2;
    /**
     * 表示下一次共享式同步状态获取,将会无条件地传播下去
     */
    static final int PROPAGATE = -3;

    /** 等待状态 */
    volatile int waitStatus;

    /** 前驱节点,当节点添加到同步队列时被设置(尾部添加) */
    volatile Node prev;

    /** 后继节点 */
    volatile Node next;

    /** 等待队列中的后续节点。如果当前节点是共享的,那么字段将是一个 SHARED 常量,也就是说节点类型(独占和共享)和等待队列中的后续节点共用同一个字段 */
    Node nextWaiter;
    
    /** 获取同步状态的线程 */
    volatile Thread thread;

    final boolean isShared() {
        return nextWaiter == SHARED;
    }

    final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }

    Node() { // Used to establish initial head or SHARED marker
    }

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

    Node(Thread thread, int waitStatus) { // Used by Condition
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
    
}

waitstatus字段,等待字段,用来控制线程的阻塞和唤醒,并且可以避免不必要的调用LockSupport的park()方法和unpark()方法。目前有5种:INITAL(初始状态)、CANCELLED、SIGNAL、CONDITION、PROPAGATE。

CLH同步队列,结构图如下:
在这里插入图片描述

  • head 和 tail 字段,是 AbstractQueuedSynchronizer 的字段,分别指向同步队列的头和尾。
  • prev 和 next 字段,分别指向 Node 节点的前一个和后一个 Node 节点,从而实现链式双向队列。再配合上 head 和 tail 字段,快速定位到同步队列的头尾。
  • thread 字段,Node 节点对应的线程 Thread 。
  • nextWaiter 字段,Node 节点获取同步状态的模型( Mode )。#tryAcquire(int args) 和 #tryAcquireShared(int args) 方法,分别是独占式和共享式获取同步状态。在获取失败时,它们都会调用 #addWaiter(Node mode) 方法入队。nextWaiter 就是用来表示是哪种模式:(1)SHARED 静态 + 不可变字段,枚举共享模式。(2)EXCLUSIVE 静态 + 不可变字段,枚举独占模式。(3)#isShared() 方法,判断是否为共享式获取同步状态。
  • #predecessor() 方法,获得 Node 节点的前一个 Node 节点。在方法的内部,Node p = prev 的本地拷贝,是为了避免并发情况下,prev 判断完 == null 时,恰好被修改,从而保证线程安全。
入列:

1、tail 指向新节点。
2、新节点的 prev 指向当前最后的节点。
3、当前最后一个节点的 next 指向当前节点。
在这里插入图片描述
但是,实际上,入队逻辑实现的 #addWaiter(Node) 方法,需要考虑并发的情况。它通过 CAS 的方式,来保证正确的添加 Node 。代码如下:

private Node addWaiter(Node mode) {
    // 新建节点
    Node node = new Node(Thread.currentThread(), mode);
     // 记录原尾节点
     Node pred = tail;
    // 快速尝试,添加新节点为尾节点
     if (pred != null) {
        // 设置新 Node 节点的尾节点为原尾节点
         node.prev = pred;
         // CAS 设置新的尾节点
        if (compareAndSetTail(pred, node)) {
            // 成功,原尾节点的下一个节点为新节点
            pred.next = node;
            return node;
        }
     }
     // 失败,多次尝试,直到成功
     enq(node);
    return node;
}

private static final Unsafe unsafe = Unsafe.getUnsafe();

private static final long tailOffset = unsafe.objectFieldOffset (AbstractQueuedSynchronizer.class.getDeclaredField("tail"));  // 这块代码,实际在 static 代码块,此处为了方便理解,做了简化。

private final boolean compareAndSetTail(Node expect, Node update) {
    return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}

private Node enq(final Node node) {
    // 多次尝试,直到成功为止
    for (;;) {
        // 记录原尾节点
        Node t = tail;
        // 原尾节点不存在,创建首尾节点都为 new Node()
        if (t == null) {
            if (compareAndSetHead(new Node()))
                tail = head;
        // 原尾节点存在,添加新节点为尾节点
        } else {
            //设置为尾节点
            node.prev = t;
            // CAS 设置新的尾节点
            if (compareAndSetTail(t, node)) {
                // 成功,原尾节点的下一个节点为新节点
                t.next = node;
                return t;
            }
        }
    }
}
出列:

CLH 同步队列遵循 FIFO,首节点的线程释放同步状态后,将会唤醒它的下一个节点(Node.next)。而后继节点将会在获取同步状态成功时,将自己设置为首节点( head )。
这个过程非常简单,head 执行该节点并断开原首节点的 next 和当前节点的 prev 即可。注意,在这个过程是不需要使用 CAS 来保证的,因为只有一个线程,能够成功获取到同步状态。
在这里插入图片描述
#setHead(Node node) 方法,实现上述的出列逻辑。代码如下:

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

5、主要的内置方法

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

从上面的方法看下来,基本上可以分成3类:
1、独占式获取与释放同步状态。
2、共享式释放与获取同步状态。
3、查询同步队列中等待线程情况。

7、J.U.C 之并发工具类:CyclicBarrier

简介

CyclicBarrier,它允许一组线程互相等待,直到达到某个公共屏障点(Common Barrier Point)。在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待,此时CyclicBarrier在释放等待线程后,可以重用,所以称它为循环(Cyclic)的屏障(Barrier)。

通俗点讲就是:让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。

实现分析

java.util.concurrent.CyclicBarrier 的结构如下:
在这里插入图片描述
通过上图,我们可以看到 CyclicBarrier 的内部是使用重入锁 ReentrantLock 和 Condition 。

它有两个构造函数:

  • CyclicBarrier(int parties):创建一个新的 CyclicBarrier,它将在给定数量的参与者(线程)处于等待状态时启动,但它不会在启动 barrier 时执行预定义的操作。
  • CyclicBarrier(int parties, Runnable barrierAction) :创建一个新的 CyclicBarrier,它将在给定数量的参与者(线程)处于等待状态时启动,并在启动 barrier 时执行给定的屏障操作,该操作由最后一个进入 barrier 的线程执行。
public CyclicBarrier(int parties, Runnable barrierAction) {
    if (parties <= 0) throw new IllegalArgumentException();
    this.parties = parties;
    this.count = parties;
    this.barrierCommand = barrierAction;
}

public CyclicBarrier(int parties) {
    this(parties, null);
}

await方法
每个线程调用 #await() 方法,告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。当所有线程都到达了屏障,结束阻塞,所有线程可继续执行后续逻辑。

如果该线程不是到达的最后一个线程,则他会一直处于等待状态,除非发生以下情况:

1、最后一个线程到达,即 index == 0 。
2、超出了指定时间(超时等待)。
3、其他的某个线程中断当前线程。
4、其他的某个线程中断另一个等待的线程。
5、其他的某个线程在等待 barrier 超时。
6、其他的某个线程在此 barrier 调用 #reset() 方法。#reset() 方法,用于将屏障重置为初始状态。

应用场景

CyclicBarrier 适用于多线程结果合并的操作,用于多线程计算数据,最后合并计算结果的应用场景。比如,我们需要统计多个 Excel 中的数据,然后等到一个总结果。我们可以通过多线程处理每一个 Excel ,执行完成后得到相应的结果,最后通过 barrierAction 来计算这些线程的计算结果,得到所有Excel的总和。

应用示例

比如我们开会只有等所有的人到齐了才会开会,如下:

public class CyclicBarrierTest {

    private static CyclicBarrier cyclicBarrier;

    static class CyclicBarrierThread extends Thread{
        public void run() {
            System.out.println(Thread.currentThread().getName() + "到了");
            //等待
            try {
                cyclicBarrier.await();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args){
        cyclicBarrier = new CyclicBarrier(5, new Runnable() {
            @Override
            public void run() {
                System.out.println("人到齐了,开会吧....");
            }
        });

        for(int i = 0 ; i < 5 ; i++){
            new CyclicBarrierThread().start();
        }
    }
    
}

J.U.C 之并发工具类:CountDownLatch

简介

CyclicBarrier 所描述的是“允许一组线程互相等待,直到到达某个公共屏障点,才会进行后续任务,而 CountDownLatch 所描述的是“在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待”。

用给定的计数初始化 CountDownLatch。由于调用了 #countDown() 方法,所以在当前计数到达零之前,#await() 方法会一直受阻塞。之后,会释放所有等待的线程,#await() 的所有后续调用都将立即返回。这种现象只出现一次——计数无法被重置。如果需要重置计数,请考虑使用 CyclicBarrier 。
在这里插入图片描述
CountDownLatch 是通过一个计数器来实现的,当我们在 new 一个 CountDownLatch 对象的时候,需要带入该计数器值,该值就表示了线程的数量。

 - 每当一个线程完成自己的任务后,计数器的值就会减 1 。
 - 当计数器的值变为0时,就表示所有的线程均已经完成了任务,然后就可以恢复等待的线程继续执行了。

虽然,CountDownLatch 与 CyclicBarrier 有那么点相似,但是他们还是存在一些区别的:

  • CountDownLatch 的作用是允许 1 或 N 个线程等待其他线程完成执行;而 CyclicBarrier 则是允许 N 个线程相互等待。
  • CountDownLatch 的计数器无法被重置;CyclicBarrier 的计数器可以被重置后使用,因此它被称为是循环的 barrier 。

实现分析

java.util.concurrent.CountDownLatch 结构如下图:
在这里插入图片描述
通过上面的结构图我们可以看到,CountDownLatch 内部依赖 Sync 实现,而 Sync 继承 AQS 。

CountDownLatch 仅提供了一个构造方法,代码如下:

public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
    this.sync = new Sync(count);
}
Sync

sync 变量,为 CountDownLatch 的一个内部类 Sync :

private static final class Sync extends AbstractQueuedSynchronizer {
        
        private static final long serialVersionUID = 4982264981922014374L;

        Sync(int count) {
            setState(count);
        }

        // 获取同步状态
        int getCount() {
            return getState();
        }

        // 获取同步状态
        @Override
        protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }

        // 释放同步状态
        @Override
        protected boolean tryReleaseShared(int releases) {
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c-1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }
    }
    
}
await

CountDownLatch 提供 #await() 方法,来使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断。

countDown

CountDownLatch 提供 #countDown() 方法,递减锁存器的计数。如果计数到达零,则唤醒所有等待的线程。

public void countDown() {
    sync.releaseShared(1);
}

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

// Sync.java
@Overrride
protected boolean tryReleaseShared(int releases) {
    for (;;) {
        //获取锁状态
        int c = getState();
        //c == 0 直接返回,释放锁成功
        if (c == 0)
            return false;
        //计算新“锁计数器”
        int nextc = c-1;
        //更新锁状态(计数器)
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}
总结

CountDownLatch 内部通过共享锁实现。

  • 在创建 CountDownLatch 实例时,需要传递一个int型的参数:count,该参数为计数器的初始值,也可以理解为该共享锁可以获取的总次数。
  • 当某个线程调用 #await() 方法,程序首先判断 count 的值是否为 0 ,如果不为 0 的话,则会一直等待直到为 0 为止。
  • 当其他线程调用 #countDown() 方法时,则执行释放共享锁状态,使 count 值 - 1。
  • 当在创建 CountDownLatch 时初始化的 count 参数,必须要有 count 线程调用#countDown() 方法,才会使计数器 count 等于 0 ,锁才会释放,前面等待的线程才会继续运行。
    注意 CountDownLatch 不能回滚重置。

应用示例

示例仍然使用开会案例。老板进入会议室等待 5 个人全部到达会议室才会开会。所以这里有两种线程:老板等待开会线程、员工到达会议室线程:

public class CountDownLatchTest {

    private static CountDownLatch countDownLatch = new CountDownLatch(5);

    /**
     * Boss线程,等待员工到达开会
     */
    static class BossThread extends Thread{
        @Override
        public void run() {
            System.out.println("Boss在会议室等待,总共有" + countDownLatch.getCount() + "个人开会...");
            try {
                //Boss等待
                countDownLatch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("所有人都已经到齐了,开会吧...");
        }
    }

    // 员工到达会议室线程
    static class EmpleoyeeThread  extends Thread{
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + ",到达会议室....");
            //员工到达会议室 count - 1
            countDownLatch.countDown();
        }
    }

    public static void main(String[] args){
        //Boss线程启动
        new BossThread().start();

        for(int i = 0 ; i < countDownLatch.getCount() ; i++){
            new EmpleoyeeThread().start();
        }
    }
}

在这里插入图片描述

8、J.U.C 之并发工具类:Semaphore

简介

信号量 Semaphore 是一个控制访问多个共享资源的计数器,和 CountDownLatch 一样,其本质上是一个“共享锁”。
Semaphore 通常用于限制可以访问某些资源(物理或逻辑的)的线程数目

下面我们就一个停车场的简单例子来阐述 Semaphore :

1、为了简单起见我们假设停车场仅有 5 个停车位。一开始停车场没有车辆所有车位全部空着,然后先后到来三辆车,停车场车位够,安排进去停车,然后又来三辆,这个时候由于只有两个停车位,所有只能停两辆,其余一辆必须在外面候着,直到停车场有空车位。当然,以后每来一辆都需要在外面候着。当停车场有车开出去,里面有空位了,则安排一辆车进去(至于是哪辆,要看选择的机制是公平还是非公平)。

2、从程序角度看,停车场就相当于信号量 Semaphore ,其中许可数为 5 ,车辆就相对线程。当来一辆车时,许可数就会减 1 。当停车场没有车位了(许可数 == 0 ),其他来的车辆需要在外面等候着。如果有一辆车开出停车场,许可数 + 1,然后放进来一辆车。

3、信号量 Semaphore 是一个非负整数( >=1 )。当一个线程想要访问某个共享资源时,它必须要先获取 Semaphore。当 Semaphore > 0 时,获取该资源并使 Semaphore – 1 。如果S emaphore 值 = 0,则表示全部的共享资源已经被其他线程全部占用,线程必须要等待其他线程释放资源。当线程释放资源时,Semaphore 则 +1 。

实现分析

java.util.concurrent.Semaphore 结构如下图:
在这里插入图片描述
从上图可以看出,Semaphore 内部包含公平锁(FairSync)和非公平锁(NonfairSync,默认),继承内部类 Sync ,其中 Sync 继承 AQS(再一次阐述 AQS 的重要性)。

Semaphore 提供了两个构造函数:

Semaphore(int permits) :创建具有给定的许可数和非公平的公平设置的 Semaphore 。
Semaphore(int permits, boolean fair) :创建具有给定的许可数和给定的公平设置的 Semaphore 。

  • Semaphore 默认选择非公平锁。
  • 当信号量 Semaphore = 1 时,它可以当作互斥锁使用。其中 0、1 就相当于它的状态:1)当 =1 时表示,其他线程可以获取;2)当 =0 时,排他,即其他线程必须要等待。
  • Semaphore 的代码实现结构,和 ReentrantLock 类似。
信号量获取

Semaphore 提供了 #acquire() 方法,来获取一个许可。

public void acquire() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

公平情况的 FairSync 的方法实现,代码如下:

// FairSync.java
@Override
protected int tryAcquireShared(int acquires) {
    for (;;) {
        //判断该线程是否位于CLH队列的列头,从而实现公平锁
        if (hasQueuedPredecessors())
            return -1;
        //获取当前的信号量许可
        int available = getState();

        //设置“获得acquires个信号量许可之后,剩余的信号量许可数”
        int remaining = available - acquires;

        //CAS设置信号量
        if (remaining < 0 ||
                compareAndSetState(available, remaining))
            return remaining;
    }
}

通过 #hasQueuedPredecessors() 方法,判断该线程是否位于 CLH 队列的列头,从而实现公平锁。

非公平情况的 NonfairSync 的方法实现,代码如下:

// NonfairSync.java
protected int tryAcquireShared(int acquires) {
    return nonfairTryAcquireShared(acquires);
}

// Sync.java
final int nonfairTryAcquireShared(int acquires) {
    for (;;) {
        int available = getState();
        int remaining = available - acquires;
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}

对于非公平而言,因为它不需要判断当前线程是否位于 CLH 同步队列列头,所以相对而言会简单些。

信号量释放

获取了许可,当用完之后就需要释放,Semaphore 提供 #release() 方法,来释放许可。代码如下:

public void release() {
    sync.releaseShared(1);
}

内部调用 AQS 的 #releaseShared(int arg) 方法,释放同步状态。

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

releaseShared(int arg) 方法,会调用 Semaphore 内部类 Sync 的 #tryReleaseShared(int arg) 方法,释放同步状态。

// Sync.java
protected final boolean tryReleaseShared(int releases) {
    for (;;) {
        int current = getState();
        //信号量的许可数 = 当前信号许可数 + 待释放的信号许可数
        int next = current + releases;
        if (next < current) // overflow
            throw new Error("Maximum permit count exceeded");
        //设置可获取的信号许可数为next
        if (compareAndSetState(current, next))
            return true;
    }
}

如该方法返回 true 时,代表释放同步状态成功,从而在 #releaseShared(int args) 方法中,调用 #doReleaseShared() 方法,可唤醒阻塞等待 Semaphore 的许可的线程。

应用示例

public class SemaphoreTest {

    static class Parking {
    
        //信号量
        private Semaphore semaphore;

        Parking(int count) {
            semaphore = new Semaphore(count);
        }

        public void park() {
            try {
                //获取信号量
                semaphore.acquire();
                long time = (long) (Math.random() * 10);
                System.out.println(Thread.currentThread().getName() + "进入停车场,停车" + time + "秒..." );
                Thread.sleep(time);
                System.out.println(Thread.currentThread().getName() + "开出停车场...");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                semaphore.release();
            }
        }
    }


    static class Car extends Thread {
        Parking parking ;

        Car(Parking parking){
            this.parking = parking;
        }

        @Override
        public void run() {
            parking.park();     //进入停车场
        }
    }

    public static void main(String[] args){
        Parking parking = new Parking(3);

        for(int i = 0 ; i < 5 ; i++){
            new Car(parking).start();
        }
    }
}

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值