Java “并发工具类”面试清单(含超通俗生活案例与深度理解)

一、请解释 CountDownLatch(倒计数器)的核心作用,并结合生活案例说明其常见应用场景?核心方法有哪些?

CountDownLatch 是 Java 并发包中用于协调多线程同步的工具类,其核心逻辑是通过一个“不可重置的倒计数器”,让一个或多个线程(等待线程)暂停执行,直到其他指定数量的线程(任务线程)完成各自操作、计数器减至 0 后,等待线程才会继续执行。简单来说,它就像“集体活动中的‘等人’环节”——必须等所有该到的人/事到位,后续动作才能开始,且一旦人齐了,这个“等人”的过程就不会重复。

从实际使用来看,CountDownLatch 主要有两大典型应用场景,结合生活中常见的场景能更清晰理解:

场景一:统一控制线程开始时机

比如公司组织团建去郊区游玩,10 名员工各自开车前往集合点,必须等所有人都到齐(主线程调用 await()),导游才会说“出发”(主线程调用 countDown(1),计数器从 1 归 0),避免有人掉队。这里还可以延伸到“超时等待”的场景:如果约定早上 8 点集合,超过 8 点 15 分还有 2 名员工没到,老师不会一直等(避免耽误其他 48 名员工的时间),此时会调用 await(15, TimeUnit.MINUTES) 方法,15 分钟超时后,不管计数器是否归 0,都会唤醒等待线程,决定“先出发,让迟到员工的家长送去郊区汇合”。

场景二:等待所有线程完成任务后再执行后续操作

比如班级组织爬山,老师要求“所有人爬到山顶后再合影”,山顶就是集合点(计数器初始为 30,对应 30 名学生),每个学生爬到山顶后就“报到”(调用 countDown()),最后一个学生报到后(计数器归 0),老师才会组织大家合影(主线程继续执行)。

CountDownLatch 的核心方法需结合实际用途理解,每个方法都对应具体场景需求:

• await():等待线程调用该方法后,会进入阻塞状态,直到计数器减至 0 才会被唤醒;如果等待过程中线程被中断,会抛出 InterruptedException。比如春游时学生等发车信号,没收到信号就一直等。

• countDown():任务线程调用该方法后,计数器会减 1(每次减 1,不可反向增加);即使多个线程同时调用,计数器的递减也是线程安全的(底层通过 CAS 操作保证)。比如爬山时,每个学生到山顶后调用一次,计数器逐步减少。

• await(long timeout, TimeUnit unit):带超时时间的等待方法,等待线程会阻塞指定时长,如果超时前计数器归 0,线程正常唤醒;如果超时后计数器仍未归 0,线程也会唤醒,但会返回 false,后续可根据返回值做特殊处理(比如春游超时后决定不等迟到学生)。

• getCount():获取当前计数器的剩余值,可用于实时查看任务完成进度。比如爬山时,老师调用该方法查看“还剩几个学生没到山顶”,如果返回 5,就知道还有 5 名学生在攀爬。

二、什么是 CyclicBarrier(同步屏障)?它的“可循环”特性该如何理解?请结合生活案例说明其应用场景与核心方法?

CyclicBarrier 是 Java 并发包中另一种多线程同步工具,字面意思是“可循环使用的同步屏障”。它的核心逻辑是:预先约定一组线程,每个线程执行到指定的“屏障点”时,都会暂停下来等待其他线程;只有当这组线程中的最后一个线程也到达屏障点后,“屏障”才会打开,所有暂停的线程同时被唤醒,继续执行后续操作。而“可循环”是它与 CountDownLatch 最显著的区别——屏障打开后,可通过重置恢复初始状态,支持同一组线程重复进行“到达屏障-等待-唤醒”的流程,就像“每周重复的集体活动,每次都要等所有人到齐才能开始”。

要理解 CyclicBarrier 的“可循环”和应用场景,结合生活中高频出现的“周期性集体任务”会更直观:

案例一:小区物业的“每周公共区域清洁”任务

某小区物业规定,每周二、周五下午 2 点,需要 5 名保洁员同时对小区的“中心花园、单元楼道、地下车库、健身区、垃圾站”5 个区域做清洁;为了保证清洁效率,要求“所有保洁员必须同时开始清洁”——不能有人提前打扫,也不能有人迟到导致部分区域没人管。而且每周的这两天都要重复这个流程,这就完美契合 CyclicBarrier 的“可循环+屏障同步”特性。

具体对应 CyclicBarrier 的逻辑:

1. 初始化 CyclicBarrier:创建实例时,指定“参与线程数”为 5(对应 5 名保洁员),还可以传入一个 Runnable 对象(比如“清洁前组长强调安全注意事项”,这个操作会在最后一个保洁员到达屏障时执行);

2. 周二下午的清洁流程:

◦ 保洁员 A(线程 1)13:50 到达小区门口(屏障点),调用 await() 方法,发现自己不是最后一个到的,就暂停等待;

◦ 保洁员 B(线程 2)13:55 到达,同样调用 await() 暂停;

◦ 保洁员 C、D 陆续到达,都暂停等待;

◦ 14:00 保洁员 E(线程 5,最后一个)到达,调用 await() 方法——此时所有 5 个线程都到齐,屏障打开;

◦ 屏障打开时,先执行构造方法中传入的 Runnable(组长强调安全事项),然后所有暂停的保洁员同时开始清洁各自负责的区域;

3. 周五下午的清洁流程:

◦ 周二的清洁任务完成后,CyclicBarrier 可以通过 reset() 方法重置为初始状态(参与线程数仍为 5);

