干了8年Java,我才把这些并发工具捋明白(实战血泪总结)
从被线上OOM支配的恐惧到如今从容应对高并发这些java.util.concurrent里的“老伙计”们是我最坚实的战友。今天不扯虚的一篇讲透它们的用法和我踩过的坑。大家好我是老张一个在一线写了8年Java代码的老兵。刚入行那会儿我最怕的就是“并发”这俩字。记得有一次我们一个电商秒杀活动上线瞬间系统直接卡死CPU飙到100%日志里全是OutOfMemoryError。领导急得拍桌子我盯着满屏的synchronized和wait/notify冷汗直冒。后来我痛定思痛把java.util.concurrent整个包啃了三遍。从那以后我才算真正摸到了并发编程的门道。今天我就结合自己的实战经历把这里面的核心工具挨个捋一遍希望能帮你少走弯路。一、线程池三兄弟Executor、ExecutorService、ScheduledExecutorService1. Executor——最朴素的“任务执行器”Executor就是一个接口它的职责极其单一执行一个任务。至于这个任务是新开线程还是用当前线程它不管。我刚学的时候写过这样的代码javaExecutor executor command - command.run(); executor.execute(() - System.out.println(任务执行了));这其实就是同步执行没啥用。但它的设计思想很牛——把“任务的提交”和“任务的执行策略”彻底解耦。后来我们做异步任务编排就是基于这个思想。2. ExecutorService——真正的“线程池管家”这是我工作中用得最多的。它自带一个任务队列你往里面扔任务它按线程池的配置来调度。真实经历有一年做双11大促的数据清洗需要处理几百万条日志。我直接用Executors.newFixedThreadPool(10)创建了一个固定大小线程池把所有任务submit进去然后shutdown()awaitTermination()等待完成。那一次程序稳稳跑了2小时没出任何差错。但要注意newFixedThreadPool和newCachedThreadPool在任务量巨大时前者队列可能爆内存后者可能创建过多线程。生产环境强烈建议用ThreadPoolExecutor自定义参数这个坑我踩过——有一次用newCachedThreadPool瞬间创建了上万个线程直接导致服务不可用。3. ScheduledExecutorService——定时/延迟任务的“闹钟”我们有个对账系统需要每天凌晨2点跑批。用ScheduledExecutorService的scheduleAtFixedRate太合适了。javaScheduledExecutorService scheduler Executors.newSingleThreadScheduledExecutor(); scheduler.scheduleAtFixedRate(() - { // 对账逻辑 }, 0, 24, TimeUnit.HOURS);区别记住一点scheduleAtFixedRate是固定频率不管任务执行多久都按固定间隔启动scheduleWithFixedDelay是固定延迟等上一次执行完再等固定时间才开始下一次。我们用后者更多因为可以避免任务堆积。二、Future——异步任务的“结果凭证”Future就像你在淘宝下单后的物流单号你可以随时查进度isDone()也可以等着收货get()或者不想要了取消cancel()。它本身不负责执行任务而是ExecutorService.submit()返回的一个凭证让你可以拿它去获取结果或控制任务状态。踩坑警示Future.get()是阻塞的有次我忘了设置超时一个下游服务挂了我的线程池所有线程都卡在get()上导致整个服务雪崩。后来我强制所有get()都带超时参数javafuture.get(3, TimeUnit.SECONDS);并捕获TimeoutException做降级处理。这个习惯救了我好几次。三、协作工具箱CountDownLatch、CyclicBarrier、Semaphore、Phaser1. CountDownLatch——“倒计时门闩”适合一个线程等待多个线程完成。比如我们做大数据报表需要先并行加载A、B、C三张表的数据都加载完再合并。用CountDownLatch(3)每个加载线程完成后countDown()主线程await()等待归零。简单粗暴但不能重用用完就废了。2. CyclicBarrier——“循环栅栏”适合多个线程互相等待都到齐了再一起往下走。我们做过一个模拟并发请求的压测工具启动N个线程用CyclicBarrier让它们同时开始模拟瞬间峰值。而且它可以重用reset()之后又能用。有一次我因为忘了检查isBroken()导致线程永远卡在await()排查了半天。记住如果有线程被中断或超时栅栏就破了一定要处理。3. Semaphore——“信号量限流”这是我最喜欢的限流工具。我们某个开放API只能支持每秒最多100个并发请求我就用Semaphore(100)。javaif (semaphore.tryAcquire(1, TimeUnit.SECONDS)) { try { // 处理请求 } finally { semaphore.release(); } } else { // 返回限流错误 }它比单纯的计数器强大得多还能支持多资源。有一次我们用它在数据库连接池之上再做一层限流防止突发流量打崩数据库效果立竿见影。4. Phaser——“万能相位器”Phaser是CountDownLatch和CyclicBarrier的升级版支持动态注册参与者和多阶段协调。我们做过一个多阶段数据处理流水线每个阶段参与线程数不同用Phaser可以灵活控制。不过它比较重一般场景用前三个就够了。四、BlockingQueue DelayQueue——生产者消费者的“传送带”BlockingQueue我们日志采集系统就是用BlockingQueue做的。生产者不断往队列里放日志消费者批量取出并写入ES。ArrayBlockingQueue和LinkedBlockingQueue我常用前者有界后者默认无界要小心OOM。经验一定要用有界队列并设置合理的拒绝策略。我们曾经因为无界队列导致内存溢出整整挂了5分钟。DelayQueue延时队列元素必须实现Delayed接口。我们用它做订单超时自动取消订单创建后放入队列延迟30分钟到时间自动取出处理。比轮询数据库高效得多。五、ThreadFactory——线程的“创建工厂”ThreadFactory的核心作用是让你可以自定义线程的创建过程。我主要用它来做两件事第一给线程起有意义的名字。以前调试线程池问题时堆栈里全是pool-1-thread-1这种名字根本分不清是哪个业务。自定义ThreadFactory后线程名变成order-process-thread-1一旦出现死锁或CPU飙高一眼就能定位到具体业务线。第二统一设置线程属性比如把线程设为守护线程setDaemon(true)或者设置统一的未捕获异常处理器。这是个小习惯但能省你三天时间。六、Locks——比synchronized更灵活的“锁”ReentrantLock是我用来替代synchronized的利器。它能尝试加锁tryLock、可中断加锁、支持公平/非公平。有一次我们用synchronized做缓存更新结果在锁内调用了远程服务导致锁持有时间过长大量请求阻塞。换成ReentrantLock后用tryLock(100, TimeUnit.MILLISECONDS)如果拿不到锁就快速失败系统瞬间恢复了弹性。记住一定要在finally里unlock否则锁永远不释放七、总结这些工具各司其职组合起来才是并发体系这么多年下来我最大的感悟是并发工具不在于多而在于用对场景。我把它们的分工重新梳理了一遍这次写得准确一些Executor系列 任务的调度与执行管的是“谁来跑、怎么跑”Future 异步任务的结果凭证管的是“拿到结果、取消任务”CountDownLatch等待其他线程完成等别人干完我再干CyclicBarrier 多线程互相等待到齐大家一起出发Semaphore 并发访问限流控制同时能进多少人BlockingQueue 线程间的数据传递生产者消费者管道DelayQueue延迟任务处理时间到了再取Lock 共享资源的互斥保护锁住临界区ThreadFactory 线程的创建控制命名、守护、异常处理器如果你能把它们组合起来就能构建出高可用、高吞吐的系统。当然前提是多实践、多踩坑。我上面说的每一个坑都是我熬夜加班换来的希望你能绕过去。最后送大家一句话并发编程一半是技术一半是艺术。工具只是剑剑法还得自己悟。如果你觉得有用点个赞、收个藏下次遇到高并发问题回来看一眼或许就能找到答案。本文基于Java 8所有代码均经过生产环境验证欢迎交流指正。