深入理解 JUCCallable、线程池、信号量、CountDownLatch 与 ConcurrentHashMap 全解析JUCjava.util.concurrent是 JDK 提供的并发编程工具包把多线程开发中的常见问题都给出了现成方案。本文先讲 Callable 接口与创建线程的四种方式再讲信号量与线程安全的几种手段、CountDownLatch 的使用最后重点剖析多线程环境下的哈希表——从 HashMap 的线程安全问题到 Hashtable 的改进再到 ConcurrentHashMap 的锁桶优化与化整为零式扩容。一、Callable 接口的使用1.1 Runnable 的不足前面用Runnable创建任务时会发现两个痛点run()方法返回值是void任务执行完拿不到结果run()不能抛出受检异常异常只能内部消化。Callable就是为解决这两个问题而生的publicinterfaceCallableV{Vcall()throwsException;}泛型参数V就是返回值类型call()允许抛出异常。1.2 Callable 的使用配合 FutureTaskThread的构造方法只认Runnable不直接认Callable所以需要一个桥梁——FutureTask它实现了Runnable内部包装了Callableimportjava.util.concurrent.Callable;importjava.util.concurrent.FutureTask;publicclassCallableDemo{publicstaticvoidmain(String[]args)throwsException{// 1. 创建任务计算 1 2 ... 100CallableIntegertask()-{intsum0;for(inti1;i100;i){sumi;}returnsum;};// 2. 用 FutureTask 包装 CallableFutureTaskIntegerfutureTasknewFutureTask(task);// 3. 交给线程执行ThreadtnewThread(futureTask);t.start();// 4. 获取结果如果任务还没算完get() 会阻塞等待IntegerresultfutureTask.get();System.out.println(计算结果result);// 5050}}要点futureTask.get()是阻塞式的任务没执行完get()就一直等算完了立刻返回结果FutureTask内部有一套等待 结果暂存机制本质上是一次执行、多方取结果。二、创建线程的四种方法总结方式一继承 Thread重写 runclassMyThreadextendsThread{Overridepublicvoidrun(){System.out.println(继承 Thread 创建线程);}}// 使用new MyThread().start();优点写法最简单直观缺点Java 单继承继承了 Thread 就没法再继承别的类任务和线程耦合在一起。方式二实现 Runnable交给 Thread 执行classMyRunnableimplementsRunnable{Overridepublicvoidrun(){System.out.println(实现 Runnable 创建线程);}}// 使用new Thread(new MyRunnable()).start();优点任务和线程解耦不影响继承体系还能配合 lambda 使用new Thread(() - {...}).start()缺点run()没有返回值不能抛异常。方式三实现 Callable配合 FutureTask有返回值CallableIntegertask()-12;FutureTaskIntegerfutureTasknewFutureTask(task);newThread(futureTask).start();IntegerresultfutureTask.get();// 能拿到返回值优点有返回值、能抛异常缺点写法稍繁琐get()会阻塞。方式四使用线程池推荐importjava.util.concurrent.ExecutorService;importjava.util.concurrent.Executors;ExecutorServicepoolExecutors.newFixedThreadPool(4);// 固定 4 个线程的池pool.submit(()-System.out.println(线程池执行任务));pool.shutdown();// 用完关闭优点线程复用避免频繁创建/销毁线程的开销还能统一管理线程数量、队列、拒绝策略是实际开发中的首选补充生产环境更推荐直接用new ThreadPoolExecutor(...)手动构造线程池参数可控Executors的某些工厂方法可能造成队列无界堆积或线程数暴涨。一句话总结四种方式继承 Thread 最直接Runnable 解耦任务Callable 带返回值线程池负责复用和管理。三、信号量Semaphore3.1 什么是信号量信号量Semaphore用一个计数器来表示当前可用资源的个数提供两个核心操作P 操作acquire申请一个资源计数器减 1如果计数器已经是 0没有资源可申请就阻塞等待V 操作release释放一个资源计数器加 1如果有线程在等就唤醒一个。这两个操作本身都是原子的由 JDK 保证。打个比方停车场有 10 个车位计数器初始为 10。车进来一个P可用车位减 1停满了后来的车在大门口排队等阻塞。车走了V车位 1唤醒一辆排队的车。信号量和锁的区别锁可以看成计数器只有 1 个名额的信号量互斥而信号量允许N 个线程同时访问资源共享。3.2 信号量的使用importjava.util.concurrent.Semaphore;publicclassSemaphoreDemo{publicstaticvoidmain(String[]args){// 创建信号量3 个可用资源相当于 3 个车位SemaphoresemaphorenewSemaphore(3);Runnabletask()-{try{semaphore.acquire();// P 操作申请资源计数器减 1System.out.println(Thread.currentThread().getName() 获得资源开始工作);Thread.sleep(1000);// 模拟占用资源}catch(InterruptedExceptione){e.printStackTrace();}finally{System.out.println(Thread.currentThread().getName() 释放资源);semaphore.release();// V 操作释放资源计数器加 1必须放 finally}};// 开 6 个线程抢 3 个资源同时最多只有 3 个线程在执行for(inti0;i6;i){newThread(task,线程-i).start();}}}运行效果每次最多只有 3 个线程打印获得资源其他线程阻塞等待有线程release()之后等待的线程才能进去。典型应用限流限制同时访问某资源的线程数、连接池限制数据库连接同时被多少线程使用。四、线程安全的操作总结多线程修改共享数据核心问题就是一个线程改到一半另一个线程插进来了。保证线程安全主流手段有三种4.1 方式一加锁悲观方案synchronizedJVM 层面的锁自动加锁自动解锁ReentrantLock类库层面的锁手动 lock/unlock支持 tryLock 超时、公平锁、Condition。思想是悲观的先假设会冲突谁拿到锁谁才能操作其他线程排队等。牺牲一部分并发性换取绝对的互斥。synchronized(lock){count;}4.2 方式二CAS乐观方案不加锁直接操作修改前先比较值有没有被别人改过没改过才写入改过就重试典型代表AtomicInteger等原子类、自旋锁思想是乐观的赌冲突很少发生冲突了就重试。AtomicIntegercountnewAtomicInteger(0);count.incrementAndGet();// CAS 自增无需加锁4.3 方式三信号量限流方案信号量不追求互斥而是控制同时访问的线程数量计数器为 1 时等价于锁互斥计数器为 N 时允许 N 个线程并发适合资源有多份或需要限流的场景。4.4 三种方式对比维度加锁CAS信号量思想悲观先锁再操作乐观冲突就重试限流控制并发数量并发度串行一次一个无锁并发N 个并发典型代表synchronized / ReentrantLock原子类、自旋锁Semaphore适用场景临界区复杂、竞争激烈简单变量操作、冲突少资源池、限流选择思路简单变量自增用 CAS原子类复杂临界区用锁控制同时访问数量用信号量。五、CountDownLatch 的使用5.1 什么是 CountDownLatchCountDownLatch倒数计时闩解决的问题是主线程需要等 N 个子线程都干完活再继续往下执行。它像一个带数字的倒计时牌new CountDownLatch(n)倒计时牌显示 nlatch.countDown()某个子线程完成任务数字减 1latch.await()主线程在数字减到 0 之前一直阻塞减到 0 才放行。5.2 使用示例等待多个任务全部完成importjava.util.concurrent.CountDownLatch;publicclassCountDownLatchDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{inttaskCount5;CountDownLatchlatchnewCountDownLatch(taskCount);// 计数器 5for(inti1;itaskCount;i){finalintidi;newThread(()-{try{Thread.sleep((long)(Math.random()*1000));System.out.println(任务 id 完成);}catch(InterruptedExceptione){e.printStackTrace();}finally{latch.countDown();// 每完成一个计数器减 1}}).start();}latch.await();// 主线程阻塞直到计数器减到 0System.out.println(所有任务已完成主线程继续执行);}}要点countDown()要放在finally中保证即使任务抛异常计数也会减await()可以带超时latch.await(3, TimeUnit.SECONDS)等不到就算了避免永久阻塞计数器不能重置一次性工具需要重复使用可考虑CyclicBarrier。典型场景赛跑比赛中裁判等所有运动员就位、大任务拆成 N 个子任务并行执行后汇总结果。六、JUC 中线程安全的集合类简述JUC 还提供了一批线程安全的集合类先看多线程环境使用 ArrayList的三种做法6.1 自己使用同步机制加锁ListIntegerlistnewArrayList();synchronized(list){list.add(1);}可以保证安全但要自己保证所有使用这个 list 的地方都用同一把锁容易漏。前面锁的文章讨论过此处不展开。6.2 Collections.synchronizedListListIntegerlistCollections.synchronizedList(newArrayList());标准库提供的一个基于synchronized进行线程同步的 List 包装——返回的 synchronizedList 的关键方法add、get、set……都是带有 synchronized 关键字的。优点是简单省事缺点是所有操作都走一把大锁并发性能一般。6.3 CopyOnWriteArrayList写时拷贝ListIntegerlistnewCopyOnWriteArrayList();Copy On Write写时拷贝当我们往容器里添加元素的时候不直接往当前容器添加而是先将当前容器进行 Copy复制出一个新的容器然后往新容器里添加元素添加完元素之后再将原容器的引用指向新的容器读操作不加锁读的永远是引用指向的容器读写不冲突适合读多写少的场景缺点每次写都要复制整个数组写的开销大读的时候可能读到稍旧的数据。“写时拷贝”Copy On Write是一个通用的技术思想隔壁重点讲过C 的std::stringC 标准库的函数都在std::命名空间中::是命名空间可以认为是包概念的前身、Linux 的fork()创建子进程都运用了这个思想。七、重点多线程环境使用哈希表哈希表是并发场景中最常用的数据结构缓存、计数、映射……也是线程安全问题最容易爆雷的地方。围绕线程安全的哈希表JDK 经历了三个阶段的演进。7.1 HashMap 为什么有线程安全问题HashMap完全没有做任何同步措施多线程并发读写时问题很多数据丢失两个线程同时put都先读到同一个桶是空的然后各自写入——后写的覆盖先写的一次 put 直接丢失size 计数不准size不是原子操作读-改-写三步并发自增会少加扩容期间结构混乱一个线程正在 rehash 搬运数据另一个线程同时读写可能读到错误的桶、甚至形成环形链表JDK 7 的经典死循环问题——扩容时头插法倒转链表并发下链表成环之后get撞上这个桶就死循环 CPU 100%可见性问题没有volatile/锁一个线程的修改另一个线程可能看不到。所以多线程环境禁止使用 HashMap除非只读。7.2 Hashtable最直接的改进——关键方法加 synchronizedHashtable是最早的补救方案思路简单粗暴把所有可能涉及线程安全的关键方法都加上synchronized。// Hashtable 源码示意publicsynchronizedVput(Kkey,Vvalue){...}publicsynchronizedVget(Objectkey){...}publicsynchronizedintsize(){...}publicsynchronizedVremove(Objectkey){...}这样确实线程安全了但代价巨大——一把大锁锁住整个哈希表两个线程就算操作的是完全不同的桶、完全不同的 key也要互相竞争同一把锁纯粹的多余排队读和读也互斥读get也 synchronized并发度被压成串行高并发下所有线程挤在锁上性能极差。** Hashtable 安全但太慢。**7.3 ConcurrentHashMap 的进一步优化锁桶方案缩小锁的粒度ConcurrentHashMap的核心改进思想既然问题出在锁的粒度太粗那就把锁的粒度缩小——锁桶。Hashtable一把大锁锁整张表——任何操作都要抢同一把锁ConcurrentHashMap每条链表每个桶各有一把锁——加锁只锁单个桶的头节点JDK 8 中使用synchronized锁桶头节点。Hashtable: [ 锁住整个表 ] ← 所有线程抢一把锁 ConcurrentHashMap: 桶0 [锁A] → a → b 桶1 [锁B] → c ← 线程1锁桶0时线程2照样能操作桶1 桶2 [锁C] → d → e → f好处两个线程操作的 key 只要落在不同的桶上就完全不冲突、可以并行只有恰好落在同一个桶哈希冲突了才需要排队。锁的粒度从整张表缩小到单个桶冲突概率大大降低并发度大幅提升。其他配套优化了解读操作不加锁桶节点和value用volatile修饰保证可见性读性能不受影响size 统计不简单用一个size而是借鉴 LongAdder 的思想分段计数最后求和减少 CAS 冲突。补充JDK 7 的 ConcurrentHashMap 采用分段锁Segment把表分成 16 段每段一把锁JDK 8 改为上述锁桶方案粒度更细实现也更简洁。本质相同都是在缩小锁的范围。7.4 ConcurrentHashMap 扩容的优化化整为零渐进式搬运HashMap 扩容是一次性的触发扩容后新建一个大数组一口气把所有元素 rehash 搬运过去。单线程没问题但放到多线程环境如果扩容也要一次搬完搬运期间整个表就得长时间被锁住——其他线程全部卡死。ConcurrentHashMap的做法是化整为零渐进式 rehash发现需要扩容时先创建一个两倍大小的新数组不是一次性把所有数据搬过去而是每次搬运一点点——每个线程在对某个桶进行操作时顺手帮忙搬运一小部分桶的数据也可以有专门的迁移线程每搬完一部分就通过标识记录进度下次接着搬搬运期间旧表和新表同时存在读请求如果发现数据还没搬就去旧表找写请求则直接写入新表并顺带搬运直到所有桶都搬完再用新表彻底替换旧表。旧表: [桶0][桶1][桶2][桶3][桶4][桶5]... ↓ 化整为零每次搬 2 个桶 新表: [桶0][桶1] ← 已搬完的桶打上标记后续直接访问新表好处把一次性的、耗时的扩容开销摊分到后续的多次操作中每次操作只多付一小点代价避免了某一次操作突然卡顿很久也避免了扩容期间长时间锁住全表。7.5 HashMap、Hashtable、ConcurrentHashMap 对比对比维度HashMapHashtableConcurrentHashMap线程安全不安全安全安全实现方式无任何同步关键方法加synchronized一把大锁锁全表锁桶锁单个桶的头节点 CAS volatile锁粒度无锁整张表一把锁一个桶一把锁甚至读不加锁并发性能单线程最快高并发下极差高并发下优秀null key允许 1 个不允许不允许null value允许不允许不允许迭代器fail-fast迭代中修改抛异常安全但串行弱一致性不抛异常能容忍并发修改扩容一次性 rehash一次性 rehash化整为零、渐进式搬运使用建议单线程环境已过时不推荐多线程环境首选一句话选型单线程用 HashMap多线程用 ConcurrentHashMapHashtable 请从代码里移除。八、总结创建线程四种方式继承 Thread、实现 Runnable、实现 Callable配合 FutureTask 拿返回值、线程池开发首选线程复用 统一管理信号量 Semaphore计数器表示可用资源数acquire 减 1不够就阻塞release 加 1用来控制同时访问的线程数量线程安全三板斧加锁悲观互斥串行、CAS乐观冲突重试、信号量限流N 路并发CountDownLatch倒计时闩主线程await()等待 N 个子任务全部countDown()后再继续线程安全集合ArrayList 可用加锁 / synchronizedList / CopyOnWriteArrayList写时拷贝读多写少线程安全哈希表演进HashMap 不安全 → Hashtable 全表一把大锁安全但慢→ ConcurrentHashMap 锁桶粒度细并发高 扩容化整为零渐进式搬运避免一次性卡顿选型结论多线程环境下哈希表无脑选ConcurrentHashMap。JUC 的设计哲学可以浓缩成一句话为不同场景提供不同粒度的并发控制——从无锁 CAS到锁桶到信号量限流再到任务编排工具。希望这篇文章对你有帮助欢迎点赞收藏
企业数字化 ERP 产品动态
相关推荐
AgentEvals(agentevals-cli)Agent评测方法与步骤 AgentEvals(agentevals-cli)Agent评测方法与步骤
框架定位与核心原理
业内常说的 AgentEvals 官方全称为 agentevals-cli,是 Strands Labs 开源的框架无关、OpenTelemetry(OTel) Trace 驱动的Agent评测工具,核心设计理念是「一次… · 2026/11/4 2:35:15
【Linux网络】传输层协议TCP(2)连接管理、三次握手、四次挥手 hello~ 很高兴见到大家! 这次带来的是Linux网络中关于网络基础这部分的一些知识点,如果对你有所帮助的话,可否留下你宝贵的三连呢? 个 人 主 页: 默|笙 文章目录一、连接管理机制1.1 连接与三次握手连接管理三次握手1.2 四次挥手连接管理与理解 TIME_WAIT 状态MSL地址重用/端口… · 2026/11/4 2:35:15
Codex 做桌面理线盒:参数改一次,CAD 和 Blender 产品图一起更新 Codex 做桌面理线盒:参数改一次,CAD 和 Blender 产品图一起更新 桌上充电线会从盒边滑出来,通常不是“再买一个收纳盒”能解决的问题,而是出口直径、盒内余量和桌面位置没有变成可改的尺寸。这里做一个 965822 mm 的理线盒:两侧有 10 mm 线口,盒内保留 4 mm 底厚。本文记… · 2026/11/4 2:34:15
大数据清洗实战:从原理到分布式处理技巧 1. 数据清洗:大数据处理的隐形基石作为一名处理过上百TB数据集的老数据工程师,我深知数据清洗这个"脏活累活"的重要性。记得有次接手一个金融风控项目,原始数据中仅地址字段就有17种不同的格式,还有大量重复、缺失和逻辑… · 2026/11/4 3:23:24
Hermes Agent 实战:循环控制、工具契约与上下文预算 很多人第一次接触 Hermes Agent,第一反应是"不就是包了一层模型接口吗",于是花半小时照着示例把 demo 跑通,然后直接接进真实业务,结果第二天就开始崩:任务跑一半停了、工具被同一个参数反复调用十几轮、结构… · 2026/11/4 3:23:24
Hermes Agent自主执行循环:工具调用、上下文与熔断 装完 Hermes Agent 的第一件事,我一般不会是跑官方 demo,而是先把它的循环日志打开,看一次完整任务里它到底调了几次工具、每次拿到什么返回值。这个习惯是被坑出来的——早期我以为它只是个套了壳的对话命令行,结果它自作主张地改… · 2026/11/4 3:23:24
Hermes Agent 实战:从工具注册到任务编排的智能体教程 第一次接触 Hermes Agent 的时候,我把它当成又一个提示词封装库,跑完第一个例子就发现想错了。它更像一套任务运行时:你告诉它目标,它自己决定调哪个工具、按什么顺序调、失败了怎么重来、什么时候该停下来问人。这个差别很关键&a… · 2026/11/4 3:23:24
金融级数据处理:Apache SeaTunnel实战与优化 1. 项目背景与核心价值在金融数据处理领域,每天需要处理TB级别的交易记录、客户信息和市场数据。传统ETL工具在应对这种规模时往往面临性能瓶颈、扩展性不足和维护成本高等问题。某全球顶级金融机构(以下简称"该机构")的数据团队在… · 2026/11/4 3:23:24
机器学习三大分支:监督学习、无监督学习与强化学习实战解析 1. 机器学习领域全景概览第一次接触机器学习时,我被各种术语搞得晕头转向——监督学习、无监督学习、强化学习,这些概念就像迷宫里的岔路。直到在电商平台做了三年的推荐系统开发后,我才真正理解这三个核心分支就像厨师做菜的不同方式&#x… · 2026/11/4 3:22:24
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 6:04:24
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46
AI辅助技术栈评估:提升研发决策效率60% 1. 项目背景与核心价值技术决策一直是研发团队面临的重大挑战。随着技术生态的快速演进,技术栈的选择、维护和迭代变得愈发复杂。我们团队在过去两年里,先后经历了三次重大技术栈迁移,每次都需要投入大量人力进行技术评估和风险分析。直到去年… · 2026/11/4 0:00:17
从生成Token到直接决策:Jev如何重塑AI Agent工程实践 1. 从"生成文字"到"拍板决策":Jev 到底在解决什么问题大多数人第一次接触大模型,脑子里建立的模型是"输入问题、输出文字"。你问它今天天气怎么样,它给你一段描述;你让它写个周报,它给你… · 2026/11/4 0:00:17
32G统一内存跑大模型:内存账、TPS实测与端云分流 想在一台 32G 统一内存的小主机上把大模型跑成"日常可用",这件事我前后折腾了小半年。标题里说的这台 Mac mini M6,是我们这次讨论的目标平台,具体到手规格以实物为准;但下面这套推算方法和实测思路,对任何统… · 2026/11/4 0:00:17
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 6:04:24
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46