尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Java高并发实战:JUC核心工具与线程池调优深度解析

Java高并发实战:JUC核心工具与线程池调优深度解析 1. 从“并发”到“高并发”一线工程师的实战视角“多线程”和“高并发”这两个词在Java工程师的日常里就像空气和水一样常见但真正能把它们玩明白、玩出花来的却不多。很多朋友学了一堆synchronized、volatile背了几道面试题一上生产环境面对真实的流量洪峰系统该崩还是崩。问题出在哪在我看来是缺少一套从“玩具代码”到“工业级应用”的完整知识体系和实战心法。JUCjava.util.concurrent包就是这套心法的核心秘籍它远不止是几个Atomic类或者ConcurrentHashMap那么简单而是一整套应对高并发复杂场景的“工具箱”和“设计模式”。我经历过不少从零到一搭建高并发服务的项目也处理过不少因为并发问题导致的线上事故。今天我们不聊那些枯燥的概念就从一个一线工程师的视角掰开揉碎了讲讲在真实的“大厂”级场景下JUC里的那些工具到底该怎么用背后的“为什么”又是什么。目标很明确让你不仅能应对面试更能写出健壮、高效、易于维护的并发代码。学妹收藏不收藏不重要重要的是这些经验能真正帮你在项目里少踩坑。2. JUC核心工具箱不止于API更是设计思想很多人学JUC喜欢从一个个类开始背方法。这就像学武功只记招式不懂内功心法实战起来必然僵硬。我认为理解JUC首先要建立三层认知同步工具、并发容器和执行框架。这三层环环相扣构成了处理并发问题的完整体系。2.1 同步工具从“锁”到“协作”的进化synchronized和ReentrantLock解决了互斥问题但这只是并发世界最基础的一环。在高并发场景中线程间的“协作”往往比单纯的“互斥”更复杂、也更容易出问题。CountDownLatch多线程任务的“发令枪”想象一个电商系统启动时需要加载缓存、初始化连接池、校验配置文件等多个前置任务。这些任务可以并行执行但必须全部完成后服务才能对外提供。用Thread.join()或者忙等待while循环都太笨重了。// 实战场景服务启动同步 public class ServiceBootstrap { private static final int TASK_COUNT 3; private final CountDownLatch latch new CountDownLatch(TASK_COUNT); public void start() throws InterruptedException { ExecutorService executor Executors.newFixedThreadPool(TASK_COUNT); // 并行执行初始化任务 executor.submit(() - { try { loadCache(); // 模拟耗时操作 } finally { latch.countDown(); } }); executor.submit(() - { try { initConnectionPool(); } finally { latch.countDown(); } }); executor.submit(() - { try { validateConfig(); } finally { latch.countDown(); } }); // 等待所有前置任务完成设置超时避免死等 if (latch.await(30, TimeUnit.SECONDS)) { System.out.println(所有服务初始化完成开始接收外部请求。); } else { System.err.println(服务初始化超时可能存在异常); // 这里应该触发优雅降级或告警 } executor.shutdown(); } }注意countDown()一定要放在finally块中执行确保无论任务成功与否计数器都能递减防止主线程永远等待。超时设置是生产环境的必备项绝不能少。CyclicBarriervsCountDownLatch可重复使用的“集合点”CountDownLatch是一次性的计数器减到零就失效。而CyclicBarrier是可循环使用的它更像一个“集合点”。一个经典的应用场景是数据分片计算将一个大任务拆分成多个子任务并行处理所有子任务都完成一个阶段后再一起进入下一个阶段。// 模拟多阶段数据批处理 public class BatchDataProcessor { private final int workerCount; private final CyclicBarrier barrier; public BatchDataProcessor(int workerCount) { this.workerCount workerCount; // 当所有线程到达屏障后可以选择执行一个回调Runnable用于合并阶段结果 this.barrier new CyclicBarrier(workerCount, () - { System.out.println(所有分片第一阶段处理完成开始汇总...); // 这里可以执行阶段性的数据聚合操作 }); } public void process(ListDataSlice slices) { ExecutorService executor Executors.newFixedThreadPool(workerCount); for (int i 0; i workerCount; i) { final int sliceIndex i; executor.submit(() - { try { // 第一阶段处理 phaseOneProcess(slices.get(sliceIndex)); barrier.await(); // 等待其他线程完成第一阶段 // 第二阶段处理基于第一阶段可能汇总的结果 phaseTwoProcess(slices.get(sliceIndex)); barrier.await(); } catch (Exception e) { Thread.currentThread().interrupt(); } }); } executor.shutdown(); } }实操心得CyclicBarrier的构造器中的Runnable回调是由最后一个到达屏障的线程执行的且在执行期间其他线程仍处于等待状态。这个回调不宜有耗时或阻塞操作否则会影响整体性能。Semaphore控制并发访问的“流量阀”信号量用来控制同时访问特定资源的线程数量。它最典型的应用场景就是资源池管理如数据库连接池和限流。// 实现一个简单的连接池 public class SimpleConnectionPool { private final LinkedListConnection pool new LinkedList(); private final Semaphore useful; public SimpleConnectionPool(int size) { this.useful new Semaphore(size); for (int i 0; i size; i) { pool.addLast(createConnection()); } } public Connection getConnection() throws InterruptedException { useful.acquire(); // 获取一个许可如果没有则阻塞 synchronized (pool) { return pool.removeFirst(); } } public void releaseConnection(Connection conn) { synchronized (pool) { pool.addLast(conn); } useful.release(); // 释放一个许可 } }避坑指南务必保证release()方法一定会被调用通常需要放在finally块中。否则许可无法归还最终会导致所有线程都无法获取资源造成“假死”。在Spring管理的项目中可以利用Around注解的切面来确保资源释放。2.2 并发容器告别手动同步的“性能陷阱”Hashtable和用Collections.synchronizedMap包装的HashMap其同步粒度是整个对象每次只有一个线程能进行操作性能是巨大的瓶颈。JUC提供的并发容器采用了更精妙的并发控制策略。ConcurrentHashMap分段锁与CAS的艺术这是面试高频点也是实战核心。在JDK 1.7及之前它采用分段锁Segment将数据分成一段一段的存储每段配一把锁不同段的操作可以并发。在JDK 1.8之后它做了巨大优化摒弃了分段锁改用Node数组链表/红黑树并发控制则大量使用了synchronized和CASCompare-And-Swap操作。关键方法putVal的并发逻辑当要向一个空桶数组位置插入节点时使用CAS操作避免加锁。只有当发生哈希冲突桶非空时才使用synchronized锁住这个桶的头节点。这种细粒度的锁大大提升了并发度。size()方法的变化1.7版本需要全局加锁或分段统计比较重。1.8版本采用了一个volatile的baseCount变量结合CounterCell数组一种分片计数思想通过累加来获取一个估计值性能极高且是弱一致性的这符合并发场景的常态。重要认知ConcurrentHashMap提供的迭代器是“弱一致性”的它反映的是创建迭代器那一刻或之后某个时刻的映射状态但不会抛出ConcurrentModificationException。这意味着在迭代过程中其他线程的修改可能看到也可能看不到。这在并发环境下是合理的因为强一致性的迭代器需要全局锁代价太高。CopyOnWriteArrayList读多写少场景的“利器”它的原理是“写时复制”。任何修改操作add, set, remove都会底层复制一个新的数组在新数组上操作完成后再将原数组引用指向新数组。这种机制使得读操作完全无需加锁速度极快。// 典型场景监听器列表 public class EventManager { private final CopyOnWriteArrayListEventListener listeners new CopyOnWriteArrayList(); public void addListener(EventListener listener) { listeners.add(listener); // 写操作会复制数组 } public void fireEvent(Event event) { for (EventListener listener : listeners) { // 读操作无锁直接遍历当前数组快照 listener.onEvent(event); } } }使用限制它只适用于读操作远远多于写操作的场景。因为每次写操作都会复制整个底层数组如果数组很大或写操作频繁内存和CPU开销会非常大。同时它提供的迭代器也是基于创建时的数组快照无法感知后续的修改。阻塞队列生产者-消费者模式的“标准实现”BlockingQueue及其实现类ArrayBlockingQueue,LinkedBlockingQueue,PriorityBlockingQueue,SynchronousQueue等是解耦生产者和消费者的最佳实践。它们内部实现了完整的等待/通知机制我们无需再手动wait()和notify()。ArrayBlockingQueuevsLinkedBlockingQueue特性ArrayBlockingQueueLinkedBlockingQueue底层结构定长数组可选容量的链表默认Integer.MAX_VALUE锁分离一把锁生产消费共用两把锁putLock和takeLock适用场景固定大小的有界队列吞吐量预测稳定无界或可有界高并发下吞吐量通常更高SynchronousQueue一个“手递手”的队列。它不存储元素每个插入操作必须等待另一个线程的移除操作反之亦然。它直接传递任务避免了任务在队列中的中转延迟是Executors.newCachedThreadPool默认使用的队列非常适合大量短生命周期的异步任务。2.3 原子类无锁编程的“基石”AtomicInteger、AtomicLong、AtomicReference等原子类是CAS操作的直接体现。它们通过Unsafe类调用CPU底层的原子指令如x86的CMPXCHG实现了非阻塞的线程安全更新。// 一个常见的误区原子类并不保证复合操作的原子性 public class AtomicMisuseExample { private final AtomicInteger count new AtomicInteger(0); // 这个方法不是线程安全的 public void unsafeIncrement() { if (count.get() 10) { // 步骤1检查 count.incrementAndGet(); // 步骤2递增 } // 问题线程A和B可能同时通过步骤1的检查导致最终count超过10。 } // 正确的做法使用CAS循环 public void safeIncrement() { int oldValue; do { oldValue count.get(); if (oldValue 10) { return; // 或抛出异常 } } while (!count.compareAndSet(oldValue, oldValue 1)); // CAS更新 } }核心原理compareAndSetCAS是一个“比较并交换”的原子操作。它的语义是“如果当前值等于期望值oldValue则将其更新为新值否则什么都不做”。上面的循环会不断重试直到成功更新或条件不满足。这就是无锁Lock-Free编程的一种常见模式。LongAdder高并发统计的“性能王者”在超高并发比如统计接口调用次数的场景下所有线程都去竞争更新一个AtomicLong的valueCAS失败重试会非常频繁导致性能下降。LongAdder采用了“分治”思想。它内部维护了一个Cell数组每个Cell是一个AtomicLong和一个base值。当没有竞争时直接CAS更新base。当发生竞争时线程会尝试操作自己哈希到的那个Cell将竞争分散。获取最终结果时将base和所有Cell的值累加。 这样在高并发写场景下LongAdder的吞吐量远高于AtomicLong但缺点是获取当前值的开销稍大且是最终一致性的。它非常适合用于统计、计数的场景而不适合用于需要实时精确值的场景如序列号生成。3.ThreadPoolExecutor你必须亲手“调教”的并发引擎Executors工厂类提供的newFixedThreadPool、newCachedThreadPool等快捷方法在简单 demo 里用用可以但在生产环境直接使用无异于埋雷。它们隐藏了关键的参数配置容易导致OOM内存溢出或资源耗尽。我们必须掌握ThreadPoolExecutor的七大核心参数并理解其工作原理。3.1 七大核心参数深度解析public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueueRunnable workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)corePoolSize核心线程数线程池的“常备军”。即使它们空闲除非设置了allowCoreThreadTimeOut否则不会被回收。这个值应根据任务类型CPU密集型/IO密集型和机器核心数来设定。一个经验公式CPU密集型任务corePoolSize CPU核数 1IO密集型任务corePoolSize CPU核数 * 2。但这只是起点必须通过压测调整。maximumPoolSize最大线程数线程池的“总兵力上限”。当工作队列满了且核心线程都在忙线程池会创建新线程救火队直到达到此上限。keepAliveTimeunit空闲线程存活时间超过核心线程数的那些“救火队员”如果空闲时间超过这个值就会被回收以节省资源。workQueue工作队列任务的“缓冲区”。这是调优的关键也是容易出问题的地方。常见的队列选择策略LinkedBlockingQueue无界队列newFixedThreadPool使用它。任务可以无限堆积直到耗尽内存。最大线程数参数将失效因为队列永远不会满不会触发创建新线程。SynchronousQueue直接传递队列newCachedThreadPool使用它。它没有容量来一个任务如果没有空闲线程就必须创建新线程执行。这会导致线程数暴增可能耗尽资源。ArrayBlockingQueue有界队列这是生产环境更推荐的方式。它结合了核心线程、有界队列和最大线程数形成了稳定的处理模型。threadFactory线程工厂用于创建新线程。强烈建议自定义以便为线程设置有意义的名字如order-process-thread-%d、设置为守护线程、或指定异常处理器。这在排查问题时通过线程名就能快速定位价值巨大。public class NamedThreadFactory implements ThreadFactory { private final AtomicInteger threadNumber new AtomicInteger(1); private final String namePrefix; NamedThreadFactory(String poolName) { namePrefix poolName -thread-; } public Thread newThread(Runnable r) { Thread t new Thread(r, namePrefix threadNumber.getAndIncrement()); t.setDaemon(false); // 通常设置为非守护线程 t.setUncaughtExceptionHandler((thread, throwable) - { // 在这里记录线程池内未捕获的异常非常重要 System.err.println(Uncaught exception in pool thread: thread.getName(), throwable); }); return t; } }handler拒绝策略当线程池已关闭或队列已满且线程数达到最大值时新提交的任务该如何处理。JDK提供了四种内置策略AbortPolicy默认直接抛出RejectedExecutionException。这是最直接的方式让调用者感知到系统已过载。CallerRunsPolicy让提交任务的调用者线程自己来执行这个任务。这提供了一个简单的反馈机制会拖慢调用者从而降低新任务的提交速度是一种平缓的削峰方式。DiscardOldestPolicy丢弃队列里最老的一个任务然后尝试执行当前任务。这可能会丢失重要任务。DiscardPolicy默默丢弃无法处理的任务不抛异常。风险最大。生产环境建议通常使用AbortPolicy并结合业务层的降级、熔断机制。或者自定义拒绝策略比如将拒绝的任务持久化到磁盘、发到死信队列待系统恢复后重试或者至少记录详细的日志和告警。3.2 线程池工作流程与调优实战线程池处理任务遵循一个固定的流程理解这个流程是调优的基础提交一个新任务。如果当前运行的线程数 corePoolSize则立即创建新线程执行该任务即使有空闲核心线程此策略也可能创建新线程取决于具体实现但通常优先使用空闲线程。如果运行的线程数 corePoolSize则尝试将任务放入workQueue。如果队列已满且运行的线程数 maximumPoolSize则创建新线程非核心执行任务。如果队列已满且运行的线程数已达maximumPoolSize则触发RejectedExecutionHandler。调优实战案例一个订单处理服务假设我们有一个订单处理服务任务是CPU密集型计算优惠、库存校验等。机器配置4核CPU。初步设置corePoolSize 4 1 5,maximumPoolSize 10。队列选择使用ArrayBlockingQueue容量设为100。拒绝策略自定义将拒绝的订单ID记录到Redis或发到Kafka后续补偿。上线后通过监控如Micrometer Prometheus发现线程数长期在5-6个队列很少堆积。说明核心线程数设置基本合理。在促销期间监控到有任务被拒绝。分析日志发现拒绝发生在流量尖峰持续约2秒。优化此时不应盲目调大线程数CPU密集型任务线程太多反而因频繁上下文切换导致性能下降。我们采取的措施是优化任务本身分析被拒绝的任务看是否有计算逻辑可以优化缩短单个任务处理时间。扩容队列将队列容量从100调整为200以应对更短暂的尖峰。但要注意队列容量太大会增加任务延迟。完善降级在自定义拒绝策略中除了记录立即给用户返回“系统繁忙请稍后再试”的友好提示并触发异步补偿流程。3.3 线程池的关闭与监控正确关闭shutdown()和shutdownNow()。shutdown()温和关闭。不再接受新任务但会执行完已提交的任务和队列中的任务。shutdownNow()暴力关闭。尝试中断所有正在执行的任务不再处理队列中的任务返回尚未开始执行的任务列表。最佳实践通常先调用shutdown()然后awaitTermination等待一段时间如果超时仍有任务未完成再调用shutdownNow()。executor.shutdown(); try { if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { executor.shutdownNow(); if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { System.err.println(线程池未能正常终止); } } } catch (InterruptedException ie) { executor.shutdownNow(); Thread.currentThread().interrupt(); // 保留中断状态 }监控指标生产环境必须监控线程池。活动线程数反映当前忙碌程度。队列大小反映任务积压情况。已完成任务数反映吞吐量。拒绝任务数这是最重要的告警指标之一说明系统已过载。 可以利用ThreadPoolExecutor自带的方法getActiveCount(),getQueue().size()等来暴露这些指标到你的监控系统。4.CompletableFuture异步编程的“瑞士军刀”在Java 8之前处理异步任务主要靠Future但它获取结果的方式是阻塞的get()方法且难以描述任务间的依赖关系如“任务A和B都完成后再执行C”。CompletableFuture的出现让Java拥有了强大的函数式异步编程能力。4.1 核心概念创建与简单转换创建异步任务// 1. 使用默认的 ForkJoinPool.commonPool() 执行 CompletableFutureString future1 CompletableFuture.supplyAsync(() - { // 模拟耗时计算 try { Thread.sleep(1000); } catch (InterruptedException e) { } return Result from supplyAsync; }); // 2. 使用自定义线程池生产环境推荐 ExecutorService customExecutor Executors.newFixedThreadPool(5); CompletableFutureString future2 CompletableFuture.supplyAsync(() - { return Result with custom executor; }, customExecutor);注意supplyAsync用于有返回值的任务runAsync用于无返回值的任务。强烈建议为CPU密集型或重要的IO任务指定自定义线程池避免所有CompletableFuture共享同一个公共池导致相互影响。处理计算结果thenApply,thenAccept,thenRun这三个方法是链式调用的基础分别代表转换、消费和执行。thenApply(FunctionT, U)接收上一个任务的结果进行转换返回新的CompletableFutureU。thenAccept(ConsumerT)接收结果进行消费如打印、保存不返回新结果。thenRun(Runnable)不关心上一个任务的结果只是在前一个阶段完成后执行一个动作。CompletableFuture.supplyAsync(() - Hello) .thenApply(s - s World) // 转换得到 Hello World .thenApply(String::toUpperCase) // 转换得到 HELLO WORLD .thenAccept(System.out::println) // 消费打印结果 .thenRun(() - System.out.println(All done.)); // 执行打印完成信息关键点这些方法都有对应的异步版本thenApplyAsync等它们会将后续的任务提交到线程池中执行而不是由完成上一个任务的线程直接执行。这可以避免某个耗时任务阻塞整个链。4.2 组合任务描述复杂的依赖关系这是CompletableFuture最强大的地方。thenCompose扁平化依赖类似flatMap用于串联两个有依赖关系的异步任务第二个任务需要第一个任务的结果。// 模拟先根据用户ID查询用户信息再根据用户信息中的地址ID查询地址 CompletableFutureUser userFuture getUserAsync(userId); CompletableFutureAddress addressFuture userFuture.thenCompose(user - { return getAddressAsync(user.getAddressId()); // 此操作返回一个新的CompletableFutureAddress }); // addressFuture 最终完成时得到的是地址对象而不是嵌套的Future。thenCombine合并两个独立任务的结果两个异步任务并行执行当它们都完成后对它们的结果进行合并处理。CompletableFutureInteger futureA getPriceAsync(itemA); CompletableFutureDouble futureB getDiscountAsync(user123); CompletableFutureDouble totalPriceFuture futureA.thenCombine(futureB, (price, discount) - { return price * discount; // 合并计算最终价格 });allOf/anyOf等待多个任务allOf(CompletableFuture?... cfs)返回一个新的Future当所有给定的Future都完成时它才完成。它没有结果值常用于等待一批并行任务全部结束。CompletableFutureVoid allFutures CompletableFuture.allOf(future1, future2, future3); allFutures.thenRun(() - { // 所有任务都完成了可以执行后续操作比如汇总结果 // 注意要获取各个future的结果仍需调用 future1.join() 等 });anyOf(CompletableFuture?... cfs)返回一个新的Future当任意一个给定的Future完成时它就完成其结果与最先完成的那个Future相同。可用于实现“竞速”或超时备用。4.3 异常处理与超时控制异常处理exceptionally和handleexceptionally(FunctionThrowable, T)相当于catch当链中之前的阶段出现异常时提供一个新的返回值。CompletableFuture.supplyAsync(() - { if (new Random().nextBoolean()) { throw new RuntimeException(Oops!); } return Success; }).exceptionally(ex - { System.err.println(Error: ex.getMessage()); return Default Value; // 提供降级值 }).thenAccept(System.out::println);handle(BiFunctionT, Throwable, U)无论成功还是异常都会执行它同时接收结果和异常可以统一处理。.handle((result, ex) - { if (ex ! null) { return Handled Error: ex.getMessage(); } return Result: result; })超时控制Java 9 Java 9为CompletableFuture增加了orTimeout和completeOnTimeout方法使得超时处理变得异常简单。CompletableFutureString future CompletableFuture.supplyAsync(() - { try { Thread.sleep(2000); } catch (InterruptedException e) { } return Result; }) .orTimeout(1, TimeUnit.SECONDS) // 设置1秒超时超时后抛出 TimeoutException .exceptionally(ex - Fallback due to timeout: ex.getClass().getSimpleName());对于Java 8需要通过completeOnTimeout或与ScheduledExecutorService配合来实现超时。实战心得CompletableFuture的链式调用虽然优雅但过长的链和复杂的组合会降低代码可读性。在复杂的业务流中可以考虑将其拆分成多个有命名意义的方法。另外要小心回调地狱虽然CompletableFuture比纯回调好但嵌套过深依然难以维护。对于非常复杂的异步流程可以考虑使用响应式编程库如Project Reactor。5. 锁的进阶ReentrantLock与AQS窥探synchronized是JVM内置的锁简单易用。而ReentrantLock作为JUC提供的显式锁提供了更灵活、更强大的功能。5.1ReentrantLock的核心优势可中断的锁获取lockInterruptibly()方法允许在等待锁的过程中响应中断这对于实现可取消的任务非常重要。尝试非阻塞获取锁tryLock()方法尝试获取锁如果锁被占用它不会阻塞而是立即返回false。可以用于避免死锁或实现某些特定逻辑。公平锁与非公平锁ReentrantLock的构造器可以指定是否创建公平锁。公平锁保证等待时间最长的线程优先获取锁避免了“饥饿”但会带来更大的性能开销因为需要维护一个有序队列。非公平锁是默认的也是性能更高的选择在大多数高并发场景下推荐使用。绑定多个条件一个ReentrantLock可以创建多个Condition对象实现更精细的线程间通信。synchronized只能有一个等待集wait/notifyAll。5.2 抽象队列同步器AQS浅析ReentrantLock、Semaphore、CountDownLatch等许多JUC同步工具其底层都依赖于一个共同的框架——AbstractQueuedSynchronizer (AQS)。理解AQS有助于我们看清这些工具的本质。AQS的核心思想是它维护了一个volatile int state同步状态和一个FIFO线程等待队列CLH队列的变体。对于不同的同步器state的含义不同。对于ReentrantLockstate表示锁被重入的次数对于Semaphorestate表示剩余的许可数量对于CountDownLatchstate表示倒计数的初始值。同步器需要重写AQS的tryAcquire、tryRelease等方法来定义如何获取和释放状态。当线程尝试获取状态失败时AQS会将线程封装成节点加入队列并可能阻塞该线程。当状态释放时AQS会负责唤醒队列中的后继线程。以ReentrantLock的非公平锁实现为例lock()方法首先会直接尝试用CAS将state从0改为1快速路径如果成功就将当前线程设为独占所有者。这体现了“非公平”性新来的线程可能比队列中等待的线程先拿到锁。如果快速路径失败则调用AQS的acquire方法最终会调用子类重写的tryAcquire再次尝试如果还失败就将线程加入队列并可能挂起。学习建议对于大多数应用开发者无需深究AQS的每一个细节。但了解其基本原理能让你在遇到复杂的同步问题时知道该从哪个方向去查阅源码和资料也能更好地理解那些基于AQS构建的工具的行为。这是从“会用”到“懂原理”的关键一步。6. 实战避坑与性能调优经验录理论最终要服务于实践。下面是我在多年高并发项目开发中总结的一些常见“坑”和调优经验。6.1 线程安全与可见性那些容易忽略的细节“单例模式”的双重检查锁DCL陷阱与正确写法老生常谈但依然有人写错。错误的DCL在于instance new Singleton()这行代码不是原子的它可能发生指令重排导致其他线程拿到一个未初始化完全的对象。// 错误示例在旧版本Java内存模型下有问题 public class Singleton { private static Singleton instance; public static Singleton getInstance() { if (instance null) { // 第一次检查 synchronized (Singleton.class) { if (instance null) { // 第二次检查 instance new Singleton(); // 问题在此 } } } return instance; } }正确写法方法一最简洁利用类加载机制推荐。public class Singleton { private static class Holder { private static final Singleton INSTANCE new Singleton(); } public static Singleton getInstance() { return Holder.INSTANCE; } }方法二使用volatile关键字JDK5。public class Singleton { private static volatile Singleton instance; public static Singleton getInstance() { if (instance null) { synchronized (Singleton.class) { if (instance null) { instance new Singleton(); } } } return instance; } }volatile不能保证复合操作的原子性如前所述volatile只保证可见性和禁止指令重排。count这种“读-改-写”操作必须使用synchronized或原子类。线程局部变量ThreadLocal的内存泄漏ThreadLocal的经典内存泄漏场景使用线程池时线程是复用的。如果ThreadLocal变量用完后没有调用remove()清理那么该线程的ThreadLocalMap中会一直保留对value的强引用Entry的key是弱引用但value是强引用导致value对象无法被回收。最佳实践在使用完ThreadLocal后务必在finally块中调用threadLocal.remove()。6.2 死锁与活锁的诊断与预防死锁四个必要条件互斥、持有并等待、不可剥夺、循环等待。预防死锁的核心是打破循环等待。一个实用的方法是定义锁的获取顺序所有线程都按相同的全局顺序申请锁。// 定义锁的顺序 private static final Object lock1 new Object(); private static final Object lock2 new Object(); public void method1() { synchronized (lock1) { // 先获取lock1 synchronized (lock2) { // 再获取lock2 // do something } } } public void method2() { synchronized (lock1) { // 同样先获取lock1即使它只需要lock2 synchronized (lock2) { // do something else } } }活锁线程没有阻塞但在不断重试某个总是失败的操作比如两个线程互相谦让资源导致谁都无法进行。解决方案是引入随机退避时间。6.3 性能调优监控指标线上高并发系统必须监控以下与线程相关的指标线程状态通过jstack或Arthas等工具定期查看线程状态分布。大量的BLOCKED或WAITING线程可能是锁竞争激烈或IO等待的征兆。锁竞争使用jstack查看线程等待的锁或使用JMX、ReentrantLock的getQueueLength()等方法监控等待特定锁的线程数。CPU使用率与上下文切换过高的上下文切换vmstat中的cs列意味着线程过多或锁竞争激烈。结合pidstat或top -H查看具体进程和线程的CPU使用情况。GC情况不当的并发对象创建如在循环中new大量临时对象会导致Young GC频繁甚至引发Full GC。监控GC频率和耗时。6.4 虚拟线程Java 21的展望Java 21引入的虚拟线程Virtual Threads是并发编程的一次重大革新。它由JVM管理非常轻量初始内存约几百字节可以创建数百万个而不会导致系统资源耗尽。其目标是用简单的同步阻塞代码风格获得异步非阻塞的高性能。 对于传统的、大量时间花在等待IO如数据库查询、网络调用上的业务代码可以几乎不做修改只需将ExecutorService换成Executors.newVirtualThreadPerTaskExecutor()就能获得巨大的吞吐量提升因为它将阻塞的OS线程释放出来去执行其他虚拟线程的任务。当前建议如果你的项目已使用Java 21并且是IO密集型应用强烈建议开始评估和测试虚拟线程。但对于CPU密集型任务或依赖现有复杂线程池调优逻辑的应用迁移需谨慎。虚拟线程是未来但理解好今天的平台线程Thread和JUC是拥抱这个未来的坚实基础。高并发编程是一个既需要深厚理论支撑又需要大量实战经验积累的领域。JUC提供了一套强大的工业级工具但工具本身不会写出好代码。真正的关键在于你是否理解每个工具背后的设计意图、适用场景和潜在陷阱并能在复杂的业务逻辑中做出恰当的选择和组合。希望这篇来自一线的万字心得能成为你工具箱里一件称手的兵器助你在高并发的战场上更加游刃有余。记住没有银弹持续学习、谨慎实践、重视监控才是应对并发挑战的不二法门。
返回列表