Bootstrap

Semaphore和CountDownLatch详解

一. Semaphore

前面介绍的ReentranLock是一种独占锁,而这里要介绍的Semaphore则是一种共享锁。Semaphore,俗称信号量,他是操作系统中PV操作原语在Java层面的实现,它也是基于AQS实现的。Semaphore的功能非常强大,大小为1的信号量类似于互斥锁,通过同时只能有一个线程获取信号量实现。大小为n(n>0)的信号量可以实现限流的功能,它可以实现只能有n个线程同时获取信号量。
在这里插入图片描述

1. 常用方法

  • 构造方法
//permits是信号量的大小,默认是非公平锁
public Semaphore(int permits) {
        sync = new NonfairSync(permits);
    }
    public Semaphore(int permits, boolean fair) {
        sync = fair ? new FairSync(permits) : new NonfairSync(permits);
    }
  • 常用方法:
    public void acquire() throws InterruptedException //尝试去获取资源
    public boolean tryAcquire()
    public void release() {
        sync.releaseShared(1);
    }

2. 应用场景

Semaphore常用的应用场景就是做流量控制(特别是资源有限的场景)。

public static void main(String[] args)  {
        //模拟有三个买票窗口
        Semaphore windows=new Semaphore(3);
        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                try {
                    windows.acquire();
                    System.out.println(Thread.currentThread().getName()+"开始买票");
                    Thread.sleep(5000);
                    System.out.println(Thread.currentThread().getName()+"买票成功");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    windows.release();
                }
            }).start();
        }
    }

在这里插入图片描述

3. Semaphore源码分析

关键点:
Semaphore的加锁解锁(共享锁)逻辑
线程竞争失败入队阻塞和获取锁的线程释放锁唤醒阻塞线程竞争锁的逻辑实现

  • 构造函数
Sync(int permits) {
            setState(permits);
        }

其实底层就是AQS的state设置为了资源的数,所以state在共享锁中就是代表的资源数

  • 获取锁
 windows.acquire();

进入acquire方法

public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }

核心就是调用tryAcquireShared(arg)尝试去获取共享锁

 final int nonfairTryAcquireShared(int acquires) {
            for (;;) {
                //获取当前的资源数
                int available = getState();
                int remaining = available - acquires;       //如果资源数小于0,说明资源数不够直接返回remaining
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    //或cas成功返回,cas后剩余的资源数
                    return remaining;
            }
        }

上面的关键操作就是cas操作来更新资源数,如果资源数小于0,直接返回。如果CAS成功说明线程获取共享锁成功

 if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }

如果返回的资源数小于0,执行doAcquireSharedInterruptibly(arg)

private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        //创建一个node,并设置模式为共享模式,然后入队
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            for (;;) {
            //获得当前节点的前驱节点
                final Node p = node.predecessor();
                //如果前面的节点是头节点
                if (p == head) {
                //再次尝试获取资源
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                    //获取资源成功后续操作
                   setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                //获取资源失败,准备执行阻塞操作
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

这个就是实现资源数小于0,线程入队阻塞

 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;
    }

这个方法是AQS类的方法,主要是实现线程入队

 private Node enq(final Node node) {
        for (;;) {
            Node t = 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;
                }
            }
        }
    }

这个放法的作用主要就是为了节点入队,它和ReentrantLock的原理是一样的,核心就是两次for循环,第一次for循环创建队列,第二次for循环节点入队

 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 {
        //将前驱节点置为-1,只有将前驱级节点的pred.waitStatus置为-1,它才能唤醒当前节点(-1表示后继节点需要被unparking唤醒)
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

这里就是再次获取资源失败,尝试线程阻塞的逻辑。它同样是AQS实现的模版方法,所以和ReetrantLock原理是一样的

 private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        //返回的布尔值表示线程在调用 park() 方法期间是否被中断过。
        return Thread.interrupted();
    }

shouldParkAfterFailedAcquire是做一些阻塞的准备工作,而parkAndCheckInterrupt才是真正的开始阻塞。底层调用LockSupport的park方法来阻塞当前线程节点。

以上便是Semaphore获取锁以及获取资源失败阻塞的过程。上面是加锁的逻辑,下面看看释放锁的逻辑:

 windows.release();
   public void release() {
        sync.releaseShared(1);
    }