◦ 5 名保洁员再次按约定时间到达小区门口,重复周二的“到达-等待-唤醒”流程,屏障再次打开,开始周五的清洁;

◦ 这个过程可以每周重复,直到物业调整清洁计划,体现了 CyclicBarrier 的“可循环”特性。

案例二:公司“月度部门总结会”

某互联网公司的产品部门,每月最后一个周五下午 4 点要开“月度总结会”,要求部门的 8 名成员(产品经理、UI 设计师、需求分析师等)全部到场才能开始——如果有人出差,会推迟到所有人能参会的时间,但只要开会,就必须等所有人到齐。

这里的“每月一次总结会”就是“循环”,“等 8 人全部到场”就是“屏障”:

• 每月开会前,初始化 CyclicBarrier(参与线程数 8),部门成员(线程)陆续到达会议室(屏障点),调用 await() 等待;

• 最后一个成员到齐后,屏障打开,会议开始;

• 下个月重复这个过程,无需重新创建 CyclicBarrier,只需重置即可,这也是“可循环”的体现。

CyclicBarrier 的核心方法需结合“屏障同步”和“可循环”特性理解,每个方法都服务于“线程集合-唤醒-重置”的流程:

• await():这是 CyclicBarrier 最核心的方法,线程执行到屏障点时调用。如果当前线程不是最后一个到达的,会进入阻塞状态,直到所有线程到齐;如果是最后一个到达的,会先执行构造方法中传入的 Runnable(若有),再唤醒所有阻塞线程,同时屏障会自动准备好下一次使用(无需手动重置,除非主动调用 reset())。比如保洁员到达后调用 await(),最后一个保洁员调用时触发唤醒和前置操作。

• await(long timeout, TimeUnit unit):带超时时间的等待方法。线程到达屏障点后,若在指定时间内其他线程仍未到齐,会抛出 TimeoutException,同时该线程会退出等待,且 CyclicBarrier 会进入“破损状态”(broken),此时其他仍在等待的线程会收到 BrokenBarrierException。比如保洁员 E 迟到超过 30 分钟,保洁员 A 等待时触发超时,A 抛出异常退出,其他保洁员也会收到异常,当天的清洁任务可能需要重新安排。

• reset():将 CyclicBarrier 重置为初始状态,无论当前是否有线程在等待,都会唤醒所有等待线程并标记为“屏障破损”。比如周五清洁前,发现周二的 CyclicBarrier 状态异常,调用 reset() 恢复初始状态,确保周五的流程正常。

• getNumberWaiting():获取当前正在屏障点等待的线程数量。比如部门总结会时,主持人调用该方法,发现有 3 人还在等待,就知道还有 3 名成员没到。

• isBroken():判断 CyclicBarrier 是否处于“破损状态”(比如线程等待超时、被中断导致屏障未正常打开)。比如保洁员等待时有人触发超时,调用该方法会返回 true,此时需要处理破损状态,避免后续线程异常。

三、CountDownLatch 和 CyclicBarrier 都用于多线程同步,二者的核心区别是什么?实际开发中该如何选择?请结合案例对比说明?

CountDownLatch 和 CyclicBarrier 虽都能实现多线程间的等待同步,但二者的设计初衷、核心特性和适用场景有本质区别,不能混用。理解区别的关键在于抓住“是否可循环”“线程协作方式”“设计核心对象”“异常影响范围”四个维度,结合具体案例对比会更清晰。

区别一:是否可循环使用——一次性 vs 可重复

这是二者最直观的区别:CountDownLatch 的计数器一旦减至 0,就彻底失效,无法重置,只能使用一次;而 CyclicBarrier 的屏障可通过 reset() 重置,支持同一组线程重复进行“等待-唤醒”流程,可循环使用。

案例对比:

• CountDownLatch(一次性):公司年度年会筹备
公司筹备年度年会,需要完成“场地租赁、节目排练、礼品采购、流程策划”4 个任务,只有这 4 个任务全部完成,才能开始“年会检票入场”。这里的 CountDownLatch 初始计数器设为 4,每个任务完成后 countDown(),计数器从 4 减至 0 后,“检票入场”开始;一旦年会结束,这个 CountDownLatch 就没用了——明年筹备年会时,需要重新创建新的 CountDownLatch,因为去年的计数器已经归 0 且无法重置。

• CyclicBarrier(可循环):小区每周“垃圾分类宣传”
小区物业每周三、周日上午 10 点,需要 3 名志愿者在小区门口做“垃圾分类宣传”,每次都要等 3 人到齐才能开始。第一次(周三)用 CyclicBarrier 让 3 人同步;第二次(周日)无需重新创建,只需调用 reset() 重置屏障,3 名志愿者再次同步;下周还能继续用,体现了“可循环”,避免重复创建对象的开销。

区别二:线程协作方式——“任务完成即离开” vs “线程互相等待”

CountDownLatch 中,“任务线程”只需完成自己的操作、调用 countDown() 后即可离开,无需等待其他任务线程;等待线程(如“检票入场”线程)只等计数器归 0,不关心任务线程的后续动作。而 CyclicBarrier 中,所有参与的线程都需要等待彼此——每个线程到达屏障点后,必须暂停等待其他线程,直到最后一个线程到齐,所有线程才会一起继续执行,没有“任务线程”和“等待线程”的区分。

案例对比:

• CountDownLatch:班级组织“元旦联欢会”布置教室
老师安排“小明贴窗花、小红挂气球、小刚摆桌椅、小丽买零食”4 个任务,要求“所有任务完成后,大家一起坐下来彩排节目”。这里:

◦ 小明贴完窗花(任务完成),可以先休息,不用等小红挂气球;

◦ 小红挂完气球(任务完成),也可以先看手机,不用等小刚;

