【JVM深度解析】第27篇:并发编程实战案例与陷阱
摘要理论千遍不如实践一遍。本文通过六个真实场景的并发问题展示多线程编程中的常见陷阱线程池 OOM、ThreadLocal 内存泄漏、双重检查锁单的隐藏危险、HashMap 并发死循环、生产者消费者模式死锁、以及 CountDownLatch 误用导致的测试失败。每个案例都包含问题描述、根因分析、解决方案和验证代码。掌握这些你才能避免踩坑写出正确高效的并发代码。案例一线程池 OOM1.1 问题描述// 问题代码publicclassThreadPoolOOM{// 线程池核心线程数为 100队列无限大privatestaticfinalExecutorServiceexecutorExecutors.newFixedThreadPool(100);publicvoidprocess(ListTasktasks){for(Tasktask:tasks){executor.submit(()-{// 处理任务doProcess(task);});}}}// 根因分析// - newFixedThreadPool(100) 实际创建了 LinkedBlockingQueue()// - LinkedBlockingQueue 默认容量 Integer.MAX_VALUE// - 当任务提交速度 处理速度时队列无限增长// - 导致 OOM1.2 解决方案// 解决方案限制队列大小publicclassThreadPoolOOMFixed{privatestaticfinalintCORE_POOL_SIZE100;privatestaticfinalintMAX_POOL_SIZE100;privatestaticfinalintQUEUE_SIZE1000;privatestaticfinalExecutorServiceexecutornewThreadPoolExecutor(CORE_POOL_SIZE,MAX_POOL_SIZE,0L,TimeUnit.MILLISECONDS,newLinkedBlockingQueue(QUEUE_SIZE),// 限制队列大小newThreadPoolExecutor.CallerRunsPolicy()// 拒绝策略调用者执行);}案例二ThreadLocal 内存泄漏2.1 问题描述// 问题代码publicclassUserContextHolder{publicstaticfinalThreadLocalUsercurrentUsernewThreadLocal();}// Spring MVC 控制器publicclassUserController{GetMapping(/user/{id})publicUsergetUser(PathVariableLongid){UseruseruserService.getById(id);UserContextHolder.currentUser.set(user);// 设置用户// 处理请求...// 问题忘记清理// 线程池复用时ThreadLocal 中的数据不会被 GC 回收returnuser;}}2.2 解决方案// 方案一finally 中清理publicclassUserControllerFixed{GetMapping(/user/{id})publicUsergetUser(PathVariableLongid){UseruseruserService.getById(id);UserContextHolder.currentUser.set(user);try{// 处理请求returnprocessUser(user);}finally{UserContextHolder.currentUser.remove();// 清理}}}// 方案二使用框架的请求上下文推荐publicclassUserContextHolder2{publicstaticfinalThreadLocalUserContextcontextHoldernewThreadLocal();}// 方案三使用阿里 TransmittableThreadLocal异步场景publicclassTtlExample{// TransmittableThreadLocal 会在子线程中自动继承父线程的值// 且在线程池复用时自动处理清理}案例三HashMap 并发死循环3.1 问题描述// 问题代码publicclassConcurrentHashMapOOM{privateMapString,ObjectcachenewHashMap();publicvoidput(Stringkey,Objectvalue){cache.put(key,value);// HashMap 不是线程安全的}publicObjectget(Stringkey){returncache.get(key);}}// 根因分析// 并发 put 时HashMap 可能发生// 1. 扩容时链表成环// 2. 数据丢失// 3. get 时死循环CPU 100%3.2 解决方案// 方案一ConcurrentHashMapprivateMapString,ObjectcachenewConcurrentHashMap();publicvoidput(Stringkey,Objectvalue){cache.put(key,value);// 线程安全}// 方案二Collections.synchronizedMap性能较差privateMapString,ObjectcacheCollections.synchronizedMap(newHashMap());// 方案三Guava Cache推荐privateLoadingCacheString,ObjectcacheCacheBuilder.newBuilder().maximumSize(10000).expireAfterWrite(Duration.ofMinutes(10)).build(newCacheLoaderString,Object(){OverridepublicObjectload(Stringkey)throwsException{returnloadFromDB(key);}});案例四双重检查锁的陷阱4.1 问题描述// 错误版本publicclassSingletonBroken{privatestaticSingletonBrokeninstance;publicstaticSingletonBrokengetInstance(){if(instancenull){// T1: 第一个检查synchronized(SingletonBroken.class){if(instancenull){// T2: 第二个检查instancenewSingletonBroken();// 问题这行代码不是原子的// 分解为// 1. 分配内存// 2. 调用构造函数// 3. 写入 instance 引用// 可能发生重排序1 - 3 - 2}}}returninstance;}}// 危险场景// 线程 A 进入同步块执行了 instance new SingletonBroken(); 的步骤 1 和 3// 线程 B 检查 instance ! null直接返回 instance但对象还未初始化4.2 解决方案// 方案一volatile推荐publicclassSingletonFixed{privatestaticvolatileSingletonFixedinstance;publicstaticSingletonFixedgetInstance(){if(instancenull){synchronized(SingletonFixed.class){if(instancenull){instancenewSingletonFixed();}}}returninstance;}}// 方案二饿汉式类加载时就初始化publicclassSingletonHungry{privatestaticfinalSingletonHungryinstancenewSingletonHungry();privateSingletonHungry(){}publicstaticSingletonHungrygetInstance(){returninstance;}}// 方案三静态内部类延迟加载 线程安全publicclassSingletonInner{privateSingletonInner(){}privatestaticclassHolder{staticfinalSingletonInnerinstancenewSingletonInner();}publicstaticSingletonInnergetInstance(){returnHolder.instance;// 类加载时由 JVM 保证线程安全}}案例五CountDownLatch 误用5.1 问题描述// 错误代码publicclassCountDownLatchWrong{privatestaticfinalintTHREAD_COUNT10;TestpublicvoidtestConcurrent()throwsInterruptedException{CountDownLatchstartSignalnewCountDownLatch(1);// 错误CountDownLatchdoneSignalnewCountDownLatch(THREAD_COUNT);for(inti0;iTHREAD_COUNT;i){newThread(()-{try{startSignal.await();// 等待开始信号doWork();doneSignal.countDown();}catch(InterruptedExceptione){Thread.currentThread().interrupt();}}).start();}// 错误主线程等待 doneSignal但没有先触发 startSignaldoneSignal.await();// 永远等待下去}}5.2 解决方案// 正确代码publicclassCountDownLatchCorrect{privatestaticfinalintTHREAD_COUNT10;TestpublicvoidtestConcurrent()throwsInterruptedException{CountDownLatchstartSignalnewCountDownLatch(1);CountDownLatchdoneSignalnewCountDownLatch(THREAD_COUNT);for(inti0;iTHREAD_COUNT;i){newThread(()-{try{startSignal.await();// 所有线程先等待doWork();}finally{doneSignal.countDown();}}).start();}// 正确顺序先发送开始信号再等待完成Thread.sleep(100);// 确保所有线程都已就绪startSignal.countDown();// 发送开始信号doneSignal.await();// 等待所有线程完成assertTrue(所有线程应该执行完成,doneSignal.getCount()0);}}案例六生产者消费者死锁6.1 问题描述// 问题代码publicclassProducerConsumerDeadlock{privateBlockingQueueStringqueuenewLinkedBlockingQueue(10);publicvoidproduce()throwsInterruptedException{while(true){StringdatagenerateData();queue.put(data);// 阻塞直到队列不满}}publicvoidconsume()throwsInterruptedException{while(true){Stringdataqueue.take();// 阻塞直到队列不空process(data);}}}// 问题// 队列容量为 10// 如果只启动一个生产者它会一直生产直到队列满// 如果只启动一个消费者它会一直等待// 如果生产者和消费者同时启动队列满了后两者都阻塞// 但如果队列为空且消费者多于生产者 → 死锁6.2 解决方案// 方案使用线程池 信号量控制publicclassProducerConsumerFixed{privatefinalSemaphoreproducerSemaphorenewSemaphore(10);privatefinalSemaphoreconsumerSemaphorenewSemaphore(0);privatefinalQueueStringqueuenewLinkedList();publicvoidproduce()throwsInterruptedException{while(true){StringdatagenerateData();producerSemaphore.acquire();// 获取生产许可synchronized(queue){queue.add(data);}consumerSemaphore.release();// 释放消费许可}}publicvoidconsume()throwsInterruptedException{while(true){consumerSemaphore.acquire();// 获取消费许可Stringdata;synchronized(queue){dataqueue.poll();}process(data);producerSemaphore.release();// 释放生产许可}}}总结并发编程的陷阱无处不在线程池 OOM、ThreadLocal 泄漏、HashMap 并发、Double-Checked Locking、CountDownLatch 误用、生产者消费者死锁……每一种都是血泪教训。核心原则使用线程安全的集合、使用 ThreadLocal 时务必清理、使用 volatile/CAS/synchronized 保证可见性和原子性、使用限流和超时防止资源耗尽。系列导航上一篇【JVM深度解析】第26篇CAS、AQS与并发工具类原理下一篇【JVM深度解析】第28篇JVM发展史从Sun到Oracle系列目录JVM深度解析参考资料Java Concurrency in PracticeJava ThreadLocal - BaeldungDouble-Checked Locking - Wikipedia
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2530325.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!