public final boolean releaseShared(int arg) {
//首先tryReleaseShared主要是做锁释放的准备工作
        if (tryReleaseShared(arg)) {
        //真正开始释放锁
            doReleaseShared();
            return true;
        }
        return false;
    }
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");
                if (compareAndSetState(current, next))
                //然后通过CAS操作更新资源数
                    return true;
            }
        }

上面的操作主要是将要释放锁的线程占用的资源数归还的state

  private void doReleaseShared() {
        for (;;) {
        //获得头节点
            Node h = head;
            if (h != null && h != tail) {
            //这个if判断就是判断目前还是否有线程在等待资源
            //获取头节点的等待状态
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                //如果头节点的值为-1,表示后续节点可以被唤醒
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    //然后使用CAS操作将头节点的h.waitStatu置为0,开始执行唤醒逻辑
                        continue;              
                     //唤醒后继线程
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

AQS实现,主要是指性唤醒后继节点的操作

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

当幻想之后下一个线程就可以被唤醒执行后面的逻辑,假设被唤醒的线程可以获取到资源 ,它又会重新进入doAcquireSharedInterruptibly的for循环执行上面的获取资源的流程

 private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        //创建一个node,并设置模式为共享模式,然后入队
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            for (;;) {
            //获得当前节点的前驱节点
                final Node p = node.predecessor();
                //如果前面的节点是头节点
                if (p == head) {
                //再次尝试获取资源
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                    //获取资源成功后续操作
                   setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                //获取资源失败,准备执行阻塞操作
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

前面介绍的第一次获取资源的流程,我们知道由于z资源获取失败进入下面代码

   if (p == head) {
                //再次尝试获取资源
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                    //获取资源成功后续操作
                   setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }

这里的关键代码是setHeadAndPropagate

 private void setHeadAndPropagate(Node node, int propagate) {
       //获取当前头节点
        Node h = head; // Record old head for check below
        //设置当前节点头节点
        setHead(node);
        //propagate>0表示还有资源
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            //获取当前节点的下一个节点
            Node s = node.next;
            if (s == null || s.isShared())
            //如果下一个节点为共享模式,还可以进行继续唤醒(这就是和独占锁不同的地方),然后执行新的唤醒流程(这就解决了有资源线程却在阻塞的问题)
                doReleaseShared();
        }
    }

在学习ReentrantLock我们知道,当一个线程从等待队列中获取锁后,它需要作为一个新的双向链表头节点,这里就是在做这个。

   private void setHead(Node node) {
        head = node;
        //头节点的node.thread属性要为空
        node.thread = null;
        node.prev = null;
    }

上面就是在设置新的head,原理上就是在做链表的更新操作

二、CountDownLatch

CountDownLatch(闭锁)是一个同步协助类,允许一个或多个线程等待,直到其他线程完成操作集。CountDownLatch使用给定的计数值(count)初始化。await方法会阻塞直到当前的计数值 (count)由于countDown方法的调用达到0,count为0之后所有等待的线程都会被释放,并且随后对await方法的调用都会立即返回。这是一个一次性现象 —— count不会被重置。如果 你需要一个重置count的版本,那么请考虑使用CyclicBarrier。(它也是共享锁的一种实现)

在这里插入图片描述

1. 常用方法

  • 构造函数
//参数count就是我们的计数器
public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }
  • 常见方法
//调用await方法的线程会被挂起,它会等待知道count值为0才会继续进行
public void await() throws InterruptedException
//挂起timeout时间,count还不为0,不再挂起继续执行
public boolean await(long timeout, TimeUnit unit)
//减小count的值
public void countDown();

2. 应用场景

CountDownLatch一般作多线程倒数计时计数器,强制它们等待一组(CountDownLatch的初始化决定)任务执行完成,它有两种使用场景

  • 场景1:让多个线程等待
  • 场景2:让单个线程等待

场景一:让多个线程等待,模拟并发,让并发线程一起执行

public class Main {
    public static void main(String[] args) throws  InterruptedException{
        CountDownLatch countDownLatch = new CountDownLatch(1);
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try { //准备完毕......运动员都阻塞在这,等待号令
                    countDownLatch.await();
                    String parter = "【" + Thread.currentThread().getName() + "】";
                    System.out.println(parter + "开始执行......"+"当前时间为"+System.currentTimeMillis());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
        Thread.sleep(2000);// 裁判准备发令
        countDownLatch.countDown();// 发令枪:执行发令
    }
}

在这里插入图片描述

几乎同时开始执行

场景二:让单个线程等待,多个任务完成之后进行汇总

public class Main {
    public static void main(String[] args) throws  InterruptedException{
        CountDownLatch countDownLatch = new CountDownLatch(5);
        for(int i=0;i<5;i++){
            final int index = i;
            new Thread(()->{
                try{
                    Thread.sleep(1000 + ThreadLocalRandom.current().nextInt(1000));
                    System.out.println(Thread.currentThread().getName()+" finish task" + index);
                    countDownLatch.countDown();
                   } catch (InterruptedException e) {
                         e.printStackTrace();
                   }
            }).start();
        }
        // 主线程在阻塞,当计数器==0,就唤醒主线程往下执行。
        countDownLatch.await();
        System.out.println("主线程:在所有任务运行完成后,进行结果汇总");
    }
}

在这里插入图片描述

2. 底层原理

底层基于 AbstractQueuedSynchronizer 实现,CountDownLatch 构造函数中指定的 count直接赋给AQS的state;每次countDown()则都是release(1)减1,最后减到0时unpark阻塞线程;这一步是由最后一个执行countdown方法的线程执行的。而调用await()方法时,当前线程就会判断state属性是否为0,如果为0,则继续往下执行,如果不为0,则使当前线程进入等待状态,直到某个线程将state属性置为0,其就会唤醒在 await()方法中等待的线程。

  • CountDownLatch与Thread.join的区别

CountDownLatch的作用就是允许一个或多个线程等待其他线程完成操作,看起来有点类似join() 方法,但其提供了比 join() 更加灵活的API。 CountDownLatch可以手动控制在n个线程里调用n次countDown()方法使计数器进行减一操作,也可以在一个线程里调用n次执行减一操作。而 join() 的实现原理是不停检查join线程是否存活,如果 join 线程存活则让当前线程永远等待。所以两者之间相对来说还是CountDownLatch使用起来较为灵活。

  • CountDownLatch与CyclicBarrier的区别

CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同: CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset() 方法重置。所以CyclicBarrier能处理更为复杂的业务场景,比如如果计算发生错误,可 以重置计数器,并让线程们重新执行一次。CyclicBarrier还提供getNumberWaiting(可以获得CyclicBarrier阻塞的线程数量)、 isBroken(用来知道阻塞的线程是否被中断)等方法。CountDownLatch会阻塞主线程,CyclicBarrier不会阻塞主线程,只会阻塞子线程。CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同。CountDownLatch一般用于一个或多个线程,等待其他线程执行完任务后,再执行。CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行。CyclicBarrier 还可以提供一个 barrierAction,合并多线程计算结果。
6. CyclicBarrier是通过ReentrantLock的"独占锁"和Conditon来实现一组线程的阻塞唤 醒的,而CountDownLatch则是通过AQS的“共享锁”实现。

  • await()方法原理
 public void await() throws InterruptedException {
 //默认资源数是1(尝试获取的资源数)        sync.acquireSharedInterruptibly(1);
    }
 //AQS类的代码
    public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }

可以发现和Semaphore一样

protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }

通过state为0,返回的值为1,否则为-1(因为state不为0,所以这里始终为-1)所以会执行doAcquireSharedInterruptibly()进行阻塞

  • await()方法原理
   public void countDown() {
        sync.releaseShared(1);
    }

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

  protected boolean tryReleaseShared(int releases) {
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c-1; 
                if (compareAndSetState(c, nextc))
                  //如果nextc减1后等于0,就可以执行唤醒操作了
                    return nextc == 0;
            }
        }

然后执行共享锁唤醒逻辑:

 private void doReleaseShared() {
        /*
         * Ensure that a release propagates, even if there are other
         * in-progress acquires/releases.  This proceeds in the usual
         * way of trying to unparkSuccessor of head if it needs
         * signal. But if it does not, status is set to PROPAGATE to
         * ensure that upon release, propagation continues.
         * Additionally, we must loop in case a new node is added
         * while we are doing this. Also, unlike other uses of
         * unparkSuccessor, we need to know if CAS to reset status
         * fails, if so rechecking.
         */
        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);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

上面代码还是比较简单的

;