◦ 直到小丽买完零食(最后一个任务),计数器归 0,老师(等待线程)才会喊“大家过来彩排”——此时不管小明、小红之前在做什么,都会过来彩排,任务线程无需互相等待。

• CyclicBarrier:朋友组队“周末自驾游”集合
5 个朋友约定周六早上 9 点在高速口集合,一起开车去景区。这里:

◦ 朋友 A 8:50 到高速口,不能自己先上高速,必须等其他人;

◦ 朋友 B 8:55 到,同样要等;

◦ 直到朋友 E 9:00 到(最后一个),所有人才能一起上高速——所有参与的“线程(朋友)”都在等待彼此,没有谁完成任务就离开,必须一起继续后续动作(自驾游)。

区别三:设计核心对象——面向“任务数量” vs 面向“线程数量”

CountDownLatch 的设计核心是“等待指定数量的任务完成”,不关心这些任务由哪个线程执行,只要任务总数达标、计数器归 0 即可;而 CyclicBarrier 的设计核心是“等待指定数量的线程到达屏障点”,必须是预先约定的那组线程(或指定数量的线程)全部到场,屏障才会打开,与任务数量无关。

案例对比:

• CountDownLatch:学校“期中考试后批改试卷”
三年级有 3 个班,共 120 份数学试卷,老师安排“5 名老师一起批改,改完所有试卷后统计平均分”。这里 CountDownLatch 的计数器设为 120(对应 120 份试卷,即 120 个“批改任务”),不管是 5 名老师中的谁批改,每改完 1 份试卷就调用 countDown()(计数器减 1),直到 120 份试卷改完(计数器归 0),才开始统计平均分——核心是“任务数量(试卷数)”,不是“线程数量(老师数)”。

• CyclicBarrier:公司“新项目启动会”
公司启动一个新项目,要求“产品、研发、测试、运营、市场 5 个部门的负责人必须到场,会议才能开始”。这里 CyclicBarrier 的参与线程数设为 5(对应 5 个负责人),不管每个负责人当天有多少其他任务,只要 5 个人都到会议室(屏障点),会议就开始——核心是“线程数量(负责人数)”,不是“任务数量(负责人的其他工作)”。

区别四:异常影响范围——局部影响 vs 全局影响

CountDownLatch 中,若某个任务线程执行异常(如崩溃、中断),只会导致该任务对应的 countDown() 未被调用,计数器可能无法归 0,但其他任务线程不受影响,仍会正常完成任务;而 CyclicBarrier 中,若某个等待线程发生异常(如超时、中断),会导致 CyclicBarrier 进入“破损状态”,所有其他仍在等待的线程都会收到 BrokenBarrierException,整个同步流程被打断。

案例对比:

• CountDownLatch:社区“重阳节慰问老人”活动
社区安排“4 个小组分别去 4 个老人家里送慰问品,所有小组完成后汇总情况”。CountDownLatch 计数器设为 4:

◦ 小组 1、2、3 顺利完成任务,调用 countDown(),计数器减至 1;

◦ 小组 4 途中遇到堵车,手机没电无法联系(线程异常),未调用 countDown();

◦ 此时计数器停留在 1,汇总线程会一直等待(若用了超时方法,超时后会退出),但小组 1、2、3 的任务已完成,不受小组 4 异常的影响。

• CyclicBarrier:团队“周末羽毛球比赛”集合
8 名同事约定周六下午 2 点在羽毛球馆集合,一起比赛。CyclicBarrier 参与线程数设为 8:

◦ 7 名同事按时到达,调用 await() 等待;

◦ 最后 1 名同事路上发生交通事故(线程异常),未到达且触发了中断;

◦ 此时 CyclicBarrier 进入破损状态,正在等待的 7 名同事都会收到 BrokenBarrierException,当天的羽毛球比赛无法正常进行,异常影响了所有等待线程。

实际开发中的选择原则

结合上述区别,实际开发中选择的核心是“匹配需求的核心特性”:

1. 若需求是“一次性同步,等待多个任务完成后执行后续操作”,选 CountDownLatch
比如:项目上线前等待所有模块测试完成、数据迁移时等待所有数据表迁移完成、批量处理文件时等待所有文件解析完成。

2. 若需求是“周期性同步,需要同一组线程重复进行同步操作”,选 CyclicBarrier
比如:定时任务(每天凌晨同步数据,需多线程到齐后开始)、周期性测试(每周执行一次性能测试,需测试线程同步启动)、重复的集体任务(每小时执行一次日志清理,需多线程同步执行)。

3. 若核心是“等待任务数量达标”,选 CountDownLatch;若核心是“等待线程数量达标”,选 CyclicBarrier
比如:处理 1000 条数据(任务数量),用 CountDownLatch;等待 10 个线程同时开始处理(线程数量),用 CyclicBarrier。

4. 若担心“单个线程异常影响全局”,优先选 CountDownLatch;若需“线程间强同步,一个不到位则整体不执行”,选 CyclicBarrier
比如:数据统计任务中,个别任务失败不影响整体统计,用 CountDownLatch;重要会议需所有人到场,缺一人则不开会,用 CyclicBarrier。

四、请解释 Semaphore(信号量)的核心作用?结合生活案例说明其工作原理与核心方法?实际开发中有哪些典型应用场景?

Semaphore(信号量)是 Java 并发包中用于“控制共享资源并发访问数量”的工具类,其核心作用是通过“许可证(Permits)”机制,限制同时访问某一共享资源的线程数量,避免资源因过度竞争而导致的性能下降或异常。简单来说,它就像“餐厅的座位管理”——餐厅只有 10 个座位(许可证数量),客人(线程)必须拿到座位才能用餐,离开时释放座位,后面的客人才能继续使用,从而避免客人过多导致混乱。

