引言
在 Java 多线程编程中,Java.util.concurrent 包(简称 JUC)提供了一系列线程安全、高性能的工具类,解决了传统多线程开发中线程同步、任务调度、并发容器等核心问题。
本文将围绕 JUC 中常用的核心组件(Callable 接口、ReentrantLock、信号量、CountDownLatch 及并发集合)展开解析,并结合代码示例说明其用法与设计思想。
一、先搞懂:JUC到底是什么?
JUC其实就是Java提供的一个“多线程工具包”,里面封装了各种现成的工具类,不用我们自己手动写复杂的同步逻辑,就能轻松实现线程安全、任务调度、并发容器等需求,核心作用就是“简化多线程开发,提升并发性能”。
二、Callable接口:带返回值的“任务”
我们之前学过Runnable接口,它是用来定义线程任务的,但有个缺点——run方法没有返回值,也不能抛出异常。
而Callable接口和Runnable是并列的,它解决了这个问题,能定义“带返回值”的线程任务。
核心区别很简单:
1. Callable:实现call()方法,有返回值(返回值是泛型,可指定类型),能抛出异常;
2. Runnable:实现run()方法,无返回值,不能抛出异常。
这里有个注意点:Thread的构造方法里,不能直接传入Callable对象,必须借助一个“中间件”——FutureTask。
FutureTask的作用就是“接收Callable的返回值”,相当于一个“任务容器”,把Callable包装后,再传给Thread执行。
具体代码示例如下:
public static void main(String[] args) throws ExecutionException, InterruptedException { // 1. 创建Callable任务:定义一个计算1到100和的任务,返回值是Integer Callable<Integer> callable = new Callable<Integer>() { @Override public Integer call() throws Exception { int result = 0; for (int i = 0; i <= 100; i++) { result += i; } return result; // 任务的返回值 } }; // 2. 用FutureTask包装Callable,目的是接收返回值 FutureTask<Integer> task = new FutureTask<>(callable); // 3. 把FutureTask传给Thread,启动线程 Thread t = new Thread(task); t.start(); // 4. 用get()方法获取返回值(重点) // 注意:get()会阻塞——如果线程没执行完,就一直等,直到拿到结果 System.out.println("1到100的和:" + task.get()); }为什么Thread不能直接接收Callable?核心是“解耦合”。
Thread的作用只是“启动线程”,它不需要关心任务有没有返回值、会不会抛异常,把这些细节交给FutureTask处理,让Thread更专注于自己的核心职责。
三、ReentrantLock:更灵活的“可重入锁”
ReentrantLock的核心是“可重入”(Reentrant就是可重入的意思),它和synchronized是并列的锁工具,都能解决线程安全问题,但ReentrantLock的用法更灵活,功能也更强大。
先看最基础的用法(解决线程安全问题,代码示例):
private static int count = 0; public static void main(String[] args) { // 1. 创建ReentrantLock实例 ReentrantLock locker = new ReentrantLock(); // 2. 线程1:对count自增100次 Thread t1 = new Thread(() -> { for (int i = 0; i < 100; i++) { locker.lock(); // 加锁 count++; locker.unlock(); // 解锁 } }); // 3. 线程2:对count自增100次 Thread t2 = new Thread(() -> { for (int i = 0; i < 100; i++) { locker.lock(); // 加锁 count++; locker.unlock(); // 解锁 } }); t1.start(); t2.start(); // 等待两个线程执行完毕 try { t1.join(); t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("count= " + count); // 最终输出200,线程安全 }和synchronized相比,ReentrantLock最明显的区别是:加锁、解锁需要手动调用lock()和unlock()方法。
但这里有个关键细节——如果在unlock()之前,程序抛出异常或者提前return,这就会导致没进行到解锁操作。
解决办法很简单:把unlock()放在finally代码块里,这样无论程序是否抛出异常,都会执行解锁操作,示例如下:
locker.lock(); try { count++; // 可能抛出异常的操作 } finally { locker.unlock(); // 确保解锁,避免死锁 }ReentrantLock 与 synchronized 的核心区别
- 1.synchronized是一个关键字(它的内部实现是在JVM内部通过C++代码来完成的)而ReentrantLock是Java标准库中的类(由Java代码来实现)
- 2.synchronized是通过代码块来实现加锁与解锁操作,而ReentrantLock需要使用lock/unlock方法,同时也需要注意unlock不被调用的问题。
- 3.ReentrantLock还提供了一个trylock()方法,这个方法有一个特点就是它不会阻塞,当加锁成功就返回true,加锁失败就返回false,由调用者决定接下来怎么处理。
- 同时这个方法也可以传入一个时间参数,这就设置了超时的时间,这就可以设置当等待时间超时之后再返回true/false
4.ReentrantLock提供了公平锁的实现: ReentrantLock locker=new ReentrantLock(true);- 当我们传入true的时候,这把锁就是一把公平锁;相反,当传入false时,这把锁就是一把非公平锁
- 5.ReentrantLock 配套的 Condition 类实现了等待/通知机制,其功能比 wait/notify 更为强大。
四、Semaphore:资源访问控制工具
Semaphore(信号量)用于控制同时访问特定资源的线程数量,本质是一个计数器,通过 “申请资源(P 操作)” 和 “释放资源(V 操作)” 实现资源的协调分配。
核心逻辑
- 申请资源:调用 acquire (),计数器减 1;若计数器为 0,线程阻塞等待;
- 释放资源:调用 release (),计数器加 1;唤醒阻塞的线程(若有)。
下面我们用代码来演示一下:
Semaphore semaphore=new Semaphore(4); semaphore.acquire(); System.out.println("进行一次P操作"); semaphore.acquire(); System.out.println("进行一次P操作"); semaphore.acquire(); System.out.println("进行一次P操作"); semaphore.acquire(); System.out.println("进行一次P操作");Semaphore semaphore=new Semaphore(3); semaphore.acquire(); System.out.println("进行一次P操作"); semaphore.acquire(); System.out.println("进行一次P操作"); semaphore.acquire(); System.out.println("进行一次P操作"); semaphore.acquire(); System.out.println("进行一次P操作");二元信号量(模拟锁)
初始值为 1 的 Semaphore 可模拟锁的效果,仅允许一个线程访问资源,解决线程安全问题:
private static int count=0; public static void main(String[] args) { Semaphore semaphore=new Semaphore(1); Thread t1=new Thread(()->{ for (int i = 0; i <100 ; i++) { try { semaphore.acquire(); count++; semaphore.release(); } catch (InterruptedException e){ e.printStackTrace(); } } }); Thread t2=new Thread(()->{ for (int i = 0; i <100 ; i++) { try { semaphore.acquire(); count++; semaphore.release(); } catch (InterruptedException e){ e.printStackTrace(); } } }); t1.start(); t2.start(); try { t1.join(); t2.join(); } catch (InterruptedException e){ e.printStackTrace(); } System.out.println("count= "+ count); }五、CountDownLatch:多任务等待工具
我们使用多线程的时候经常把一个大的任务拆解成多个子任务,然后我们使用多线程执行这些子任务的时候就可以提高程序执行的效率。
那么我们如何来衡量多个子任务都完成了呢?也就是整个任务都完成了呢?
这时 CountDownLatch就应运而生了。
CountDownLatch latch=new CountDownLatch(10);我们可以像上面这样创建一个CountDownLatch类的实例的时候,传入一个参数,这表示子任务的个数。
当我们执行完一个任务的时候,就调用一次countDown()方法。
当调用该方法的次数,和传入的任务个数相同的时候,整个任务就结束了。
我们在主线程应当调用await()方法,这个方法是等待所有的子任务都结束。
下面我们就用代码来演示一下:
public static void main(String[] args) throws InterruptedException { // 我们现在可以把一个大任务拆成10个子任务 // 接下来把这些任务放到线程池中,让线程池来执行 // 构造方法中传入的参数表示任务的个数 CountDownLatch latch=new CountDownLatch(10); ExecutorService executorService= Executors.newFixedThreadPool(4); for (int i = 0; i <10 ; i++) { int id=i; executorService.submit(()->{ System.out.println("子任务开始执行:"+ id); System.out.println("子任务结束执行:"+ id); latch.countDown(); }); } //下面这个方法是在等待所有的任务都结束 latch.await(); System.out.println("所有任务都执行完毕"); }六、JUC 并发集合:线程安全的容器实现
在多线程环境下,Java 原生集合类(如 ArrayList、HashMap)并非线程安全,直接使用可能导致数据异常,需要额外处理以保证并发安全。我们以 ArrayList 为例,先梳理常见的三种线程安全处理方式,再重点解析 JUC 提供的并发容器。
(一)ArrayList 的三种线程安全处理方式
- 自行加锁:手动分析代码中的原子操作,将需要保证线程安全的逻辑包裹在同步块或锁中,通过显式控制锁的范围来实现原子性。
Collections.synchronizedList(new ArrayList<>())调用该方法后,返回的 List 会对所有核心方法(如add、get、remove)自动添加synchronized修饰,通过粗粒度锁保证线程安全。CopyOnWriteArrayList(JUC 提供)不依赖传统加锁逻辑,采用写时拷贝机制实现线程安全,是读多写少场景下的高性能选择。
(二)CopyOnWriteArrayList:写时拷贝的线程安全列表
核心原理:读操作直接访问原数组,无需加锁;写操作(添加 / 修改 / 删除)会先拷贝一份原数组,在新数组上完成修改后,将原数组的引用指向新数组。
执行逻辑:
- 读操作始终从 “旧数组” 读取数据,即使写操作正在进行,也不会读到 “修改一半” 的中间状态;
- 写操作完成后,引用切换,后续读操作才能感知到最新数据。
优势:读操作无锁,性能极高;读操作与写操作互不阻塞,适合读多写少的场景。
缺点:
- 写操作需要拷贝整个数组,数据量较大时性能损耗明显;
- 多线程同时写时会产生多次拷贝,可能出现数据覆盖问题;
- 无法保证数据实时一致性,写操作完成前,读操作只能看到旧数据。
(三)ConcurrentHashMap:高效的线程安全哈希表
HashMap 是线程不安全的,而Hashtable和ConcurrentHashMap是线程安全的哈希表实现。
Hashtable对所有public方法添加全局synchronized锁,导致任意两个线程访问任意数据时都会产生锁竞争,并发性能极低。而ConcurrentHashMap通过细粒度锁设计,大幅提升了并发效率。
核心设计:
- 锁粒度优化:以哈希桶(数组的每个元素)的头节点作为锁对象,仅当多个线程操作同一哈希桶时才会产生锁竞争,不同哈希桶的操作可并行执行,无需额外空间开销(Java 中任意对象均可作为锁)。
- 扩容策略:采用 “分步扩容”,将旧表数据迁移到新表的过程拆分为多次操作,每次仅迁移部分数据,避免一次性扩容耗时过长导致阻塞。
与 Hashtable 的核心区别:
总结
Java.util.concurrent(JUC)是 Java 多线程编程的核心工具集,为开发者提供了一套高效、线程安全的并发解决方案。本文从任务定义、锁机制、资源控制、任务协同到安全容器五个维度,系统解析了 JUC 的核心组件:
- Callable + FutureTask弥补了 Runnable 无返回值的缺陷,实现了带结果的异步任务;
- ReentrantLock提供了比 synchronized 更灵活的锁控制;
- Semaphore以计数器为核心,实现了多线程资源访问的限流与协调;
- CountDownLatch解决了多任务并行执行后的等待汇总问题;
- 并发集合(CopyOnWriteArrayList、ConcurrentHashMap)针对不同场景优化了线程安全策略,在保证数据一致性的同时最大化并发性能。