Java 并发编程必会的 JUC 辅助类:从 Callable 到阻塞队列

简介: Java 并发编程的 Callable、CountDownLatch、Semaphore、读写锁与阻塞队列,一篇用场景和代码讲透,附四组 API 速查表。

大家好,我是晚安code。

学 Java 并发编程那阵子,我最怕的不是死锁,是 JUC 这堆辅助类的名字:CountDownLatch 和 CyclicBarrier 哪个是减法哪个是加法?Semaphore 到底管什么的?阻塞队列那四个 API 又有什么区别?面试被问一次、背一遍,过两周又混。今天我把它们放进一篇里串起来讲,每个配一段能直接跑的代码和一个生活化场景,再给一张阻塞队列的速查表,看完基本就忘不掉了。示例基于 JDK 8,2026 年 8 月整理,这些类的接口在 JDK 8~21 一直稳定。

先点个收藏,我们开始。

一、Callable:线程也要有返回值

Runnable 最大的遗憾就是 run() 没有返回值——当你需要让线程干完活把结果交回来时,就该上 Callable + FutureTask,这也是面试常问的「Callable 接口用法」。

Callable:Java 提供的带返回值任务接口,泛型 V 就是 call() 方法的返回类型。你可以把它理解成「能领工资的 Runnable」——同样是交给线程跑,但干完活会给你带回一个结果,还允许抛异常。

问题来了:new Thread() 只认 Runnable,接不了 Callable。中间的桥就是 FutureTask:它本身是个 Runnable,同时又能把 Callable 算出的结果存下来,等你去取。

public class CallableTest {
   
    public static void main(String[] args) throws Exception {
   
        FutureTask<String> task = new FutureTask<>(new MyCallable());
        new Thread(task, "线程A").start();
        String result = task.get();   // 没算完会阻塞等待
        System.out.println(result);
    }
}

class MyCallable implements Callable<String> {
   
    @Override
    public String call() {
   
        return "返回结果";   // 这里放耗时业务
    }
}

整条链路是:主线程创建 FutureTask → 子线程执行 call() → 结果存进 FutureTask → 主线程 get() 拿到返回值。看这张时序图,眼睛顺着箭头走一遍就记住了:

Java 并发编程中 Callable 和 FutureTask 拿返回值的时序链路:主线程启动子线程执行 call,get 阻塞等待后拿到结果

这里有两个细节,踩过的都懂:

  1. 同一个 FutureTask 丢给多个线程跑,结果会被缓存。 第二个线程再 get,直接拿缓存返回,不用重新算——我当初以为每个线程都会重跑一遍 call(),其实只有第一个跑。
  2. get() 会阻塞。 我第一次用没注意,直接在业务线程里 task.get(),主线程被卡到怀疑人生。

可能有人会问:futureTask.get() 把主线程卡住了怎么办?

三个办法:把 get 挪到所有线程都启动完的最后再调;用带超时的 get(3, TimeUnit.SECONDS),等不到就放弃;或者把拿结果的动作丢给另一个线程去做,主线程继续干别的。总之别让 get 堵住关键路径。

二、CountDownLatch 和 CyclicBarrier:一个关门,一个发车

CountDownLatch 和 CyclicBarrier 都有计数,但方向完全相反——一个是线程往外走、数到零关门,一个是线程往里聚、凑齐数发车。记住这一句,两者区别就不会再混。

CountDownLatch(倒计时门闩):一个初始计数,每有线程调用 countDown() 计数减一,调用 await() 的主线程一直等到计数归零才继续往下走。你可以把它想成放学锁门——等最后一个同学走出教室,阿姨才落锁。

CountDownLatch latch = new CountDownLatch(6);

for (int i = 0; i < 6; i++) {
   
    new Thread(() -> {
   
        System.out.println(Thread.currentThread().getName() + " 跑路");
        latch.countDown();          // 数量 -1
    }, "同学" + i).start();
}