核心工作原理:许可证的“获取-释放”循环

Semaphore 的工作逻辑围绕“许可证”展开,核心流程可概括为:

1. 初始化时指定“可用许可证数量”(即允许同时访问资源的最大线程数);

2. 线程需要访问共享资源时,先调用 acquire() 方法“申请许可证”:若有可用许可证,直接获取(许可证数量减 1),线程可访问资源;若没有可用许可证,线程会阻塞,直到有其他线程释放许可证;

3. 线程访问完资源后,调用 release() 方法“释放许可证”,许可证数量加 1,此时会唤醒一个正在阻塞等待许可证的线程,让其获取许可证访问资源;

4. 这个“申请-访问-释放”的循环会持续,直到所有线程都完成对资源的访问,整个过程中,同时访问资源的线程数始终不超过初始指定的许可证数量。

生活案例:小区停车场的车位管理

某小区有 20 个固定车位(共享资源),每天早高峰有 50 辆业主的车(线程)要进入停车场,为了避免车位满了还进车导致拥堵,物业安装了“车位管理系统”(Semaphore),核心逻辑就是 Semaphore 的工作原理:

1. 系统初始化时,设置“可用车位数量”为 20(对应 Semaphore 的许可证数量 20);

2. 业主 A 开车到停车场入口,系统检查“是否有可用车位”(线程调用 acquire()):若有,给业主 A 分配车位(许可证数量减 1,从 20 变 19),业主 A 开车进入停车场;若没有(20 个车位都满了),业主 A 需在入口排队等待(线程阻塞);

3. 业主 B 开车离开停车场,系统回收车位(线程调用 release()),许可证数量加 1(从 19 变 20),此时入口排队的业主 C 会被“通知”(线程唤醒),获取车位进入停车场;

4. 早高峰期间,无论多少车来,同时停在停车场的车始终不超过 20 辆,避免了拥堵——这就是 Semaphore 限流的核心价值。

这个案例还能延伸到 Semaphore 的“超时获取”和“尝试获取”特性:

• 超时获取:业主 D 到入口后,不想一直排队,设置“最多等 10 分钟”(调用 acquire(10, TimeUnit.MINUTES)),若 10 分钟内有车位就进入,否则直接开车去附近的公共停车场;

• 尝试获取:业主 E 到入口后,先“问一下有没有车位”(调用 tryAcquire()),若有就进,没有就走,不等待。

核心方法解析:从“申请”到“释放”的全流程

Semaphore 的核心方法围绕“许可证的获取、释放、查询”设计,每个方法都对应实际场景中的需求:

• acquire():线程申请 1 个许可证,若有可用许可证,直接获取(许可证数量减 1);若没有,线程会阻塞,直到有许可证可用或线程被中断(中断会抛出 InterruptedException)。比如小区业主进入停车场时“申请车位”,没车位就排队。

• acquire(int permits):线程一次性申请多个许可证(比如某公司的班车需要占用 2 个相邻车位,就申请 2 个许可证),若可用许可证数量 >= 申请数量,直接获取(许可证数量减申请数);否则阻塞。

• tryAcquire():尝试申请 1 个许可证,若获取成功返回 true,失败返回 false,不会阻塞线程。比如业主“问一下有没有车位”,有就进,没有就走,不排队。

• tryAcquire(long timeout, TimeUnit unit):在指定时间内尝试申请 1 个许可证,若超时前获取成功返回 true,超时后仍未获取返回 false,不会一直阻塞。比如业主“最多等 10 分钟”,超时就走。

• tryAcquire(int permits, long timeout, TimeUnit unit):在指定时间内尝试申请多个许可证,逻辑与单个许可证的超时获取一致。

• release():线程释放 1 个许可证,许可证数量加 1,同时唤醒一个阻塞等待的线程。比如业主离开停车场,释放车位,让排队的业主进入。

• release(int permits):线程一次性释放多个许可证,比如班车离开,释放 2 个车位,许可证数量加 2。

• availablePermits():查询当前可用的许可证数量,比如物业查询“还有多少个空车位”,方便告知业主。

• getQueueLength():查询当前正在等待许可证的线程数量,比如物业查询“入口有多少辆车在排队”,判断是否需要引导到其他停车场。

• hasQueuedThreads():判断是否有线程正在等待许可证,比如物业判断“入口是否有排队车辆”,有就安排人员引导。

实际开发中的典型应用场景

Semaphore 的核心价值是“限流”,凡是需要“控制并发访问数量”的场景,都可以用它,常见场景包括:

场景一:数据库连接池的并发控制

某系统使用数据库连接池,连接池最大容量为 10(即同时只能有 10 个线程使用数据库连接)。如果有 50 个线程同时需要操作数据库,直接访问会导致“连接耗尽”错误,此时用 Semaphore 控制:

• 初始化 Semaphore,许可证数量设为 10(与连接池容量一致);

• 每个线程需要操作数据库时,先调用 acquire() 获取许可证,再从连接池获取连接;

• 线程操作完数据库后,释放连接到连接池,再调用 release() 释放许可证;

• 这样同时操作数据库的线程始终不超过 10 个,避免连接耗尽。

场景二:接口限流(防止高并发压垮服务)

某电商平台的“商品详情页”接口,服务器最多能承受 100 个并发请求(超过会导致响应变慢或崩溃)。在促销活动期间,可能有上千个请求同时到来,此时用 Semaphore 限流:

• 初始化 Semaphore,许可证数量设为 100;

• 每个请求到达接口时,先调用 tryAcquire() 尝试获取许可证:

