面试必备之并发工具包

72 阅读4分钟

《Java并发工具包:从面试被虐到吊打面试官的奇幻之旅》

面试官(推了推眼镜,镜片闪过一道寒光):"听说你熟悉Java并发?那你知道CountDownLatch和CyclicBarrier的区别吗?"

(额头冒汗,大脑飞速运转):"呃...一个用一次,一个能循环用?"

面试官(冷笑):"就这?"

别慌!今天我们就用一场"面试情景剧",带你彻底搞懂Java并发工具包(JUC),让你下次面试时反向让面试官怀疑人生!


第一幕:CountDownLatch —— 五黑等队友的绝望

面试官:"假设你正在开发一个王者荣耀组队系统,5人组队成功后才能开始游戏,你怎么实现?"

(灵光一闪):"用CountDownLatch!就像等队友点'准备'一样!"

public class WangZheTeam {
    public static void main(String[] args) throws InterruptedException {
        int playerCount = 5;
        CountDownLatch latch = new CountDownLatch(playerCount);
        
        for (int i = 1; i <= playerCount; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "已准备");
                latch.countDown(); // 队友点准备了!
            }, "玩家" + i).start();
        }
        
        latch.await(); // 主线程阻塞等待
        System.out.println("全军出击!");
    }
}

设计思路

  • 就像游戏里的"准备确认"弹窗,每个玩家点准备就countDown()
  • 当计数器归零(所有人都准备),await()解除阻塞

灵魂拷问:如果有个队友永远不点准备怎么办?(答案:用await(long timeout, TimeUnit unit)设置超时!)


第二幕:CyclicBarrier —— 旅游团的"集合点"模式

面试官(突然变脸):"那如果导游要等所有游客在景点A集合后,再去景点B呢?"

(邪魅一笑):"这次用CyclicBarrier,还能重复使用!"

public class TouristGuide {
    public static void main(String[] args) {
        int touristNum = 3;
        CyclicBarrier barrier = new CyclicBarrier(touristNum, 
            () -> System.out.println("所有人到齐,出发下一个景点!"));
        
        for (int i = 1; i <= touristNum; i++) {
            new Thread(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + "到达集合点");
                    barrier.await(); // 等待其他游客
                    System.out.println(Thread.currentThread().getName() + "继续游览");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, "游客" + i).start();
        }
    }
}

爆笑时刻
当最后一个游客调用await()时,所有线程同时解除阻塞,就像导游大喊:"3号家庭终于上完厕所了,我们走!"

进阶技巧

  • 可以重置reset()(但会引发BrokenBarrierException)
  • 比较适合MapReduce分阶段处理的场景

第三幕:Semaphore —— KTV的残酷现实

面试官(突然唱起来):"爱情不是你想买~ 想买就能买~ 现在KTV只有2个麦克风,10个人要唱歌怎么办?"

(拍桌而起):"用Semaphore!比抢麦打架文明多了!"

public class KTV {
    public static void main(String[] args) {
        int micNum = 2;
        int peopleNum = 10;
        Semaphore semaphore = new Semaphore(micNum);
        
        for (int i = 1; i <= peopleNum; i++) {
            new Thread(() -> {
                try {
                    semaphore.acquire(); // 抢到麦克风!
                    System.out.println(Thread.currentThread().getName() + ":在唱《青藏高原》");
                    Thread.sleep(2000); // 唱high了不肯放手
                    System.out.println(Thread.currentThread().getName() + ":唱完了(不情愿)");
                    semaphore.release(); // 终于释放麦克风
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, "麦霸" + i).start();
        }
    }
}

血泪教训

  • 这就是为什么KTV要按小时收费——tryAcquire(long timeout, TimeUnit unit)
  • 数据库连接池也是这个原理!(突然升华)

第四幕:Exchanger —— 二手市场的交易艺术

面试官(突然摆地摊):"现在你是闲鱼程序员,如何实现两个人交换物品?"

(掏出键盘):"Exchanger,比线下见面交易安全多了!"

public class XianYu {
    public static void main(String[] args) {
        Exchanger<String> exchanger = new Exchanger<>();
        
        new Thread(() -> {
            try {
                String fromThread2 = exchanger.exchange("PS5");
                System.out.println("线程1用PS5换到了:" + fromThread2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        
        new Thread(() -> {
            try {
                String fromThread1 = exchanger.exchange("Switch");
                System.out.println("线程2用Switch换到了:" + fromThread1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

防坑指南

  • 如果只有一个线程调用exchange(),它会等到天荒地老(就像你挂的二手商品没人问津)
  • 适合两个线程间的数据交换(遗传算法中常用)

终极大招:Phaser —— 多阶段世界杯

面试官(穿上球衣):"现在模拟世界杯:小组赛->淘汰赛->决赛,各阶段要等所有比赛结束!"

(直接跪了):"师傅别念了...我用Phaser还不行吗!"

public class WorldCup {
    public static void main(String[] args) {
        Phaser phaser = new Phaser(4) { // 4支球队
            protected boolean onAdvance(int phase, int parties) {
                System.out.println("第" + (phase + 1) + "阶段结束");
                return phase >= 2 || parties == 0; // 共3个阶段
            }
        };
        
        for (int i = 1; i <= 4; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + ":小组赛拼杀");
                phaser.arriveAndAwaitAdvance(); // 等所有球队完成
                
                if(Thread.currentThread().getName().contains("4")) {
                    System.out.println("球队4被淘汰了");
                    phaser.arriveAndDeregister(); // 注销
                    return;
                }
                
                System.out.println(Thread.currentThread().getName() + ":淘汰赛血战");
                phaser.arriveAndAwaitAdvance();
                System.out.println(Thread.currentThread().getName() + ":决赛!");
            }, "球队" + i).start();
        }
    }
}

设计精髓

  • 动态注册/注销参与者(register()/arriveAndDeregister())
  • 比CyclicBarrier更灵活的分阶段控制

课后思考题(让你装X用)

  1. 为什么CyclicBarrier的构造方法可以传Runnable,而CountDownLatch不行?
    (提示:想想执行时机和线程归属)
  2. 如何用Semaphore实现一个"过独木桥"问题,桥最多承重3人?
    (提示:new Semaphore(3, true)第二个参数是公平模式)
  3. StampedLock的乐观读是什么黑魔法?
    (提示:类似数据库乐观锁,适合读多写少场景)

下次面试时,当面试官问到并发工具,你可以优雅地反问:"您想听哪种场景的实现?我有五种方案可以对比。" 深藏功与名!