latch.await();                      // 计数归零才放行
System.out.println("关门");

跑这段代码,你会看到 6 个同学依次「跑路」,然后主线程才被唤醒,最后一行才打印「关门」——注意「关门」永远是最后出现的:

CyclicBarrier(循环栅栏):N 个线程互相等,全部调用 await() 到达屏障后,才一起放行,同时执行一个可选的集合任务。你可以把它想成集齐七颗龙珠召唤神龙——凑不齐,谁也别想先走。

CyclicBarrier barrier = new CyclicBarrier(7, () -> {
   
    System.out.println("神龙召唤!");
});

for (int i = 1; i <= 7; i++) {
   
    final int temp = i;
    new Thread(() -> {
   
        System.out.println("收集到第 " + temp + " 颗龙珠");
        try {
   
            barrier.await();        // 等齐 7 个线程
        } catch (Exception e) {
   
            e.printStackTrace();
        }
    }).start();
}

两者都是「等够数就放行」,区别就在谁等、放行之后干什么。这张对比图一眼看清:

CountDownLatch 与 CyclicBarrier 对比:一个倒计时关门,一个集齐发车

一句话记住:CountDownLatch 是减法,数完任务执行完,主线程最后收尾;CyclicBarrier 是加法(集合),人齐了大家一起开工。

三、Semaphore:停车场里练限流

Semaphore 是 JUC 里最像限流器的工具——许可数量固定,抢到就干,抢不到就排队,release 一个让一个进来。做并发限流、控制最大线程数时它都是首选。

Semaphore(信号量):维护 N 个许可证,acquire() 拿一个、release() 还一个,没许可证就阻塞等待。你可以把它想成停车场——一共就 2 个车位,车满了,后来的只能停在外面等位。

Semaphore semaphore = new Semaphore(2);   // 2 个车位

for (int i = 0; i < 6; i++) {
   
    new Thread(() -> {
   
        try {
   
            semaphore.acquire();          // 抢到车位
            System.out.println(Thread.currentThread().getName() + " 进停车场");
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
   
            e.printStackTrace();
        } finally {
   
            semaphore.release();          // 释放车位
        }
    }, "车" + i).start();
}

acquire() 拿走一个许可,release() 还回来并唤醒一个等待线程。用在哪?数据库连接池、接口限流、控制并发数,都是它的主场——多个共享资源互斥使用,就是信号量最典型的作用。

Semaphore 用固定许可数限流,2 个车位放行 2 辆车。

四、读写锁:读多写少的「分工授权」

普通的锁一视同仁,读和写互相挤;读写锁把「读」和「写」分开授权——读可以一起读,写必须独占。读写锁就是读多写少场景的最优解。

ReentrantReadWriteLock(读写锁):内部维护一把读锁(共享锁)和一把写锁(独占锁)。读锁可被多个线程同时持有,写锁同一时刻只能一个线程持有。你可以把它想成图书馆——读者可以好多人一起看书,但有人要改书架,其他人就得先让开。

三种组合的共存关系,一张表记住:

线程组合 能否共存 原因
读-读 ✅ 共存 共享锁,谁读都不影响谁
读-写 ❌ 互斥 写着读着会读到半成品
写-写 ❌ 互斥 独占锁,同时写必然打架

一句话:读读共存、读写互斥、写写互斥。用它包一层缓存,比给整个 HashMap 上普通锁高效得多:

class MyCacheLock {
   
    private final Map<String, Object> map = new HashMap<>();
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public void put(String key, Object value) {
   
        lock.writeLock().lock();
        try {
    map.put(key, value); }
        finally {
    lock.writeLock().unlock(); }
    }

    public Object get(String key) {
   
        lock.readLock().lock();
        try {
    return map.get(key); }
        finally {
    lock.readLock().unlock(); }
    }
}

不加锁的 HashMap 缓存,写的同时另一个线程在读,轻则读到脏数据,重则在并发 put 时 key 值丢失甚至死循环。读写锁就是专门补这个口子的。