◦ 获取成功:处理请求,返回商品详情;

◦ 获取失败:返回“当前访问人数过多,请稍后再试”,避免请求压垮服务器;

• 请求处理完成后,调用 release() 释放许可证,供后续请求使用。

场景三:批量任务的并发控制

某系统需要处理 1000 个文件的解析任务,每个解析任务需要占用一定的内存和 CPU 资源。如果同时启动 1000 个线程解析,会导致内存溢出,此时用 Semaphore 控制并发数:

• 初始化 Semaphore,许可证数量设为 20(根据服务器性能确定,比如同时允许 20 个线程解析);

• 每个文件解析线程启动时,先调用 acquire() 获取许可证,再开始解析;

• 解析完成后,调用 release() 释放许可证,唤醒下一个等待的解析线程;

• 这样同时解析的线程始终为 20 个,平衡了处理效率和服务器资源占用。

场景四:共享资源的并发访问控制

某系统中有一个“打印服务”,同一时间只能有 3 个线程使用打印机(避免打印任务排队混乱)。此时用 Semaphore 控制:

• 初始化 Semaphore,许可证数量设为 3;

• 线程需要打印时,获取许可证,使用打印机;

• 打印完成后,释放许可证,让其他线程使用;

• 确保同时只有 3 个打印任务在执行,避免打印机过载。

五、请解释 Exchanger(交换者)的核心作用?结合生活案例说明其工作原理与核心方法?使用时需要注意哪些问题?

Exchanger 是 Java 并发包中专门用于“线程间数据交换”的工具类,其核心作用是让两个线程在预先约定的“同步点”交换彼此的数据——当第一个线程到达同步点后,会阻塞等待第二个线程;当第二个线程也到达同步点后,两个线程会将各自携带的数据传递给对方,同时唤醒并继续执行后续操作。简单来说,它就像“两个人在约定地点交换物品”——甲带 A 物品,乙带 B 物品,甲先到就等乙,乙到后两人交换物品,然后各自离开。

核心工作原理:“等待-交换-唤醒”的双向数据传递

Exchanger 的工作逻辑围绕“两个线程的同步与数据交换”展开,核心流程可概括为:

1. 两个线程(线程 A、线程 B)预先约定一个“交换点”(即调用 Exchanger 的 exchange() 方法的位置);

2. 线程 A 先到达交换点,调用 exchange(dataA) 方法,将自己要交换的数据 dataA 传入,此时线程 A 会阻塞,等待线程 B 到达;

3. 线程 B 后到达交换点,调用 exchange(dataB) 方法,将自己要交换的数据 dataB 传入;

4. Exchanger 接收到两个线程的交换数据后,会将 dataB 传递给线程 A,将 dataA 传递给线程 B;

5. 数据交换完成后,线程 A 和线程 B 同时被唤醒,各自拿着交换后的数据继续执行后续操作。
整个过程中,Exchanger 确保了“数据交换的双向性”和“线程同步”——只有两个线程都到达交换点,才会交换数据,避免了“一个线程传完数据,另一个线程没接收到”的问题。

生活案例:超市“供应商送货与库管员入库”的单据交换

某超市每天早上 9 点,供应商的送货员(线程 A)会带着“送货单”和“货物”到超市仓库,库管员(线程 B)会带着“入库单”在仓库等待,两者需要交换单据才能完成入库流程,这个过程完美契合 Exchanger 的工作原理:

1. 送货员(线程 A)的任务:8:50 到达仓库(交换点),携带“送货单”(数据 A,包含送货的商品名称、数量、规格),等待库管员;

2. 库管员(线程 B)的任务:9:00 到达仓库(交换点),携带“入库单”(数据 B,包含超市预入库的商品信息);

3. 送货员先到,调用“等待交换”(对应 exchange(送货单)),阻塞等待库管员;

4. 库管员到达后,调用“交换单据”(对应 exchange(入库单));

5. 两者交换单据:送货员拿到库管员的“入库单”,确认入库信息与送货信息一致;库管员拿到送货员的“送货单”,核对货物数量是否与送货单一致;

6. 单据交换完成,送货员开始卸货物,库管员开始登记入库,两个“线程”继续执行后续操作。

这个案例还能延伸到“超时交换”的场景:如果库管员 9:20 还没到,送货员不能一直等(避免耽误其他超市的送货),此时送货员可以设置“最多等 30 分钟”(对应 exchange(送货单, 30, TimeUnit.MINUTES)),若 30 分钟内库管员没到,就会触发超时,送货员联系超市负责人确认情况,而不是一直阻塞。

核心方法解析:聚焦“数据交换”与“同步等待”

Exchanger 的核心方法较少,主要围绕“数据交换”和“超时控制”设计,每个方法都直接服务于“双向数据传递”的需求:

• exchange(V x):这是 Exchanger 最核心的方法,线程调用该方法时,传入自己要交换的数据 x,然后阻塞等待另一个线程到达交换点;当另一个线程也调用该方法后,两个线程交换数据,当前线程会收到对方传入的数据,并唤醒继续执行。比如送货员调用 exchange(送货单),等待库管员交换入库单。

• exchange(V x, long timeout, TimeUnit unit):带超时时间的交换方法,线程传入交换数据 x 后,会阻塞等待指定时长;若在超时前另一个线程到达,正常交换数据并返回对方数据;若超时后另一个线程仍未到达,会抛出 InterruptedException,当前线程唤醒并退出,避免一直阻塞。比如送货员设置“等 30 分钟”,超时后联系负责人。

• 注:Exchanger 的泛型 V 表示交换数据的类型,两个线程交换的数据类型必须一致(比如都是 String 类型的单据,或都是自定义的“凭证类”对象),否则会抛出 ClassCastException。

