线程基础
面试题:线程和进程的区别?
- 进程是正在运行程序的实例,进程中包含了线程,每个线程执行不同的任务
- 不同的进程使用不同的内存空间,在当前进程下的所有线程可以共享内存空间
- 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低(上下文切换指的是从一个线程切换到另一个线程)
面试题:并行和并发有什么区别?
现在都是多核CPU,在多核CPU下
- 并发是同一时间应对多件事情的能力,多个线程轮流使用一个或多个CPU
- 并行是同一时间动手做多件事情的能力,4核CPU同时执行4个线程
面试题:创建线程的方式有哪些?
共有四种方式可以创建线程,分别是:
- 继承Thread类
- 实现runnable接口
- 实现Callable接口
- 线程池创建线程
面试题:runnable 和 callable 有什么区别?
- Runnable 接口run方法没有返回值
- Callable接口call方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果
- Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛
面试题:线程的 run()和 start()有什么区别?
start(): 用来启动线程,通过该线程调用run方法执行run方法中所定义的逻辑代码。start方法只能被调用一次。
run(): 封装了要被线程执行的代码,可以被调用多次。
面试题:线程包括哪些状态,状态之间是如何变化的
新建(NEW)、可运行(RUNNABLE)、阻塞(BLOCKED)、等待( WAITING )、时间等待(TIMED_WALTING)、终止(TERMINATED)
- 创建线程对象是新建状态
- 调用了start()方法转变为可执行状态
- 线程获取到了CPU的执行权,执行结束是终止状态
- 在可执行状态的过程中,如果没有获取CPU的执行权,可能会切换其他状态
- 如果没有获取锁(synchronized或lock)进入阻塞状态,获得锁再切换为可执行状态
- 如果线程调用了wait()方法进入等待状态,其他线程调用notify()唤醒后可切换为可执行状态
- 如果线程调用了sleep(50)方法,进入计时等待状态,到时间后可切换为可执行状态
面试题:新建 T1、T2、T3 三个线程,如何保证它们按顺序执行?
可以使用线程中的join方法解决
面试题:notify()和 notifyAll()有什么区别?
- notifyAll:唤醒所有wait的线程
- notify:只随机唤醒一个 wait 线程
面试题:java中wait和sleep方法的不同?
共同点
- wait() ,wait(long) 和 sleep(long) 的效果都是让当前线程暂时放弃 CPU 的使用权,进入阻塞状态
不同点
- 类的不同:sleep() 是 Thread线程类的静态方法,wait() 是 Object类的方法。
- 是否释放锁:sleep() 不释放锁;wait() 释放锁。
- 用途不同:Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。
- 用法不同:wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)超时后线程会自动苏醒。
面试题:如何停止一个正在运行的线程?
有三种方式可以停止线程
- 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止
- 使用stop方法强行终止(不推荐,方法已作废)
- 使用interrupt方法中断线程
- 打断阻塞的线程( sleep,wait,join )的线程,线程会抛出InterruptedException异常
- 打断正常的线程,可以根据打断状态来标记是否退出线程
面试题:synchronized关键字的底层原理
- Synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】
- 它的底层由monitor实现的,monitor是jvm级别的对象( C++实现),线程获得锁需要使用对象(锁)关联monitor
- 在monitor内部有三个属性,分别是owner、entrylist、waitset
- 其中owner是关联的获得锁的线程,并且只能关联一个线程;entrylist关联的是处于阻塞状态的线程;waitset关联的是处于Waiting状态的线程
面试题:Monitor实现的锁属于重量级锁,你了解过锁升级吗?
- Monitor实现的锁属于重量级锁,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。
- 在JDK 1.6引入了两种新型锁机制:偏向锁和轻量级锁,它们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下因使用传统锁机制带来的性能开销问题。
面试题:你谈谈 JMM(Java内存模型)
- JMM(Java Memory Model)Java内存模型,定义了共享内存中多线程程序读写操作的行为规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性
- JMM把内存分为两块,一块是私有线程的工作区域(工作内存),一块是所有线程的共享区域(主内存)
- 线程跟线程之间是相互隔离,线程跟线程交互需要通过主内存
面试题:讲一下CAS
- 需要读写内存值V;进行比较的值A;准备写入的值B
- 当且仅当V的值等于A的值等于V的值的时候,才用B的值去更新V的值,否则不会执行任何操作(比较和替换是一个原子操作-A和V比较,V和B替换),一般情况下是一个自旋操作,即不断重试。
- CAS的全称是: Compare And Swap(比较再交换);它体现的一种乐观锁的思想,在无锁状态下保证线程操作数据的原子性。
- CAS使用到的地方很多:AQS框架、AtomicXXX类
- 在操作共享变量的时候使用的自旋锁,效率上更高一些
- CAS的底层是调用的Unsafe类中的方法,都是操作系统提供的,其他语言实现
面试题:乐观锁和悲观锁的区别
CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。
synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
面试题:请谈谈你对 volatile 的理解
①保证线程间的可见性
用 volatile 修饰共享变量,能够防止编译器等优化发生,让一个线程对共享变量的修改对另一个线程可见
② 禁止进行指令重排序
指令重排:用 volatile 修饰共享变量会在读、写共享变量时加入不同的屏障,阻止其他读写操作越过屏障,从而达到阻止重排序的效果
面试题:volatile如何保证线程间的可见性?
通过内存屏障和缓存一致性协议实现
volatile关键字通过以下机制保证多线程环境下的可见性:
- 强制主内存读写
- 被volatile修饰的变量,每次读写都直接操作主内存,而非线程本地缓存。写操作会立即刷新到主内存,读操作会从主内存重新加载最新值。
- 示例:若线程A修改了volatile变量,线程B读取时会强制从主内存获取新值,避免读取本地过期的缓存。
- 插入内存屏障(Memory Barrier)
- 写屏障:在volatile写操作后插入,确保写操作前的所有修改同步到主内存。
- 读屏障:在volatile读操作前插入,确保后续读取能获取主内存最新值。
- 屏障还禁止指令重排序,防止编译器或CPU优化破坏可见性。
- 基于缓存一致性协议(如MESI)
- 多核CPU中,volatile通过MESI协议(或类似协议)使其他核的缓存行失效。当线程修改volatile变量时,其他核的缓存会被标记为无效(Invalid),强制重新从主内存加载。
- 核心状态包括:Modified(修改)、Exclusive(独占)、Shared(共享)、Invalid(无效)。
面试题:什么是AQS?
- 是多线程中的队列同步器。是一种锁机制,它是做为一个基础框架使用的,像ReentrantLock、Semaphore都是基于AQS实现的
- AQS内部维护了一个先进先出的双向队列,队列中存储的排队的线程
- 在AQS内部还有一个属性state,这个state就相当于是一个资源,默认是0(无锁状态),如果队列中的有一个线程修改成功了state为1,则当前线程就相等于获取了资源
- 在对state修改的时候使用的cas操作,保证多个线程修改的情况下原子性
面试题:ReentrantLock的实现原理
- ReentrantLock表示支持重新进入的锁,调用 lock 方 法获取了锁之后,再次调用 lock,是不会再阻塞
- ReentrantLock主要利用CAS+AQS队列来实现
- 支持公平锁和非公平锁,在提供的构造器的中无参默认是非公平锁,也可以传参设置为公平锁
面试题:synchronized和Lock有什么区别 ?
-
语法层面
synchronized 是关键字,源码在 jvm 中,用 c++ 语言实现
Lock 是接口,源码由 jdk 提供,用 java 语言实现
使用 synchronized 时,退出同步代码块锁会自动释放,而使用 Lock 时,需要手动调用 unlock 方法释放锁 -
功能层面
二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
Lock 提供了许多 synchronized 不具备的功能,例如公平锁、可打断、可超时、多条件变量
Lock 有适合不同场景的实现,如 ReentrantLock, ReentrantReadWriteLock(读写锁) -
性能层面
在没有竞争时,synchronized 做了很多优化,如偏向锁、轻量级锁,性能不赖
在竞争激烈时,Lock 的实现通常会提供更好的性能
面试题:死锁产生的条件是什么?
死锁:一个线程需要同时获取多把锁,这时就容易发生死锁
面试题:如何进行死锁诊断?
- 当程序出现了死锁现象,我们可以使用jdk自带的工具:jps和 jstack
- jps:输出JVM中运行的进程状态信息
- jstack:查看java进程内线程的堆栈信息,查看日志,检查是否有死锁
如果有死锁现象,需要查看具体代码分析后,可修复 - 可视化工具jconsole、VisualVM也可以检查死锁问题
聊一下ConcurrentHashMap
- 底层数据结构:
- JDK1.7底层采用分段的数组+链表实现
- JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树
- 加锁的方式
- JDK1.7采用Segment分段锁,底层使用的是ReentrantLock
- JDK1.8采用CAS添加新节点,采用synchronized锁定链表或红黑二叉树的首节点,相对Segment分段锁粒度更细,性能更好
面试题:Java程序中怎么保证多线程的执行安全
- 原子性 synchronized、lock
- 内存可见性 volatile、synchronized、lock
- 有序性 volatile
线程同步
面试题:如何实现线程的同步?
线程的同步是为了保证多个线程按照特定的顺序、协调地访问共享资源,避免数据不一致和竞争条件等问题
在Java中,常见的线程同步方式有以下几种:
- 使用synchronized关键字:通过在方法或代码块前加上 synchronized关键字,确保同一时间只有一个线程可以执行标记为同步的代码。这样可以避免多个线程同时访问共享资源造成的数据不一致问题。
- 使用ReentrantLock类:它是一个可重入锁,通过调用
lock()
和unlock()
方法获取和释放锁。与synchronized
不同,ReentrantLock
提供了更灵活的同步控制,例如可实现公平性和试锁等待时间。 - 使用wait()、notify()和notifyAll()方法:这些方法是
Object
类的方法,允许线程间进行协作和通信,通过调用wat()
方法使线程进入等待状态,然后其他线程可以通过notify()
或notifyAIl()
方法唤醒等待的线程。 - 使用CountDownLatch和CyclicBarrier:它们是并发工具类,用于线程之间的同步和等待。
CountDownLatch
可用于等待一组线程完成操作,而CyclicBarrier
用于等待一组线程互相达到屏障位置,选择适合的同步方式会根据具体需求和场景而定。在使用任何同步机制时,需要注意避免死锁和性能问题,合理设计同步范围和粒度。
如何保证线程安全?
- 首先考虑使用不可变对象,这是最简单的线程安全实现;
- 对于需要共享的可变状态,可以使用同步控制如synchronized或Lock;
- Java并发包提供了许多线程安全容器和工具类,例如:ConcurrentHashMap、CopyOnWriteArrayList、CountDownLatch;
- 对于特定场景,可以使用volatile保证可见性或原子类实现无锁编程。
在实际项目中,我会根据具体场景选择最合适的方案,比如读多写少时考虑CopyOnWriteArrayList,高并发计数使用AtomicLong等。
面试题:如何唤醒阻塞的线程?
- 对于wait()阻塞的线程,可以使用notify()/notifyAll();
- 对于sleep()/join()阻塞的,可以通过interrupt()中断;
- 使用Lock/Condition时,调用signal()/signalAll();
- 对LockSupport.park()阻塞的,使用unpark()精确唤醒。
在实际开发中,我会优先考虑JUC包中的高级同步工具,它们提供了更安全易用的API。同时需要注意正确处理中断,避免丢失唤醒和虚假唤醒问题,通常采用循环检查条件的方式确保正确性
面试题:Synchorized对不同方法加锁,访问有什么影响?
- 同一对象的不同 synchronized 实例方法:互斥访问,其他线程不能同时调用该对象的其他 synchronized 实例方法。
- 不同对象的 synchronized 实例方法:互不干扰,各自锁住自己的对象实例。
- synchronized 静态方法:全局互斥,所有线程共享同一个锁(类的 Class 对象),与 synchronized 实例方法也互斥。
- synchronized 块:根据锁对象的不同,其行为和互斥关系也不同,锁住同一个对象则互斥,锁住不同对象则互不干扰。
面试题:Synchronized和ReentrantLock的挂起逻辑
- synchronized中的wait和notify具体的逻辑?
- 在线程持有synchronized锁时,可以执行wait方法,释放掉锁资源,并且挂起当前线程。
- 在线程持有synchronized锁时,可以执行notify/notifyAll方法,唤醒上面基于wait方法挂起的线程。
- ReentrantLock中Condition中支持了类似上述的功能:
- 在持有lock锁的前提下,可以执行await方法,释放掉锁资源,并且挂起当前线程。
- 在持有lock锁的前提下,可以执行signal/signalAll方法,唤醒上面基于await方法挂起的线程。
面试题:Java中的锁有哪些?
- 按线程安全策略划分
- 悲观锁:假设并发冲突必然发生,直接加锁保护资源。例如synchronized关键字和ReentrantLock。
- 乐观锁:假设并发冲突较少,通过版本控制(如CAS操作)实现。例如Atomic原子类和StampedLock的乐观读模式。
- 按锁的获取顺序划分
- 公平锁:严格按照线程请求顺序分配锁,如ReentrantLock(true)。
- 非公平锁:允许插队获取锁,默认的synchronized和ReentrantLock均为非公平锁,性能更高但可能引发线程饥饿。
- 按锁的重入性划分
- 可重入锁:同一线程可重复获取已持有的锁,如synchronized和ReentrantLock,避免死锁。
- 不可重入锁:较少见,需手动实现。
- 按锁的实现方式划分
- 内置锁(隐式锁):通过synchronized关键字实现,自动释放。
显式锁:如ReentrantLock、StampedLock,需手动加解锁,支持更灵活的功能(如超时、中断)。 - 按锁的粒度与优化划分
- 分段锁:减小锁粒度提升并发,如ConcurrentHashMap的分段设计。
- 偏向锁/轻量级锁/重量级锁:JVM对synchronized的优化状态,分别针对无竞争、短时间竞争和长时间竞争场景。
- 按读写权限划分
- 读写锁:ReentrantReadWriteLock实现读共享、写独占,适合读多写少场景。
- StampedLock:JDK8引入,支持写锁、悲观读锁和高效的乐观读锁。
- 其他特殊锁
- 自旋锁:通过循环CAS避免线程切换,适合短时等待(如AtomicBoolean)。
- 条件锁(Condition):配合ReentrantLock实现精准线程唤醒。
- 信号量(Semaphore):控制多线程并发访问数量。
- 关键点总结:
- 最常用锁:synchronized(内置锁)和ReentrantLock(显式锁)是基础。
- 高性能选择:读多写少场景优先考虑StampedLock或读写锁。
- JVM优化:偏向锁已从JDK15开始废弃,轻量级锁和重量级锁仍是synchronized的核心优化方向。
线程池
面试题:线程池的作用?
- 提高性能:通过复用线程,减少线程创建和销毁的开销。
- 控制线程数量:通过设置最大线程数,避免系统资源被过多的线程耗尽。
- 提高资源利用率:通过合理配置线程池的参数,提高资源的利用率。
- 任务排队管理:通过任务队列,合理管理任务的排队和执行顺序。
- 线程生命周期管理:通过设置线程池的参数,合理管理线程的生命周期。
- 提供多种策略:通过设置线程池的策略,合理处理任务的拒绝和线程的生命周期。
面试题:线程池中线程异常后,销毁还是复用?
在线程池中,线程异常后的处理分两种情况:
- 对于未捕获的异常,默认会导致线程终止销毁,线程池会创建新线程替代;
- 如果使用submit提交任务或内部捕获了异常,线程可以继续复用。
在实际开发中,我会:
- 优先使用submit()提交任务以便获取异常信息
- 在任务内部做好异常处理
- 必要时设置UncaughtExceptionHandler
- 对于关键任务考虑使用Future获取执行状态
这样可以保证线程池的稳定性和异常可追溯性。
面试题:说一下线程池的核心参数
- corePoolSize 核心线程数目
- maximumPoolSize 最大线程数目 = (核心线程+救急线程的最大数目)
- keepAliveTime 生存时间 - 救急线程的生存时间,生存时间内没有新任务,此线程资源会释放
- unit 时间单位 - 救急线程的生存时间单位,如秒、毫秒等
- workQueue - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务
- threadFactory 线程工厂 - 可以定制线程对象的创建,例如设置线程名字、是否是守护线程等
- handler 拒绝策略 - 当所有线程都在繁忙,workQueue 也放满时,会触发拒绝策略
面试题:线程池的执行原理知道嘛
面试题:线程池中有哪些常见的阻塞队列
1.ArrayBlockingQueue:基于数组结构的有界阻塞队列,FIFO。
2.LinkedBlockingQueue:基于链表结构的有界阻塞队列,FIFO。
3.DelayedWorkQueue :是一个优先级队列,它可以保证每次出队的任务都是当前队列中执行时间最靠前的
4.SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作。
如何确定核心线程数?
① 高并发、任务执行时间短 ( CPU核数+1 ),减少线程上下文的切换
② 并发不高、任务执行时间长
IO密集型的任务 (CPU核数 * 2 + 1)
计算密集型任务 ( CPU核数+1 )
③ 并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考(2)
面试题:线程池的种类有哪些
- newFixedThreadPool:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待
- newSingleThreadExecutor:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任 务,保证所有任务按照指定顺序(FIFO)执行
- newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程
- newScheduledThreadPool:可以执行延迟任务的线程池,支持定时及周期性任务执行
面试题:为什么不建议用Executors创建线程池
线程池使用场景
面试题:你们项目哪里用到了多线程
- 批量导入:使用了线程池+CountDownLatch批量把数据库中的数据导入到了ES(任意)中,避免OOM
- 数据汇总:调用多个接口来汇总数据,如果所有接口(或部分接口)的没有依赖关系,就可以使用线程池+future来提升性能
- 异步线程(线程池):为了避免下一级方法影响上一级方法(性能考虑),可使用异步线程调用下一个方法(不需要下一级方法返回值),可以提升方法响应时间
面试题:如何控制某个方法允许并发访问线程的数量
在多线程中提供了一个工具类Semaphore,信号量。在并发的情况下,可以控制方法的访问量
- 创建Semaphore对象,可以给一个容量
- acquire()可以请求一个信号量,这时候的信号量个数-1
- release()释放一个信号量,此时信号量个数+1
ThreadLocal
面试题:谈谈你对ThreadLocal的理解
- ThreadLocal 可以实现【资源对象】的线程隔离,让每个线程各用各的【资源对象】,避免争用引发的线程安全问题
- ThreadLocal 同时实现了线程内的资源共享
- 每个线程内有一个 ThreadLocalMap 类型的成员变量,用来存储资源对象
3.1 调用 set 方法,就是以 ThreadLocal 自己作为 key,资源对象作为 value,放入当前线
程的 ThreadLocalMap 集合中
3.2 调用 get 方法,就是以 ThreadLocal 自己作为 key,到当前线程中查找关联的资源值
3.3 调用 remove 方法,就是以 ThreadLocal 自己作为 key,移除当前线程关联的资源值 - ThreadLocal内存泄漏问题
ThreadLocalMap 中的 key 是弱引用,值为强引用; key 会被GC 释放内存,关联 value 的内存并不会释放。建议主动 remove 释放 key,value
面试题:ThreadLocal应用场景
ThreadLocal在线程池中的应用场景
ThreadLocal结合线程池在日常开发中有多种应用场景,主要利用其线程隔离特性来保存线程相关的上下文信息。以下是一些典型应用场景:
-
用户会话信息传递
在Web应用中,当使用线程池处理异步请求时,可以将用户会话信息存储在ThreadLocal中:// 定义ThreadLocal存储用户信息 private static final ThreadLocal<User> currentUser = new ThreadLocal<>(); // 线程池任务执行前设置用户信息 executorService.submit(() -> { try { currentUser.set(SecurityContext.getCurrentUser()); // 执行业务逻辑,可以随时获取用户信息 doBusiness(); } finally { currentUser.remove(); // 必须清理,避免内存泄漏 } });
-
数据库连接管理
在ORM框架中,常用ThreadLocal来管理数据库连接,确保同一线程使用同一个连接:private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<>(); public static Connection getConnection() { Connection conn = connectionHolder.get(); if (conn == null) { conn = dataSource.getConnection(); connectionHolder.set(conn); } return conn; }
-
分布式跟踪ID传递
在微服务架构中,使用ThreadLocal传递跟踪ID:private static final ThreadLocal<String> traceIdHolder = new ThreadLocal<>(); // 线程池任务执行前设置traceId executorService.execute(() -> { try { traceIdHolder.set(UUID.randomUUID().toString()); // 跨方法调用时都可以获取到同一个traceId processRequest(); } finally { traceIdHolder.remove(); } });
-
动态数据源切换
在多数据源应用中,使用ThreadLocal保存当前线程的数据源key:private static final ThreadLocal<String> dataSourceKey = new ThreadLocal<>(); public static void setDataSourceKey(String key) { dataSourceKey.set(key); } public static String getDataSourceKey() { return dataSourceKey.get(); }
-
性能监控
记录方法执行时间等性能指标:private static final ThreadLocal<Long> startTime = new ThreadLocal<>(); public void executeTask() { executorService.submit(() -> { try { startTime.set(System.currentTimeMillis()); // 执行业务逻辑 doWork(); long duration = System.currentTimeMillis() - startTime.get(); log.info("Task executed in {} ms", duration); } finally { startTime.remove(); } }); }
-
使用注意事项
- 必须清理:在线程池中使用ThreadLocal后必须调用remove(),否则可能导致内存泄漏
- 避免跨线程传递:ThreadLocal的值不能自动传递给子线程
- 考虑使用InheritableThreadLocal:如果需要父子线程传递值,可以使用InheritableThreadLocal
在线程池环境中,由于线程会被重用,不清理ThreadLocal可能导致信息混乱或内存泄漏,因此finally块中的remove()调用尤为重要。
面试题:CycliBarriar 和 CountdownLatch 有什么区别?
CountDownLatch与CyclicBarrier都是用于控制并发的工具类,都可以理解成维护的就是一个计数器,但是这两者还是各有不同侧重点的:
- CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;CountDownLatch强调一个线程等多个线程完成某件事情。CyclicBarrier是多个线程互等,等大家都完成,再携手共进。
- 调用CountDownLatch的countDown方法后,当前线程并不会阻塞,会继续往下执行;而调用CyclicBarrier的await方法,会阻塞当前线程,直到CyclicBarrier指定的线程全部都到达了指定点的时候,才能继续往下执行;
- CountDownLatch方法比较少,操作比较简单,而CyclicBarrier提供的方法更多,比如能够通过getNumberWaiting(),isBroken()这些方法获取当前多个线程的状态,并且CyclicBarrier的构造方法可以传入 barrierAction,指定当所有线程都到达时执行的业务功能;
- CountDownLatch是不能复用的,而CyclicLatch是可以复用的。