五、阻塞队列:四组 API 一张表记牢

阻塞队列四组 API 是最容易被面试问倒的细节——add、offer、put、offer(超时) 四种添加方式,区别全在「满了怎么办」,一张表就能记牢。

BlockingQueue(阻塞队列):线程安全且自带阻塞能力的队列,放不下时 put 会阻塞,取不到时 take 会阻塞。你可以把它想成食堂打饭窗口——窗口空了厨师多做几个等着,窗口满了顾客就在队伍里排队等。

阻塞队列用在哪?最常见的就是多线程协作、线程池里塞任务的地方——生产消费两边节奏对不上,全靠它兜住。它解决的就是生产者与消费者之间的「节奏不同步」。

四组 API 对照表(背这一张就够):

方式 满了/空了直接报错 满了/空了返回特殊值 满了/空了阻塞等待 满了/空了超时放弃
添加 add offer put offer(值, 超时, 单位)
移除 remove poll take poll(超时, 单位)
探测队首 element peek

add 满了抛 IllegalStateException,offer 满了返回 false,put 满了一直堵着,offer(超时) 堵几秒就放弃。以 ArrayBlockingQueue 为例,队列容量 3:

ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(3);

queue.put("a");
queue.put("b");
queue.put("c");
// queue.put("d");          // 满了,这里会一直阻塞

System.out.println(queue.take());   // 取走一个,腾出位置
System.out.println(queue.take());
System.out.println(queue.take());

这套四组 API 我当年背了 N 遍还是混,直到自己画了上面这张表才算彻底记住。

生产者、队列缓冲、消费者,各干各的,节奏不同步也不怕。

六、SynchronousQueue:一个不存元素的队列

SynchronousQueue 是全 JUC 里最特别的队列——它没有容量,put 一个元素必须等别人 take 走,否则永远堵着,它实现的是线程间的「手递手」。

SynchronousQueue(同步队列):零容量的阻塞队列,生产者 put 一个元素必须等消费者 take 走,消费者 take 也必须等生产者 put,两边一对一交接。你可以把它想成高空抛接——一个人扔,另一个人必须正好接住,中间没有筐。

BlockingQueue<String> queue = new SynchronousQueue<>();

new Thread(() -> {
                          // 生产者 T1
    try {
   
        queue.put("1");
        System.out.println("T1 放 1,没人接,堵住了");
    } catch (InterruptedException e) {
   
        e.printStackTrace();
    }
}).start();

new Thread(() -> {
                          // 消费者 T2
    try {
   
        TimeUnit.SECONDS.sleep(3);       // 3 秒后才来接
        System.out.println("T2 取到:" + queue.take());
    } catch (InterruptedException e) {
   
        e.printStackTrace();
    }
}).start();

跑起来你会看到:T1 放 1 之后因为没人 take 而一直堵着,直到 3 秒后 T2 把它取走,两边才继续。别的队列靠「存」缓冲,它靠「等」直接交接。

零容量的队列,put 和 take 必须同时在线,中间没有缓冲。

可能有人会问:SynchronousQueue 又不能存元素,到底有什么用?

它的价值恰恰在「不存」——天然实现一对一手递手,没有中间堆积,生产消费严格同步。线程池的 Executors.newCachedThreadPool 内部就用它做任务队列,来一个任务就得有线程来接,没有人接就新建线程,所以它的线程数才会跟着任务数一起涨。

七、写在最后

做 Java 并发编程,与其背类名,不如记场景——CountDownLatch 管「凑齐就放行」,CyclicBarrier 管「凑齐才发车」,Semaphore 管「限流放行」,读写锁管「分工授权」,阻塞队列管「缓冲排队」,SynchronousQueue 管「一对一交接」。想清楚每个类让线程「等什么、怎么放行」,名字自然就记住了。