使用 Exchanger 时需要注意的问题

Exchanger 虽能实现线程间的数据交换,但在使用时需注意以下几点,避免出现死锁、数据异常或性能问题:

1. 仅支持“两个线程”交换数据,不支持多线程

Exchanger 的设计初衷是“双向交换”,只能用于两个线程之间的数据传递,若有三个或更多线程调用 exchange() 方法,会出现“随机两两交换”的情况,无法实现预期的多线程数据交换。
比如:有三个送货员(线程 A、B、C)都到仓库等待交换单据,库管员只有一个(线程 D),此时 Exchanger 会随机让其中一个送货员(比如 A)与 D 交换,B 和 C 会一直阻塞,导致流程异常。
解决方案:若需多线程交换数据,需创建多个 Exchanger 实例,确保每次交换只有两个线程参与(比如两个送货员对应两个库管员,每个组合用一个 Exchanger)。

2. 避免“线程永久阻塞”,建议使用带超时的交换方法

若一个线程到达交换点后,另一个线程因异常(如崩溃、中断)未到达,且当前线程使用的是无超时的 exchange(V x) 方法,会导致该线程永久阻塞,浪费系统资源。
比如:送货员到达仓库后,库管员在路上发生交通事故无法到场,送货员用无超时的 exchange() 会一直等下去,无法处理其他任务。
解决方案:除非明确知道“另一个线程一定会到达”,否则优先使用 exchange(V x, long timeout, TimeUnit unit) 方法,设置合理的超时时间(如 30 分钟、1 小时),超时后通过 catch 异常做后续处理(如联系负责人、重试交换)。

3. 交换数据的类型必须一致,避免类型转换异常

Exchanger 要求两个线程交换的数据类型必须相同(泛型 V 一致),若一个线程传入 String 类型,另一个线程传入 Integer 类型,交换时会抛出 ClassCastException,导致程序崩溃。
比如:送货员传入 String 类型的“送货单”,库管员传入 Integer 类型的“入库单编号”,交换时会报错。
解决方案:在创建 Exchanger 实例时,明确指定泛型类型(如 Exchanger<Bill>,其中 Bill 是自定义的单据类),确保两个线程传入的数据都是该类型的实例。

4. 交换的数据需考虑线程安全,避免数据被篡改

若交换的数据是“可变对象”(如自定义的 Bill 类,有 set 方法),在交换过程中或交换后,若有其他线程修改该对象的属性,会导致数据不一致。
比如:送货员传入的 Bill 对象,在等待交换时,被其他线程修改了“送货数量”,库管员拿到交换后的 Bill 时,数量已不是原始值,导致入库核对错误。
解决方案:要么使用“不可变对象”作为交换数据(如 String、Integer,或自定义的不可变类,没有 set 方法),要么在交换前后对数据加锁,确保数据不被篡改。

5. 避免在高并发场景下过度使用 Exchanger

Exchanger 的交换过程需要线程阻塞等待,若在高并发场景下(如每秒有上千对线程需要交换数据)大量使用,会导致线程阻塞频繁,增加 CPU 开销,影响系统性能。
比如:电商平台的“订单确认”和“支付结果”线程,每秒有 1000 对线程需要交换数据,大量线程阻塞等待会导致系统响应变慢。
解决方案:高并发场景下,可考虑用“队列”(如 ArrayBlockingQueue)替代 Exchanger,让一个线程将数据放入队列,另一个线程从队列取数据,减少线程阻塞;若必须用 Exchanger,需合理控制并发数,避免线程过多。

六、在实际开发中,如何根据业务需求选择 CountDownLatch、CyclicBarrier、Semaphore 和 Exchanger 这四种并发工具类?请结合具体业务场景分析?

CountDownLatch、CyclicBarrier、Semaphore 和 Exchanger 虽同属 Java 并发工具类,但各自的核心能力和适用场景差异显著,实际开发中选择的关键是“匹配业务需求的核心痛点”——是需要“等待任务完成”“线程同步循环”“控制并发数量”还是“线程数据交换”。在选择前,可先拆解业务需求,找到最关键的“同步/控制痛点”,再匹配工具类的核心能力:比如面对“等待多个任务完成后执行后续操作”的痛点,核心需求是“一次性任务同步,计数器归 0 触发”,对应的工具类是 CountDownLatch;面对“周期性线程同步,重复等待集合”的痛点,核心需求是“可循环的线程同步,屏障打开触发”,对应 CyclicBarrier;面对“控制共享资源的并发访问数量”的痛点,核心需求是“限流,避免资源过度竞争”,对应 Semaphore;面对“两个线程需要双向交换数据”的痛点,核心需求是“数据同步传递,双向交换”,对应 Exchanger。结合具体业务场景的分析,能更清晰地掌握选择逻辑。

场景一:电商平台“大促活动(如双 11)的系统预热与上线”

业务背景:

双 11 大促前,电商平台需要完成“系统预热”和“活动上线”两个阶段:

1. 预热阶段(大促前 1 小时):需要完成“商品库存加载、优惠券配置、用户购物车同步、活动规则缓存”4 个任务,所有任务完成后,才能进入“倒计时等待上线”状态;

2. 上线阶段(大促开始时):需要“商品详情页、下单接口、支付接口、物流接口”4 个服务的线程同时启动,避免部分服务先启动导致用户看到异常页面;

3. 大促期间:下单接口的数据库连接池最大容量为 50,需要控制同时访问数据库的线程数,避免连接耗尽;

