Java基础教程(147)线程同步之使用Concurrent集合:告别synchronized!深度剖析Java高并发之Concurrent集合,解锁高性能线程同步新纪元
一、线程同步的古老之痛 在Java的多线程编程中,当我们提到“线程同步”,很多开发者第一时间会想到synchronized关键字或显式的Lock。这些机制
一、线程同步的古老之痛
在Java的多线程编程中,当我们提到“线程同步”,很多开发者第一时间会想到synchronized关键字或显式的Lock。这些机制通过互斥排他来保证线程安全,即同一时刻只有一个线程能访问共享资源。
然而,这种粗粒度的锁策略存在显著的性能缺陷:激烈的锁竞争会导致线程大量挂起和唤醒,造成巨大的上下文切换开销,严重拖慢系统整体性能。 尤其在读多写少的场景下,让所有读操作也互相排斥,无疑是巨大的资源浪费。
二、Concurrent集合的破局之道
为化解这一矛盾,Java 1.5+引入了java.util.concurrent包。其核心设计哲学是:并非所有操作都需要互斥。 通过更聪明的算法(如CAS操作)和更细粒度的锁策略,它实现了在绝大多数场景下,读操作完全无锁,写操作锁的范围也最小化,从而实现了近乎并行的访问性能。
下面我们深入两个最具代表性的并发容器。
1. ConcurrentHashMap:分段锁与CAS的艺术
ConcurrentHashMap(CHM)是HashMap的线程安全版本,但其实现远比简单的synchronizedMap包装器高明。
Java 7及之前:分段锁(Segment)
它将数据分成一段一段(Segment)来存储,每个段独立加锁。当线程访问不同段的数据时,可以真正并行。这是一种锁分离技术,大幅降低了锁粒度。Java 8及之后:Node + CAS + synchronized
实现更为先进:它完全放弃了分段锁,转而采用:
CAS(Compare-And-Swap):用于无锁化的节点插入、扩容等乐观操作。CAS是一种CPU原子指令,无需加锁即可实现变量更新的线程安全。synchronized同步代码块:仅在发生哈希冲突(即多个线程要操作同一个链表头节点或红黑树根节点)时,才对单个桶(Bucket)的头节点进行加锁。锁的粒度从“一段”缩小到了“一个链表”,并发度得到了指数级提升。
示例:线程安全的计数器
这是一个经典场景,对比三种实现方式的性能差异。
import java.util.concurrent.*;
public class ConcurrentHashMapDemo {
// 不安全!
// private static int count = 0;
// 安全但性能较差
// private static AtomicInteger count = new AtomicInteger(0);
// 安全且高性能 (适合复杂统计,如按单词计数)
private static ConcurrentHashMap
public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(10);
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
// count++; // 不安全
// count.incrementAndGet(); // 安全
// 使用CHM的原子方法实现安全计数
countMap.compute("key", (k, v) -> (v == null) ? 1 : v + 1);
}
};
// 提交10个任务,每个任务累加1000次
for (int i = 0; i < 10; i++) {
executor.submit(task);
}
executor.shutdown();
executor.awaitTermination(1, TimeUnit.MINUTES);
System.out.println("Final Count: " + countMap.get("key"));
// 输出:Final Count: 10000
}
}
compute(K key, BiFunction remappingFunction)方法是CHM的精华之一,它能原子性地根据旧值计算新值,完美解决了“先get后put”的非原子性操作难题。
2. CopyOnWriteArrayList:读写分离的空间换时间策略
CopyOnWriteArrayList(COW)是ArrayList的线程安全变体,其策略极其独特:“写时复制”。
核心原理:
读操作:完全无锁,所有读线程共享同一个不变的数组引用,性能极高。写操作(add, set, remove):首先复制一份当前内部数组的副本,然后在副本上进行修改。修改完成后,再将集合的引用指向这个新的数组。
优缺点:
优点:极致的读性能,且读操作永远不会与写操作冲突。缺点:
内存开销:每次写操作都会复制整个底层数组,内存占用大。数据一致性:读操作可能读到旧数据,弱一致性。不适合实时性要求极高的场景。
示例:高效的监听器列表管理
在事件驱动模型中,监听器的注册、注销(写)远没有事件触发后遍历通知监听器(读)频繁。COW是该场景的绝配。
import java.util.concurrent.CopyOnWriteArrayList;
public class ListenerManager {
// 使用CopyOnWriteArrayList管理监听器列表
private final CopyOnWriteArrayList
// 添加监听器(写操作,加锁复制)
public void addListener(EventListener listener) {
listeners.add(listener);
}
// 移除监听器(写操作,加锁复制)
public void removeListener(EventListener listener) {
listeners.remove(listener);
}
// 触发事件(读操作,无锁、高性能!)
public void fireEvent(Event event) {
for (EventListener listener : listeners) {
// 遍历期间,即使有线程调用add/remove,也不会抛出ConcurrentModificationException
listener.onEvent(event);
}
}
}
interface EventListener {
void onEvent(Event event);
}
class Event {}
在这个例子中,fireEvent方法可以高并发地被调用,而无需担心遍历过程中列表被修改。COW的迭代器迭代的是创建它那一刻的数组快照。
三、总结与选型建议
ConcurrentHashMap和CopyOnWriteArrayList代表了现代并发编程的两种高级思路:细粒度锁/无锁竞争和读写分离。
ConcurrentHashMap:万能首选。适用于绝大部分需要线程安全Map的场景,尤其是读多写少、需要高性能并发访问的场合。CopyOnWriteArrayList:特化武器。仅适用于读操作极其频繁,写操作非常稀少,且能容忍短暂数据不一致的特殊场景(如监听器列表、只读视图的缓存)。
总而言之,从笨重的synchronized到精巧的Concurrent集合,是Java开发者从“保证正确性”迈向“同时追求极致性能”的关键一步。熟练掌握这些工具,是你构建高性能、高响应度Java应用的必备技能。