系列文章目录
JUC篇:volatile可见性的实现原理
JUC篇:synchronized的应用和实现原理
JUC篇:用Java实现一个简单的线程池
JUC篇:java中的线程池
JUC篇:ThreadLocal的应用与原理
文章目录
系列文章目录前言一、等待多线程完成的CountDownLatch1.1案例介绍1.2实现原理1.2.1void await()方法1.2.2.void countDown()方法 1.3小结 二、同步屏障CyclicBarrier2.1案例介绍2.2实现原理2.2.1int await()方法 2.3小结 三.控制并发线程数的Semaphore3.1案例介绍3.2实现原理3.2.1void acquire()方法3.2.2void acquire(int permits)方法3.2.3void acquireUninterruptibly()方法3.2.4void acquireUninterruptibly(intpermits)方法3.2.5void release()方法 3.3小结 总结
前言
在JDK的并发包里提供了几个非常有用的并发工具类。CountDownLatch、CyclicBarrier和
Semaphore工具类提供了一种并发流程控制的手段。本文会配合一些应用场景来介绍如何使用这些工具类并解析其中的原理。
一、等待多线程完成的CountDownLatch
CountDownLatch允许一个或多个线程等待其他线程完成操作。
在日常开发中经常会遇到需要在主线程中开启多个线程去并行执行任务,并且主线程需要等待所有子线程执行完毕后再进行汇总的场景。在CountDownLatch出现之前一般都使用线程的join()方法来实现这一点,但是join方法不够灵活,不能够满足不同场景的需要,所以JDK开发组提供了CountDownLatch这个类
1.1案例介绍
假如有这样一个需求:我们需要解析一个Excel里多个sheet的数据,此时可以考虑使用多线程,每个线程解析一个sheet里的数据,等到所有的sheet都解析完之后,程序需要提示解析完成。在这个需求中,要实现主线程等待所有线程完成sheet的解析操作,最简单的做法是使用join()方法
//用join()实现主线程等待全部子线程执行完之后再执行public class JoinCountDownLatchTest { public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread(()->{ System.out.println("线程1....start"); },"线程1"); Thread thread2 = new Thread(() -> { System.out.println("线程2.。。。start"); }, "线程2"); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println("主线程。。。。。start"); }}
//用CountDownLatch实现主线程等待全部子线程执行完之后再执行public class CountDownLatchTest { public static void main(String[] args) throws InterruptedException { CountDownLatch countDownLatch = new CountDownLatch(2); AtomicInteger res= new AtomicInteger(); Thread thread1 = new Thread(()->{ System.out.println("线程1....start"); for (int i=0;i<10000;i++){ res.incrementAndGet(); } countDownLatch.countDown(); },"线程1"); Thread thread2 = new Thread(() -> { System.out.println("线程2.。。。start"); for (int i=0;i<10000;i++){ res.incrementAndGet(); } countDownLatch.countDown(); }, "线程2"); thread1.start(); thread2.start(); countDownLatch.await(); System.out.println("主线程。。。。。start"+res); }}
总结CountDownLatch与join方法的区别
一个区别是,调用一个子线程的join()方法后,该线程会一直被阻塞直到子线程运行完毕,而CountDownLatch则使用计数器来允许子线程运行完毕或者在运行中递减计数,也就是CountDownLatch可以在子线程运行的任何时候让await方法返回而不一定必须等到线程结束。另外,使用线程池来管理线程时一般都是直接添加Runable到线程池,这时候就没有办法再调用线程的join方法了,就是说countDownLatch相比join方法让我们对线程同步有更灵活的控制。1.2实现原理
CountDownLatch的名字就可以猜测其内部应该有个计数器,并且这个计数器是递减的。下面就通过源码看看JDK开发组在何时初始化计数器,在何时递减计数器,当计数器变为0日才做了什么操作,多个线程是如何通过计时器值实现同步的。为了一览CountDownLatch的内部结构,我们先看它的类图
从类图可以看出,CountDownLatch是使用AQS实现的。通过下面的构造函数,你会发现,实际上是把计数器的值赋给了AQS的状态变量state,也就是这里使用AQS的状态值来表示计数器值。
public CountDownLatch(int count) { if (count < 0) throw new IllegalArgumentException("count < 0"); this.sync = new Sync(count); }Sync(int count) { setState(count); }
下面我们来研究CountDownLatch中的几个重要的方法,看它们是如何调用AQS来实现功能的。
1.2.1void await()方法
当线程调用CountDownLatch对象的await方法后,当前线程会被阻塞,直到下面的情况之一发生才会返回
当所有线程都调用了CountDownLatch对象的countDown方法后,也就是计数器的值为0时
其他线程调用了当前线程的interrupt()方法中断了当前线程,当前线程就会抛出InterruptedException异常,然后返回
下面看下在await()方法内部是如何调用AQS的方法的。
public void await() throws InterruptedException { sync.acquireSharedInterruptibly(1); }
从以上代码可以看到,await()方法委托sync调用了AQS的acquiresharedlnterruptibIy方法,后者的代码如下:
public final void acquireSharedInterruptibly(int arg) throws InterruptedException { //线程被中断抛出异常 if (Thread.interrupted()) throw new InterruptedException(); //查看计数器值是否为0,为0直接返回,不为0进入AQS的队列等待 if (tryAcquireShared(arg) < 0) doAcquireSharedInterruptibly(arg); }protected int tryAcquireShared(int acquires) { return (getState() == 0) ? 1 : -1; }
由如上代码可知,该方法的特点是线程获取资源时可以被中断,并且获取的资源是共享资源。acquireSharedInterruptibly首先判断当前线程是否己被中断,若是则抛出异常,否则调用sync实现的tryAcquireShared方法查看当前状态值(计数器值)是否为0,是则
当前线程的await()方法直接返回,否则调用AQS的doAcquireSharedlnterruptibly方法让当前线程阻塞。
另外可以看到,这里tryAcquireShared传递的arg参数没有被用到,调用tryAcquireShared的方法仅仅是为了检查当前状态值是不是为0,并没有调用CAS让当前状态值减1
1.2.2.void countDown()方法
线程调用该方法后,计数器的值递减,递减后如果计数器值为0则唤醒所有因调用await方法而被阻塞的线程,否则什么都不做。下面看下countDown()方法是如何调用AQS的方法的。
public void countDown() { //委托sync调用AQS的方法 sync.releaseShared(1); }
由如上代码可知,CountDownLatch的countDown()方法委托sync调用了AQS的releaseShared方法,后者的代码如下。
public final boolean releaseShared(int arg) { //调用sync实现的tryReleaseShared if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; }
在如上代码中,releaseShared首先调用了sync实现的AQS的tryReleaseShared方法,其代码如下。
protected boolean tryReleaseShared(int releases) { //循环进行CAS,直到当前线程成功完成CAS使计数器值(状态值state)减l并更新圭1]state for (;;) { int c = getState(); //如果当前状态值为0,直接返回(1) if (c == 0) return false; int nextc = c-1; //使用CAS让计数器减1(2) if (compareAndSetState(c, nextc)) return nextc == 0; } }
如上代码首先获取当前状态值(计数器值)。代码(1)判断如果当前状态值为0则直接返回false,从而countDown()方法直接返回:否则执行代码(2)使用CAS将计数器值减1,CAS失败则循环重试,否则如果当前计数器值为0则返回true,返回true说明是最后一个线程调用的countdown方法,那么该线程除了让计数器值减1外,还需要唤醒因调用CountDownLatch的await方法而被阻塞的线程,具体是调用AQS的doReleaseShared方法来激活阻塞的线程。这里代码(1)貌似是多余的,其实不然,之所以添加代码(1)是为了防止当计数器值为0后,其他线程又调用了countDown方法,如果没有代码(1)状态值就可能会变成负数。
1.3小结
本节首先介绍了CountDownLatch的使用,相比使用join方法来实现线程间同步,前者更具有灵活性和方便性。
另外还介绍了CountDownLatch的原理,CountDownLatch是使用AQS实现的。使用AQS的状态变量来存放计数器的值。
首先在初始化CountDownLatch时设置状态值(计数器值),当多个线程调用countdown方法时实际是原子性递减AQS的状态值。当线程调用await方法后当前线程会被放入AQS的阻塞队列等待计数器为0再返回。其他线程调用countdown方法让计数器值递减1,当计数器值变为0时,当前线程还要调用AQS的doReleaseShared方法来激活由于调用await()方法而被阻塞的线程。
二、同步屏障CyclicBarrier
CountDownLatch在解决多个线程同步方面相对于调用线程的join方法己经有了不少优化,但是CountDownLatch的计数器是一次性的,也就是等到计数器值变为0后,再调用CountDownLatch的await和countdown方法都会立刻返回,这就起不到线程同步的效果了。
所以为了满足计数器可以重置的需要,JDK开发组提供了CyclicBarrier类,并且CyclicBarrier类的功能并不限于CountDownLatch的功能。从字面意思理解,CyclicBarrier是回环屏障的意思,它可以让一组线程全部达到一个状态后再全部同时执行。这里之所以叫作回环是因为当所有等待线程执行完毕,并重置CyclicBarrier的状态后它可以被重用。之所以叫作屏障是因为线程调用await方法后就会被阻塞,这个阻塞点就称为屏障点,等所有线程都调用了await方法后,线程们就会冲破屏障,继续向下运行。
2.1案例介绍
假设一个任务由阶段1、阶段2和阶段3组成,每个线程要串行地执行阶段1、阶段2和阶段3,当多个线程执行该任务时必须要保证所有线程的阶段1全部完成后才能进入阶段2执行,当所有线程的阶段2全部完成后才能进入阶段3执行。下面使用CyclicBarrier来完成这个需求。
public class CyclicBarrierDemo1Test { //假设一个任务由阶段1、阶段2和阶段3组成,每个线程要串行地执行阶段1、阶段2和阶段3, // 当多个线程执行该任务时必须要保证所有线程的阶段1全部完成后才能进入阶段2执行,当所有线程的阶段2全部完成后才能进入阶段3执行。 // 下面使用CyclicBarrier来完成这个需求。 public static void main(String[] args) { CyclicBarrier cyclicBarrier = new CyclicBarrier(2); ExecutorService executorService = Executors.newFixedThreadPool(2); //线程一 executorService.execute(new Runnable() { @Override public void run() { try { System.out.println(Thread.currentThread().getName()+":step-1"); cyclicBarrier.await(); System.out.println(Thread.currentThread().getName()+":step-2"); cyclicBarrier.await(); System.out.println(Thread.currentThread().getName()+":step-3"); cyclicBarrier.await(); } catch (Exception e) { e.printStackTrace(); } } }); //线程二 executorService.execute(new Runnable() { @Override public void run() { try { System.out.println(Thread.currentThread().getName()+":step-1"); cyclicBarrier.await(); System.out.println(Thread.currentThread().getName()+":step-2"); cyclicBarrier.await(); System.out.println(Thread.currentThread().getName()+":step-3"); cyclicBarrier.await(); } catch (Exception e) { e.printStackTrace(); } } }); //关闭线程池 executorService.shutdown(); }}
输出结果
在如上代码中,每个子线程在执行完阶段1后都调用了await方法,等到所有线程都到达屏障点后才会一块往下执行,这就保证了所有线程都完成了阶段1后才会开始执行阶段2。然后在阶段2后面调用了await方法,这保证了所有线程都完成了阶段2后,才能开始阶段3的执行。这个功能使用单个CountDownLatch是无法完成的。
2.2实现原理
CyclicBarrier的类图结构
由以上类图可知,CyclicBrrier基于独占锁实现,本质底层还是基于AQS的。parties用来记录线程个数,这里表示多少线程调用await后,所有线程才会冲破屏障继续往下运行。而count一开始等于parties,每当有线程调用await方法就递减1,当count为0时就表示所有线程都到了屏障点。
你可能会疑惑,为何维护parties和count两个变量,只使用count不就可以了?
别忘了CyclicBrrier是可以被复用的,使用两个变量的原因是,parties始终用来记录总的线程个数,当count计数器值变为0后,会将parties的值赋给count,从而进行复用。这两个变量是在构造CyclicBarrier对象时传递的,如下所示。
public CyclicBarrier(int parties, Runnable barrierAction) { if (parties <= 0) throw new IllegalArgumentException(); this.parties = parties; this.count = parties; this.barrierCommand = barrierAction; }
还有一个变量barrierCommand也通过构造函数传递,这是一个任务,这个任务的执行时机是当所有线程都到达屏障点后。
使用lock首先保证了更新计数器count的原子性。
另外使用lock的条件变量trip支持线程间使用await和signal操作进行同步。
最后,在变量generation内部有一个变量broken,其用来记录当前屏障是否被打破。
注意,这里的broken并没有被声明为volatile的,因为是在锁内使用变量,所以不需要声明。
private static class Generation { boolean broken = false; }
下面来看CyclicBarrier中的几个重要的方法。
2.2.1int await()方法
当前线程调用CyclicBarrier的该方法时会被阻塞,直到满足下面条件之一才会返回:
parties个线程都调用了await()方法,也就是线程都到了屏障点;
其他线程调用了当前线程的interrupt()方法中断了当前线程,则当前线程会抛出InterruptedException异常而返回;
与当前屏障点关联的Generation对象的broken标志被设置为true时,会抛出BrokenBarrierException异常,然后返回。
由如下代码可知,在内部调用了dowait方法。第一个参数为false则说明不设置超时时间,这时候第二个参数没有意义。
public int await() throws InterruptedException, BrokenBarrierException { try { return dowait(false, 0L); } catch (TimeoutException toe) { throw new Error(toe); // cannot happen } }
2.2.2int dowait(boolean timed,long nanos)方法
private int dowait(boolean timed, long nanos) throws InterruptedException, BrokenBarrierException, TimeoutException { final ReentrantLock lock = this.lock; lock.lock(); try { final Generation g = generation; if (g.broken) throw new BrokenBarrierException(); if (Thread.interrupted()) { breakBarrier(); throw new InterruptedException(); } int index = --count; //如果index==O则说明所有线程都到了屏障点,此时执行初始化时传递的任务 if (index == 0) { // tripped boolean ranAction = false; try { final Runnable command = barrierCommand; //(2)执行任务 if (command != null) command.run(); ranAction = true; //(3)激活其他因调用await方法而被阻塞的线程,并重置CyclicBarrier nextGeneration(); //返回 return 0; } finally { if (!ranAction) breakBarrier(); } } // (4)如果index!=0 for (;;) { try { //(5)没有设置超时时间 if (!timed) trip.await(); //(6)设置了超时 else if (nanos > 0L) nanos = trip.awaitNanos(nanos); } catch (InterruptedException ie) { if (g == generation && ! g.broken) { breakBarrier(); throw ie; } else { // We're about to finish waiting even if we had not // been interrupted, so this interrupt is deemed to // "belong" to subsequent execution. Thread.currentThread().interrupt(); } } if (g.broken) throw new BrokenBarrierException(); if (g != generation) return index; if (timed && nanos <= 0L) { breakBarrier(); throw new TimeoutException(); } } } finally { lock.unlock(); } } private void nextGeneration() { // (7)唤醒等待队列里面阻塞的线程 trip.signalAll(); // (8)重置CyclicBarrier count = parties; generation = new Generation(); }
以上是dowait方法的主干代码。当一个线程调用了dowait方法后,首先会获取独占锁lock,如果创建CycleBarrier时传递的参数为10,那么后面9个调用钱程会被阻塞。然后当前获取到锁的线程会对计数器count进行递减操作,递减后count=index=9,因为
index!=0所以当前线程会执行代码(4)。
如果当前线程调用的是无参数的await()方法,则这里timed=false,所以当前线程会被放入条件变量的p的条件阻塞队列,当前线程会被挂起并释放获取的lock锁。如果调用的是有参数的await方法则timed=true,然后当前线程也会被放入条件变量的条件队列并释放锁资源,不同的是当前线程会在指定时间超时后自动被激活。
当第一个获取锁的线程由于被阻塞释放锁后,被阻塞的9个线程中有一个会竞争到lock锁,然后执行与第一个线程同样的操作,直到最后一个线程获取到lock锁,此时己经有9个线程被放入了条件变量trip的条件队列里面。最后count=index等于0,所以执行
代码(2),如果创建CyclicBarrier时传递了任务,则在其他线程被唤醒前先执行任务,任务执行完毕后再执行代码(3),唤醒其他9个线程,并重置CyclicBarrier,然后这10个线程就可以继续向下运行了。
2.3小结
本节首先通过案例说明了CycleBarrier与CountDownLatch的不同在于,前者是可以复用的,并且前者特别适合分段任务有序执行的场景。然后分析了CycleBarrier,其通过独占锁ReentrantLock实现计数器原子性更新,并使用条件变量队列来实现线程同步。
三.控制并发线程数的Semaphore
Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。
3.1案例介绍
Semaphore可以用于做流量控制,特别是公用资源有限的应用场景,比如数据库连接。假如有一个需求,要读取几万个文件的数据,因为都是IO密集型任务,我们可以启动几十个线程并发地读取,但是如果读到内存后,还需要存储到数据库中,而数据库的连接数只有10个,这时我们必须控制只有10个线程同时获取数据库连接保存数据,否则会报错无法获取数据库连接。这个时候,就可以使用Semaphore来做流量控制
public class SemaphoreTest { private static final int THREAD_COUNT=30; private static final ExecutorService THREAD_POOL= Executors.newFixedThreadPool(THREAD_COUNT); public static void main(String[] args) { Semaphore semaphore = new Semaphore(10); for (int i = 0; i <THREAD_COUNT ; i++) { THREAD_POOL.execute(()->{ try { semaphore.acquire(); System.out.println("save data"); semaphore.release(); } catch (InterruptedException e) { e.printStackTrace(); } }); } THREAD_POOL.shutdown(); }}
3.2实现原理
Semaphore的类图
由该类图可知,Semaphore还是使用AQS实现的。Sync只是对AQS的一个修饰,并且Sync有两个实现类,用来指定获取信号量时是否采用公平策略。例如,下面的代码在创建Semaphore时会使用一个变量指定是否使用公平策略。
public Semaphore(int permits) { sync = new NonfairSync(permits); }public Semaphore(int permits, boolean fair) { sync = fair ? new FairSync(permits) : new NonfairSync(permits); } Sync(int permits) { setState(permits); }
在如上代码中,Semaphore默认采用非公平策略,如果需要使用公平策略则可以使用带两个参数的构造函数来构造Semaphore对象。另外,如CountDownLatch构造函数传递的初始化信号量个数permits被赋给了AQS的state状态变量一样,这里AQS的state值也
表示当前持有的信号量个数。
下面来看Semaphore实现的主要方法。
3.2.1void acquire()方法
当前线程调用该方法的目的是希望获取一个信号量资源。如果当前信号量个数大于o,则当前信号量的计数会减1,然后该方法直接返回。否则如果当前信号量个数等于0,则当前线程会被放入AQS的阻塞队列。当其他线程调用了当前线程的interrupt()方法中
断了当前线程时,则当前线程会抛出InterruptedException异常返回。下面看下代码实现。
public void acquire() throws InterruptedException { //传递参数为1,说明要获取1个信号量资源 sync.acquireSharedInterruptibly(1); }public final void acquireSharedInterruptibly(int arg) throws InterruptedException { //(1)线程被中断,抛出异常 if (Thread.interrupted()) throw new InterruptedException(); //(2)否则调用Sync子类方法尝试获取,这里根据构造函数确定使用公平策略 if (tryAcquireShared(arg) < 0) //如果获取失败则放入阻塞队列。然后再次尝试,如果失败则调用park方法挂起当前线 doAcquireSharedInterruptibly(arg); }
由如上代码可知,acquire()在内部调用了Sync的acquireSharedlnterruptibly方法,后者会对中断进行响应(如果当前线程被中断,则抛出中断异常)。尝试获取信号量资源的AQS的方法tryAcquireShared是由Sync的子类实现的,所以这里分别从两方面来讨论。
先讨论非公平策略NonfairSync类的t叩AcquireShared方法,代码如下
protected int tryAcquireShared(int acquires) { return nonfairTryAcquireShared(acquires); }final int nonfairTryAcquireShared(int acquires) { for (;;) { //获取当前信号量 int available = getState(); //计算当前剩余值 int remaining = available - acquires; ///如果当前剩余位小于0或者CAS设置成功则返回 if (remaining < 0 || compareAndSetState(available, remaining)) return remaining; } }
如上代码先获取当前信号量值(available),然后减去需要获取的值(acquires),得到剩余的信号量个数(remaining),如果剩余值小于0则说明当前信号量个数满足不了需求,那么直接返回负数,这时当前线程会被放入AQS的阻塞队列而被挂起。如果剩余值大于0,则使用CAS操作设置当前信号量值为剩余值,然后返回剩余值。
另外,由于NonFairSync是非公平获取的,也就是说先调用aquire方法获取信号量的线程不一定比后来者先获取到信号量。考虑下面场景,如果线程A先调用了aquire()方法获取信号量,但是当前信号量个数为0,那么线程A会被放入AQS的阻塞队列。过一
段时间后线程C调用了release()方法释放了一个信号量,如果当前没有其他线程获取信号量,那么线程A就会被激活,然后获取该信号量,但是假如线程C释放信号量后,线程C调用了aquire方法,那么线程C就会和线程A去竞争这个信号量资源。如果采用非公平策略,由nonfairTryAcquireShared的代码可知,线程C完全可以在线程A被激活前,或者激活后先于线程A获取到该信号量,也就是在这种模式下阻塞线程和当前请求的线程是竞争关系,而不遵循先来先得的策略。下面看公平性的FairSync类是如何保证公平性的。
protected int tryAcquireShared(int acquires) { for (;;) { if (hasQueuedPredecessors()) return -1; int available = getState(); int remaining = available - acquires; if (remaining < 0 || compareAndSetState(available, remaining)) return remaining; } }
可见公平性还是靠hasQueuedPredecessors这个函数来保证的。前面章节讲过,公平策略是看当前线程节点的前驱节点是否也在等待获取该资源,如果是则自己放弃获取的权限,然后当前线程会被放入AQS阻塞队列,否则就去获取。
3.2.2void acquire(int permits)方法
该方法与acquire()方法不同,后者只需要获取一个信号量值,而前者则获取permits个。
3.2.3void acquireUninterruptibly()方法
该方法与acquire()类似,不同之处在于该方法对中断不响应,也就是当当前线程调用了acquireUninterruptibly获取资源时(包含被阻塞后),其他线程调用了当前线程的interrupt()方法设置了当前线程的中断标志,此时当前线程并不会抛出InterruptedException异常而返回。
3.2.4void acquireUninterruptibly(intpermits)方法
该方法与acquire(intpermits)方法的不同之处在于,该方法对中断不响应。
3.2.5void release()方法
该方法的作用是把当前Semaphore对象的信号量值增加1,如果当前有线程因为调用aquire方法被阻塞而被放入了AQS的阻塞队列,则会根据公平策略选择一个信号量个数能被满足的线程进行激活,激活的线程会尝试获取刚增加的信号量,下面看代码实现。
public void release() { //(1)arg=1 sync.releaseShared(1); }public final boolean releaseShared(int arg) { //(2)尝试释放资源 if (tryReleaseShared(arg)) { //(3)资源释放成功则调用park方法唤醒AQS队列里面最先挂起的线程 doReleaseShared(); return true; } return false; }protected final boolean tryReleaseShared(int releases) { for (;;) { //(4)获取当前信号量 int current = getState(); //(5)将当前信号量+releases,这里加1 int next = current + releases; if (next < current) // overflow throw new Error("Maximum permit count exceeded"); //(6)使用CAS保证更新信号量的原子性 if (compareAndSetState(current, next)) return true; } }
由代码release()->sync.releaseShared(1)可知,release方法每次只会对信号量值增加1,tryReleaseShared方法是无限循环,使用CAS保证了release方法对信号量递增1的原子性操作。tryReleaseShared方法增加信号量值成功后会执行代码(3),即调用AQS的方法来激活因为调用aquire方法而被阻塞的线程。
3.3小结
本节首先通过案例介绍了Semaphore的使用方法.然后介绍了Semaphore的源码实现,Semaphore也是使用AQS实现的,并且获取信号量时有公平策略和非公平策略之分。
总结
本文介绍了并发包中关于线程协作的一些重要类。
首先CountDownLatch通过计数器提供了更灵活的控制,只要检测到计数器值为0,就可以往下执行,这相比使用join必须等待线程执行完毕后主线程才会继续向下运行更灵活。另外,CyclicBarrier也可以达到CountDownLatch的效果,但是后者在计数器值变为0后,就不能再被复用,而前者则可以使用reset方法重置后复用,前者对同一个算法但是输入参数不同的类似场景比较适用。最后介绍了Semaphore的使用方法.然后介绍了Semaphore的源码实现,Semaphore也是使用AQS实现的,并且获取信号量时有公平策略和非公平策略之分。使用本章介绍的类会大大减少你在Java中使用wait、notify等来实现线程同步的代码量,在日常开发中当需要进行线程同步时使用这些同步类会节省很多代码并且可以保证正确性。