Lesson 22 · 并发编程
线程安全设计模式:不可变对象、Thread-safe Singleton、生产者-消费者
线程安全不只是加锁——而是设计
"面试问到线程安全,很多人第一反应就是 synchronized。但真正的高手知道——最好的线程安全,是从设计上就不需要加锁。"
加锁(synchronized、ReentrantLock)只是解决线程安全的"最后手段"。锁意味着竞争,竞争意味着上下文切换和性能损耗,用不好还会带来死锁。Doug Lea 说过:"线程安全的最高境界不是把锁用好,而是让共享可变状态尽量少出现。"
本文将覆盖 5 种核心模式:不可变对象、单例 5 种写法、生产者-消费者、线程安全集合选型、编码军规。
不是考你会不会写 synchronized,而是考你有没有设计并发程序的思维:能否从架构层面减少共享状态?能否选择合适的工具而不是万物皆锁?
不可变对象:天生线程安全的设计
如果对象创建后状态永远不能改变,那么无论多少线程同时访问它都不会有线程安全问题——没有"写"操作,就不存在数据竞争。JDK 中 String、Integer、LocalDate 都是不可变类。
public final class OrderSnapshot { // 规则 1:final 类,禁止继承 private final String orderId; // 规则 2:所有字段 private final private final BigDecimal totalAmount; private final LocalDateTime createTime; private final List<String> items; public OrderSnapshot(String id, BigDecimal amt, LocalDateTime time, List<String> items) { this.orderId = id; this.totalAmount = amt; this.createTime = time; this.items = List.copyOf(items); // 规则 3:防御性拷贝 } // 规则 4:只提供 getter,不提供 setter public String getOrderId() { return orderId; } public BigDecimal getTotalAmount() { return totalAmount; } public List<String> getItems() { return items; } // List.copyOf 返回不可变 List }
线程安全的本质是防止多线程同时修改共享状态。如果状态根本不能修改,就不存在"同时修改"的问题。不可变对象可被任意多线程同时读取,无需任何同步——这是零开销的线程安全。
单例模式 5 种写法:从入门到最佳实践
写法一:饿汉式
public class EagerSingleton { private static final EagerSingleton INSTANCE = new EagerSingleton(); private EagerSingleton() {} public static EagerSingleton getInstance() { return INSTANCE; } } // 优点:简单可靠 | 缺点:可能浪费内存
写法二:懒汉式
public class LazySingleton { private static LazySingleton instance; private LazySingleton() {} public static synchronized LazySingleton getInstance() { if (instance == null) instance = new LazySingleton(); return instance; } } // 优点:延迟加载 | 缺点:每次调用都加锁,性能差
写法三:双重检查锁(DCL)
public class DclSingleton { private static volatile DclSingleton instance; // 必须 volatile! private DclSingleton() {} public static DclSingleton getInstance() { if (instance == null) { // 第一次检查(无锁) synchronized (DclSingleton.class) { if (instance == null) // 第二次检查(有锁) instance = new DclSingleton(); } } return instance; } }
new DclSingleton() 字节码层面分三步:① 分配内存 → ② 构造函数初始化 → ③ 引用指向地址。JIT 可能将 ②③ 重排为 ①③②。线程 A 执行到 ③(引用非 null)但还没 ②(未初始化),线程 B 直接返回半初始化对象——NPE。volatile 禁止此重排序。
写法四:静态内部类
public class HolderSingleton { private HolderSingleton() {} private static class Holder { private static final HolderSingleton INSTANCE = new HolderSingleton(); } public static HolderSingleton getInstance() { return Holder.INSTANCE; } } // 延迟加载 + ClassLoader 保证线程安全 + 无锁
写法五:枚举(Joshua Bloch 推荐)
public enum EnumSingleton { INSTANCE; public void doSomething() { System.out.println("Working..."); } } // EnumSingleton.INSTANCE.doSomething(); // 线程安全 + 防反射 + 防序列化 + 代码极简
"不需要延迟加载选饿汉式;需要延迟加载且 JDK 允许用枚举——天然防反射、防序列化;框架限制不能用枚举时用静态内部类。DCL 容易写错 volatile,生产环境谨慎使用。"
生产者-消费者:BlockingQueue 的经典协作
生产者负责生产数据放入缓冲区,消费者从缓冲区取出处理。缓冲区满时生产者阻塞,空时消费者阻塞。BlockingQueue 把这个同步逻辑封装好了:
public class ProducerConsumerDemo { private static final BlockingQueue<String> queue = new ArrayBlockingQueue<>(100); // 有界队列! static class Producer implements Runnable { private final String name; Producer(String name) { this.name = name; } @Override public void run() { try { for (int i = 0; i < 50; i++) { queue.put(name + "-item-" + i); // 满时阻塞 } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } static class Consumer implements Runnable { @Override public void run() { try { while (true) { String data = queue.take(); // 空时阻塞 // process(data)... } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } }
| BlockingQueue 实现 | 有界/无界 | 适用场景 |
|---|---|---|
| ArrayBlockingQueue | 有界 | 通用生产者-消费者,首选 |
| LinkedBlockingQueue | 可选(默认无界) | 高吞吐,但一定要指定容量! |
| SynchronousQueue | 容量 0 | 线程间直接交接(CachedThreadPool) |
| DelayQueue | 无界 | 定时任务、延迟重试 |
无界队列在生产者速度远大于消费者时会无限增长,最终 OOM。有界队列天然形成反压:队列满时生产者阻塞,被迫降速,系统自动达到平衡。
线程安全集合选型:别再用错容器了
| 场景 | 推荐 | 避免 | 原因 |
|---|---|---|---|
| 并发 Map | ConcurrentHashMap | synchronizedMap | CHM 读不加锁;syncMap 全表加锁,差 5-10x |
| 并发 List | CopyOnWriteArrayList | Vector | COW 读无锁(读多写少);Vector 每方法 synchronized |
| 并发 Queue | ConcurrentLinkedQueue | synchronizedList 当队列 | 无锁 CAS,高并发性能远优于包装 |
| 并发 Set | ConcurrentHashMap.newKeySet() | synchronizedSet | 底层就是 CHM |
| 阻塞队列 | ArrayBlockingQueue | 自己 wait/notify | JDK 已封装好,自写易出 Bug |
// synchronizedMap:读和读之间也互斥!100 线程读也要排队 Map<String, String> syncMap = Collections.synchronizedMap(new HashMap<>()); // ConcurrentHashMap:100 线程可以同时读,互不影响 ConcurrentMap<String, String> chm = new ConcurrentHashMap<>(); // get() → 无锁读取(volatile 保证可见性) // put() → CAS + synchronized(只锁单个桶节点) // CopyOnWriteArrayList:写时复制整个数组 List<String> cow = new CopyOnWriteArrayList<>(); cow.add("hello"); // 加锁 → 复制数组 → 修改 → 替换引用 cow.get(0); // 无锁读 volatile 数组 // 适合:读 10000 次才写 1 次(配置、监听器列表)
Map 选 CHM,List 选 COW(读多写少),Queue 选 ConcurrentLinked,要阻塞就上 BlockingQueue。忘掉 Vector、Hashtable、synchronizedXxx。
线程安全编码六大军规
规则 1:最小化共享状态 —— 能线程局部就不共享,能局部变量就不实例变量。用 ThreadLocal 但记得 remove()。
规则 2:优先不可变对象 —— 不可变 = 天生线程安全 = 零同步开销。DTO、VO、配置对象都应是 Immutable。
规则 3:用并发集合代替同步包装 —— ConcurrentHashMap > synchronizedMap,锁粒度更细、算法更优。
规则 4:避免有状态单例 —— 有状态单例 = 定时炸弹。把状态外移到 AtomicInteger 或数据库。
规则 5:永远使用线程池 —— new Thread().start() ✘。手动创建线程无法控制并发数量,高峰期 OOM。
规则 6:缩小同步范围 —— synchronized 块越短越好,绝不在 synchronized 里做 IO。
// ✘ 反面:在 synchronized 里做 IO public synchronized void processOrder(Order order) { validateOrder(order); // 需要锁 updateInventory(order); // 需要锁 sendEmail(order.getUserEmail()); // 发邮件可能 2 秒!其他线程干等 } // ✔ 正面:只锁必要部分 public void processOrder(Order order) { synchronized (this) { validateOrder(order); updateInventory(order); } sendEmail(order.getUserEmail()); // IO 移到锁外 }
SimpleDateFormat 非线程安全!定义为 static 共享,多线程 format()/parse() 会日期错乱。方案:用 Java 8 的 DateTimeFormatter(不可变,线程安全,推荐)。
总结:模式选型与常见陷阱
线程安全设计模式选型指南:
- 状态不变 → 不可变对象,零开销线程安全
- 全局唯一实例 → 枚举(最佳)或静态内部类(需延迟加载)
- 生产-消费解耦 → ArrayBlockingQueue(有界)
- 并发 Map → ConcurrentHashMap
- 读多写少 List → CopyOnWriteArrayList
| 常见陷阱 | 后果 | 正确做法 |
|---|---|---|
| DCL 忘写 volatile | 半初始化对象,NPE | instance 必须 volatile |
| static SimpleDateFormat | 日期解析错乱 | 用 DateTimeFormatter |
| 无界队列不限容量 | 生产者快于消费者时 OOM | 始终用有界队列 |
| 手动 new Thread() | 线程数失控 OOM | 使用线程池 |
| HashMap 多线程使用 | 数据丢失(JDK 8+) | 换 ConcurrentHashMap |
| synchronized 里做 IO | 吞吐量暴跌 | IO 移到锁外 |
"线程安全的核心是减少共享可变状态。首先用不可变对象,其次用并发集合(ConcurrentHashMap、BlockingQueue),配合线程池管理生命周期。单例用枚举或静态内部类。真正需要锁时,优先 ReentrantLock 或 ReadWriteLock,且锁范围尽可能小。"
下一篇预告:CompletableFuture 异步编程——用链式 API 编排复杂的异步任务流。