4. 订单处理:“订单生成线程”需要将订单 ID 传递给“支付监听线程”,“支付监听线程”需要将支付状态传递给“订单生成线程”,两者需交换数据确认订单状态。

工具类选择与应用:

1. 预热阶段:用 CountDownLatch 等待所有预热任务完成

◦ 痛点:预热阶段的 4 个任务必须全部完成,才能进入倒计时,且任务只需执行一次(双 11 预热只做一次);

◦ 方案:创建 CountDownLatch 实例,计数器设为 4(对应 4 个预热任务);

◦ 库存加载线程完成后调用 countDown(),计数器减 1;

◦ 优惠券配置、购物车同步、规则缓存线程依次完成并 countDown();

◦ 主线程(预热控制线程)调用 await(),等待计数器归 0 后,输出“预热完成,进入倒计时”;

◦ 为什么不选 CyclicBarrier:预热任务只需执行一次,无需循环,CountDownLatch 一次性使用更高效。

2. 上线阶段:用 CyclicBarrier 让所有服务线程同步启动

◦ 痛点:4 个服务的线程必须同时启动,避免“商品详情页能访问,下单接口还没开”的情况,且若大促期间服务重启,需要重新同步启动;

◦ 方案:创建 CyclicBarrier 实例,参与线程数设为 4(对应 4 个服务线程),构造方法传入 Runnable(启动前检查服务状态);

◦ 商品详情页线程、下单接口线程、支付接口线程、物流接口线程到达“上线点”后,调用 await() 等待;

◦ 最后一个线程到达后,先执行 Runnable(检查服务状态),然后所有线程同时启动服务;

◦ 若服务重启,调用 reset() 重置 CyclicBarrier,重复同步启动流程;

◦ 为什么不选 CountDownLatch:服务可能需要重启,需循环同步,CountDownLatch 不可重置,无法满足需求。

3. 大促期间:用 Semaphore 控制数据库连接的并发访问

◦ 痛点:下单接口的数据库连接池最大容量为 50,若 100 个线程同时访问,会导致连接耗尽,报错“无法获取数据库连接”;

◦ 方案:创建 Semaphore 实例,许可证数量设为 50(与连接池容量一致);

◦ 每个下单线程需要访问数据库时,先调用 acquire() 获取许可证;

◦ 访问完成后,释放数据库连接,再调用 release() 释放许可证;

◦ 若许可证已用完,线程阻塞等待,直到有其他线程释放许可证;

◦ 为什么不选其他工具类:其他工具类无“限流”能力,无法控制并发访问数量。

4. 订单处理:用 Exchanger 实现订单与支付线程的数据交换

◦ 痛点:订单生成线程需要知道支付状态(是否支付成功),支付监听线程需要知道订单 ID(对应哪个订单的支付),两者需双向传递数据;

◦ 方案:创建 Exchanger 实例,泛型设为 OrderData(自定义类,包含订单 ID 和支付状态);

◦ 订单生成线程创建 OrderData(订单 ID, 未支付),调用 exchange() 等待;

◦ 支付监听线程创建 OrderData(null, 支付成功),调用 exchange();

◦ 交换后,订单生成线程拿到“支付成功”状态,更新订单;支付监听线程拿到“订单 ID”,关联支付记录;

◦ 为什么不选其他工具类:其他工具类只能单向传递数据或同步线程,无法实现双向数据交换。

场景二:物流系统“每日包裹分拣与配送”流程

业务背景:

某物流系统每天需要完成“包裹分拣”和“区域配送”两个环节,流程如下:

1. 分拣环节(每天凌晨 2 点):需要 6 个分拣员(线程)同时开始分拣包裹,避免部分分拣员先开始导致包裹混乱,且每天都要重复这个流程;

2. 分拣完成后:需要等待所有分拣员将包裹运到“配送区”(6 个任务),才能开始分配配送员;

3. 配送环节:每个配送点有 8 辆配送车(共享资源),需要控制同时出发的配送车数量,避免配送点拥堵;

4. 签收确认:配送员(线程)需要将“签收单”传递给仓库管理员(线程),仓库管理员需要将“包裹出库单”传递给配送员,两者需交换单据确认配送完成。

工具类选择与应用:

1. 分拣环节:用 CyclicBarrier 让分拣员同步开始分拣

◦ 痛点:每天凌晨 2 点需要 6 个分拣员同时开始,且每天都要重复,需循环同步;

◦ 方案:创建 CyclicBarrier 实例,参与线程数设为 6,构造方法传入 Runnable(分拣前确认包裹类型);

◦ 每个分拣员到达分拣区后调用 await(),最后一个分拣员到达后,执行 Runnable 并唤醒所有分拣员,同时开始分拣;

◦ 第二天凌晨,调用 reset() 重置屏障,重复同步流程;

◦ 为什么不选 CountDownLatch:需要每天重复同步,CountDownLatch 不可循环,无法满足。

2. 分拣完成后:用 CountDownLatch 等待所有包裹运到配送区

◦ 痛点:6 个分拣员需将各自分拣的包裹运到配送区(6 个任务),全部完成后才能分配配送员,且该任务每天只需执行一次;

◦ 方案:创建 CountDownLatch 实例,计数器设为 6;

◦ 每个分拣员将包裹运到配送区后,调用 countDown();

◦ 配送分配线程调用 await(),等待计数器归 0 后,开始分配配送员;

◦ 为什么不选 CyclicBarrier:任务只需执行一次,且分拣员运完包裹后即可离开,无需等待其他分拣员,CyclicBarrier 要求线程互相等待,不符合需求。

3. 配送环节:用 Semaphore 控制配送车的并发出发数量

◦ 痛点:配送点只有 8 辆配送车,若 15 个配送员同时要用车,会导致部分人无法取车,配送点混乱;