这几个辅助类串起来,正好是线程池和生产者-消费者模型落地前的完整拼图,下一篇可以展开线程池的七大参数怎么配。说白了就一句话:记场景,不背类名。


我是晚安code,持续分享编程干货。觉得有用的话记得点赞收藏和关注~也欢迎在评论区聊聊:你学 Java 并发编程时最容易记混的是哪两个类?有没有被 futureTask.get() 的阻塞坑过?

目录
相关文章
|
2月前
|
人工智能 自然语言处理 前端开发
百炼 Skills 实战:novel-game——让零基础用户把故事变成可玩的互动小说游戏
novel-game 是百炼官方推出的互动小说创作 Skill,无需编程即可一键生成完整视觉小说。融合 Qwen、Wan、HappyHorse、CosyVoice 多模态 AI,自动产出剧情、立绘、动画、配音及程序化音效,输出可离线运行的 React 游戏,支持分支叙事与多端适配。(239字)
|
3月前
|
前端开发 Java Nacos
Nacos 注解全解析:7 个核心注解 + 5 个生产踩坑清单(2026 实测)
Nacos 注解用错,是 90% 微服务 Bug 的根源。本文把注册发现、配置热更新的 7 个核心注解、动态刷新原理和 5 个生产踩坑一次讲透,附可运行代码。
464 2
Nacos 注解全解析:7 个核心注解 + 5 个生产踩坑清单(2026 实测)
|
2月前
|
算法 Java Nacos
Sentinel 流量治理实战:熔断降级限流 + Nacos 持久化
Sentinel 流量治理如何落地?本文从服务雪崩讲起,带你掌握熔断、降级、限流策略,配合 Nacos 实现规则持久化
350 0
|
2月前
|
人工智能
Qwen3.8抢先体验!正式版即将发布并开源!
千问Qwen3.8即将开源,参数达2.4T,进化速度以“天”计,实力媲美Fable 5。预览版Qwen3.8-Max已上线阿里Token Plan等平台,限时优惠:日间Credits低至1折,夜间更优,个人/团队版月付仅35元起!
3726 141
|
2月前
|
SQL 缓存 运维
RBAC 权限模型实战:从 ACL 踩坑到 RBAC3 落地(2026)
写过 ACL 才懂 RBAC 的好。本文把 RBAC 权限模型、RBAC0~3、角色继承、互斥约束、用户组、四类权限粒度一次讲透,并附可落地的表设计
477 0
|
23天前
|
缓存 安全 Java
Java并发集合详解:从HashMap到ConcurrentHashMap
Java并发集合必须用JUC才安全,并发写会丢数据。本文拆解HashMap加载因子与ConcurrentHashMap原理,讲透线程安全集合选型。
99 4
Java并发集合详解:从HashMap到ConcurrentHashMap
|
26天前
|
搜索推荐 Java API
JUC并发编程入门:从线程状态到Lock锁,一文吃透生产者消费者
JUC并发编程是Java面试常考点。本文用学习笔记视角,理清线程状态、Lock与Synchronized的区别,手写生产者消费者并解决虚假唤醒。
105 2
JUC并发编程入门:从线程状态到Lock锁,一文吃透生产者消费者
|
27天前
|
JSON 自然语言处理 Java
Elasticsearch查询实战:DSL与JavaRestClient一学就会
Elasticsearch查询离不开DSL语法和JavaRestClient:match、term、bool、分页、聚合一次讲透,看完就能写代码。
114 1
Elasticsearch查询实战:DSL与JavaRestClient一学就会
|
14天前
|
缓存 人工智能 5G
DeepSeek 涨价后怎么办:峰谷错峰省一半,小米多模态模型可平替
DeepSeek 涨价 8 月 17 日生效,最高涨 1100%。看懂峰谷计价、错峰调用能省一半;预算还紧,小米 MiMo 多模态模型可以平替。
219 0
DeepSeek 涨价后怎么办:峰谷错峰省一半,小米多模态模型可平替