一、前言
在Java并发编程中,我们前面学习了Java内存模型、volatile、synchronized以及锁的底层原理(AQS、CAS)。这些知识让我们能够自己编写线程安全的代码。但在实际开发中,我们往往不需要"重新发明轮子"。JDK已经提供了大量现成的并发容器和并发工具类,它们在内部已经实现了必要的同步机制,我们只需直接调用即可。
本章将深入剖析ConcurrentHashMap、阻塞队列、Fork/Join框架、原子类以及CountDownLatch、CyclicBarrier等并发工具的原理与使用。掌握这些,你的并发编程能力将再上一个台阶。
二、并发容器框架:告别手写锁
并发容器框架指的是java.util.concurrent包下的一系列数据结构和工具类。它们的特点是:
内部已经实现了线程安全机制(如加锁、CAS等)。
我们多线程调用它们的方法时,无需额外编写同步代码。
性能经过高度优化,比我们自己用synchronized包装普通容器要好得多。
一句话总结:拿来即用,线程安全,高效可靠。
三、ConcurrentHashMap:线程安全的HashMap
3.1 HashMap的线程不安全问题
HashMap是Java中最常用的键值对集合,它在单线程下性能优秀,但在多线程下却会引发严重问题:
数据覆盖:多个线程同时put,后写入的值可能覆盖前一个,导致数据丢失。
死循环(JDK 1.7及以前):多线程并发扩容时,链表可能形成环形结构,导致CPU 100%并最终宕机。死循环的根本原因是扩容时对链表进行rehash,头插法在多线程下造成节点互相引用。
因此,在多线程环境中绝对不能直接使用HashMap。
3.2 Hashtable:古老但低效
Hashtable是线程安全的,它通过对整个哈希表加锁(synchronized方法)来保证安全。但这也意味着同一时刻只有一个线程能够访问哈希表,并发性能极差。
3.3 ConcurrentHashMap:分段锁与高效并发
ConcurrentHashMap在JDK 1.7中采用分段锁(Segment)机制:内部维护一个Segment数组,每个Segment相当于一个小型的HashMap,并且自己持有一把锁。不同Segment的读写可以并发执行,从而大幅提高并发度。
JDK 1.8后,ConcurrentHashMap放弃了分段锁,改用CAS + synchronized对每个桶(Node)的头节点进行细粒度锁,进一步提升了并发性能。
面试重点:HashMap、Hashtable、ConcurrentHashMap三者的区别,以及ConcurrentHashMap的底层实现。
示例代码:使用ConcurrentHashMap
import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;
public class ConcurrentHashMapDemo {
public static void main(String[] args) {
Map<String, Integer> map = new ConcurrentHashMap<>();
// 多线程环境下直接put和get,无需额外同步
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
String key = Thread.currentThread().getName() + "-" + i;
map.put(key, i);
map.get(key);
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Map size: " + map.size());
}
}
四、阻塞队列(BlockingQueue)
阻塞队列是一种支持阻塞插入和阻塞移除的队列。当队列满时,插入线程会被阻塞直到队列有空间;当队列空时,移除线程会被阻塞直到队列有元素。
4.1 常用阻塞队列实现
ArrayBlockingQueue:基于数组的有界阻塞队列,FIFO。
LinkedBlockingQueue:基于链表的可选有界阻塞队列,吞吐量通常高于ArrayBlockingQueue。
PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
SynchronousQueue:不存储元素的阻塞队列,每个插入必须等待一个移除,反之亦然。
LinkedTransferQueue:基于链表的无界阻塞队列,支持transfer操作。
LinkedBlockingDeque:双向阻塞队列。
4.2 有界队列 vs 无界队列
有界队列:设置容量上限,当队列满时触发拒绝策略或阻塞。在生产者-消费者模型中,有界队列可以防止生产者过快导致内存溢出,并且能配合线程池的最大线程数触发扩容。
无界队列:理论容量无限(受内存限制)。会导致任务无限堆积,最终OOM,且无法触发线程池的最大线程数。
实际生产中,更推荐使用有界队列(如ArrayBlockingQueue、LinkedBlockingQueue指定容量)。
示例:使用ArrayBlockingQueue实现生产者-消费者
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class BlockingQueueDemo {
public static void main(String[] args) {
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
// 生产者
Thread producer = new Thread(() -> {
try {
for (int i = 0; i < 20; i++) {
queue.put(i);
System.out.println("生产: " + i);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// 消费者
Thread consumer = new Thread(() -> {
try {
while (true) {
Integer value = queue.take();
System.out.println("消费: " + value);
Thread.sleep(100);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producer.start();
consumer.start();
}
}
五、Fork/Join框架:分治并行
Fork/Join框架是JDK 7引入的用于并行执行任务的框架,核心思想是"分而治之"。一个大任务拆分成多个小任务(fork),小任务分别执行,最后将结果合并(join)。
适用场景:递归分解型任务,如归并排序、斐波那契数列、数组求和等。
核心类:RecursiveTask(有返回值)和RecursiveAction(无返回值)。
示例:使用Fork/Join计算1到1000000的和
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;
public class ForkJoinSumTask extends RecursiveTask<Long> {
private static final int THRESHOLD = 10000;
private final int start;
private final int end;
public ForkJoinSumTask(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
if (end - start <= THRESHOLD) {
// 小任务直接计算
long sum = 0;
for (int i = start; i <= end; i++) {
sum += i;
}
return sum;
} else {
// 拆分任务
int mid = (start + end) >>> 1;
ForkJoinSumTask left = new ForkJoinSumTask(start, mid);
ForkJoinSumTask right = new ForkJoinSumTask(mid + 1, end);
left.fork(); // 异步执行左任务
right.fork(); // 异步执行右任务
return left.join() + right.join(); // 等待结果并合并
}
}
public static void main(String[] args) {
ForkJoinPool pool = new ForkJoinPool();
ForkJoinSumTask task = new ForkJoinSumTask(1, 1_000_000);
long result = pool.invoke(task);
System.out.println("结果: " + result);
}
}
注意:ForkJoinPool采用了工作窃取算法,空闲线程可以"窃取"其他任务队列中的任务,从而提高CPU利用率。
六、原子类(Atomic)
原子类位于java.util.concurrent.atomic包下,它们利用CAS(Compare-And-Swap)实现了轻量级的线程安全操作,适用于低并发场景下的计数器、状态标识等。
6.1 常用原子类
AtomicInteger / AtomicLong / AtomicBoolean
AtomicIntegerArray / AtomicLongArray / AtomicReferenceArray
AtomicReference / AtomicStampedReference(解决ABA问题)
LongAdder / LongAccumulator(高并发下性能优于AtomicLong)
6.2 为什么原子类不适合高并发?
原子类底层使用自旋CAS,在高并发(大量线程同时竞争)时,CAS失败率很高,线程会不断重试,消耗大量CPU资源。此时应使用LongAdder(内部采用分段累加,最后再汇总)或改用锁。
示例:AtomicInteger的线程安全自增
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicDemo {
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[100];
for (int i = 0; i < 100; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
count.incrementAndGet(); // 原子自增
}
});
threads[i].start();
}
for (Thread t : threads) {
t.join();
}
System.out.println("最终结果: " + count.get()); // 预期 100000
}
}
6.3 原子类与volatile的区别
volatile只能保证可见性和有序性,不能保证复合操作的原子性(如i++)。
原子类通过CAS保证了复合操作的原子性。
七、并发工具类:CountDownLatch、CyclicBarrier
7.1 CountDownLatch:等待所有子任务完成
CountDownLatch允许一个或多个线程等待其他线程执行完毕。它内部维护一个计数器,每次调用countDown()减一,await()会阻塞直到计数器为0。
典型场景:主线程等待多个子线程完成初始化后再开始工作。
示例:主线程等待三个子线程全部启动后再继续
import java.util.concurrent.CountDownLatch;
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 执行任务");
latch.countDown(); // 完成一个,计数器减一
}).start();
}
latch.await(); // 主线程等待计数器为0
System.out.println("所有子任务完成,主线程继续");
}
}
7.2 CyclicBarrier:等待所有线程到达屏障点
CyclicBarrier让一组线程互相等待,直到所有线程都到达某个公共屏障点,然后所有线程才被唤醒继续执行。与CountDownLatch不同,CyclicBarrier可以重复使用(reset())。
示例:模拟玩家加载进度,所有玩家加载完成后再开始游戏
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(4, () -> {
System.out.println("所有玩家已加载完成,游戏开始!");
});
for (int i = 0; i < 4; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 正在加载...");
try {
Thread.sleep((long) (Math.random() * 3000));
barrier.await(); // 等待其他线程
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
八、总结
ConcurrentHashMap是HashMap的线程安全替代品,分段锁和CAS保证了高并发下的性能。
阻塞队列是实现生产者消费者模式的核心组件,有界队列更可靠。
Fork/Join框架适合任务可分治的并行计算,工作窃取算法提高效率。
原子类利用CAS实现轻量级线程安全,低并发时性能优秀,高并发请用LongAdder或锁。
CountDownLatch用于等待一组任务完成,CyclicBarrier用于多线程互相等待到达屏障点。
这些并发容器和工具类极大简化了Java并发编程的复杂度。理解它们的原理和适用场景,是成为高级Java工程师的必经之路。在实际项目中,优先使用这些成熟组件,而不是重复编写低效且易错的同步代码。