◦ 方案:创建 Semaphore 实例,许可证数量设为 8;

◦ 配送员需要用车时,调用 acquire() 获取许可证(取车);

◦ 配送完成后,还车并调用 release() 释放许可证;

◦ 若许可证已用完,配送员等待,直到有配送车归还;

◦ 为什么不选其他工具类:其他工具类无法控制“共享资源(配送车)的并发访问数量”。

4. 签收确认:用 Exchanger 实现配送员与仓库管理员的单据交换

◦ 痛点:配送员需要将“客户签收单”给仓库管理员(确认包裹已签收),仓库管理员需要将“包裹出库单”给配送员(确认包裹属于该配送员);

◦ 方案:创建 Exchanger 实例,泛型设为 Document(自定义单据类);

◦ 配送员携带 Document(签收单, null) 调用 exchange() 等待;

◦ 仓库管理员携带 Document(null, 出库单) 调用 exchange();

◦ 交换后,配送员拿到出库单确认包裹,仓库管理员拿到签收单更新库存;

◦ 为什么不选其他工具类:其他工具类无法实现“双向单据交换”,只能单向传递或同步线程。

场景三:教育平台“在线考试系统”的考试流程

业务背景:

某在线教育平台的期末考试流程如下:

1. 考试开始前(考前 10 分钟):需要完成“考生身份验证、试卷加载、答题页面初始化、计时系统启动”4 个任务,所有任务完成后,才能让考生进入“等待考试开始”状态;

2. 考试开始时:需要所有考生的“答题线程”同时启动,避免部分考生先开始答题,部分考生还在加载页面;

3. 考试过程中:系统的“试卷提交接口”最多支持 30 个并发请求,需控制同时提交的考生数量,避免接口崩溃;

4. 成绩核对:“阅卷线程”需要将“考生得分”传递给“成绩录入线程”,“成绩录入线程”需要将“考生信息”传递给“阅卷线程”,两者需交换数据确认成绩归属。

工具类选择与应用:

1. 考前准备:用 CountDownLatch 等待所有准备任务完成

◦ 痛点:4 个考前任务必须全部完成,才能让考生等待考试开始,且任务只需执行一次(每场考试一次);

◦ 方案:创建 CountDownLatch 实例,计数器设为 4;

◦ 身份验证、试卷加载、页面初始化、计时启动线程依次完成并 countDown();

◦ 考生控制线程调用 await(),计数器归 0 后,提示“等待考试开始”;

◦ 为什么不选 CyclicBarrier:任务只需一次,无需循环,CountDownLatch 更高效。

2. 考试开始:用 CyclicBarrier 让所有考生的答题线程同步启动

◦ 痛点:所有考生的答题线程必须同时启动,避免不公平,且若考试系统重启,需重新同步;

◦ 方案:创建 CyclicBarrier 实例,参与线程数设为考生人数(如 100),构造方法传入 Runnable(启动前提醒“考试开始”);

◦ 每个考生的答题线程加载完成后调用 await();

◦ 最后一个考生加载完成后,执行 Runnable 并唤醒所有线程,同时开始答题;

◦ 若系统重启,调用 reset() 重置屏障,重新同步;

◦ 为什么不选 CountDownLatch:需要确保所有线程(考生)同时启动,CountDownLatch 无法让线程互相等待,只能等待任务完成。

3. 试卷提交:用 Semaphore 控制提交接口的并发请求数

◦ 痛点:提交接口最多支持 30 个并发请求,若 100 个考生同时提交,会导致接口超时;

◦ 方案:创建 Semaphore 实例,许可证数量设为 30;

◦ 考生提交试卷时,调用 tryAcquire() 尝试获取许可证:

◦ 获取成功:提交试卷;

◦ 获取失败:提示“当前提交人数过多,请稍后再试”;

◦ 提交完成后,调用 release() 释放许可证;

◦ 为什么不选其他工具类:其他工具类无“并发请求限流”能力,无法保护接口。

4. 成绩核对:用 Exchanger 实现阅卷与成绩录入线程的数据交换

◦ 痛点:阅卷线程需要考生信息(匹配得分),成绩录入线程需要考生得分(录入系统),两者需双向传递数据;

◦ 方案:创建 Exchanger 实例,泛型设为 ScoreData(包含考生 ID 和得分);

◦ 阅卷线程携带 ScoreData(考生 ID, 90 分) 调用 exchange();

◦ 成绩录入线程携带 ScoreData(考生 ID, null) 调用 exchange();

◦ 交换后,阅卷线程确认考生 ID 正确,成绩录入线程拿到 90 分录入系统;

◦ 为什么不选其他工具类:其他工具类无法实现“考生 ID 与得分的双向匹配”,只能单向传递数据。

总结:工具类选择的核心逻辑

1. 看“是否需要循环”:需要重复同步(如每天、每周的任务)→ 选 CyclicBarrier;只需一次同步(如一次性任务)→ 选 CountDownLatch;

2. 看“核心需求是同步还是限流”:需要控制并发访问数量(如资源、接口、线程数)→ 选 Semaphore;需要线程同步或任务同步→ 选 CountDownLatch/CyclicBarrier;

3. 看“是否需要数据交换”:两个线程需双向传递数据→ 选 Exchanger;无需数据交换,只需同步→ 选其他三类;

4. 看“线程协作方式”:线程无需互相等待,只需完成任务→ 选 CountDownLatch;线程需互相等待到齐→ 选 CyclicBarrier。

通过以上逻辑,可根据具体业务场景的痛点,快速匹配到最合适的并发工具类,避免因工具类选择不当导致的性能问题或业务异常。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值