0%

代码整洁之道 - 并发编程

对象是过程的抽象,线程是调度的抽象

  1. 并发是一种架构设计:并发不是简单的 “用 Thread 跑个任务”。它是一种宏观的设计决策,应该在架构设计早期就考虑,而不是事后添加。它可以将“做什么”和“何时做”解耦。

  2. 并发与性能不是等价的

    • 并发结构性问题。旨在让程序在同一时间做多件事(更清晰的设计)。

    • 并行性能问题。旨在让程序在更短的时间内做一件事(提高速度)。

    • 并发可以带来并行,从而提升性能,但这不是唯一目的。并发只有在多个线程或处理器之间能分享大量等待时间的时候管用

  3. 并发编写的核心难点对共享数据的正确访问。不同执行线程对同一数据资源的竞争和协作是万恶之源。

  4. 并发防御原则

    • 单一权责原则 (SRP):并发相关的代码应该有自己独立的开发、修改和调优的生命周期。不要将并发代码与其他业务代码混在一起。

    • 限制数据作用域:共享数据是危险的。严格限制哪些数据可以被共享,并通过精细的同步机制来保护它们。

    • 使用数据副本:尽可能避免共享,使用数据的副本进行处理。

    • 线程应尽可能独立:让每个线程在自己的世界中运行,不与其他线程共享数据。例如,使用 ThreadLocal 变量。


书中提到的经典问题(为什么它难)
竞态条件 (Race Condition):操作的正确性依赖于线程执行的时序。

死锁 (Deadlock):多个线程互相等待对方持有的资源,导致所有线程都无法继续执行。(四个必要条件:互斥、占有且等待、不可抢占、循环等待)


超越《Clean Code》:现代并发最佳实践

以下是基于 Java 生态的一些方法论:

1. 设计原则与模式

  • 不可变性 (Immutability)这是最强大的并发“武器”。共享不可变对象是绝对安全的。尽可能将你的类设计为不可变的(使用 final 字段,不提供 setter)。
1
2
3
4
5
6
7
8
9
10
11
// 一个线程安全的不可变类
public final class ImmutablePerson {
private final String name;
private final int age;

public ImmutablePerson(String name, int age) {
this.name = name;
this.age = age;
}
// 只有 getters, 没有 setters
}
  • 线程封闭 (Thread Confinement):将数据访问限制在单个线程内。例如,使用局部变量(每个线程有自己的栈)、ThreadLocal
  • 使用线程安全的组件 :优先使用现有的高并发组件,而不是自己从头实现。
    • Java 并发集合 (java.util.concurrent):永远优先使用 ConcurrentHashMap 而不是自行同步的 HashMap,使用 CopyOnWriteArrayList 用于读多写少的场景。它们的性能和数据一致性远优于用 Collections.synchronizedList() 包装的集合。
    • 同步器 (Synchronizers):熟练使用 CountDownLatchCyclicBarrierSemaphoreExchanger 等高级工具来协调线程间的合作。

2. 执行任务与管理线程

  • 不要手动管理线程:不要再 new Thread(...).start() 了。
  • 使用 Executor 框架:使用 ExecutorService 来管理线程池。它帮你处理了线程的生命周期、调度和资源管理。
1
2
3
4
5
6
7
8
  // 使用线程池执行任务
ExecutorService executor = Executors.newFixedThreadPool(4);
for (int i = 0; i < 10; i++) {
executor.submit(() -> {
System.out.println("Task running on: " + Thread.currentThread().getName());
});
}
executor.shutdown(); // 优雅关闭
  • 明确任务类型:你的任务是 Runnable(无返回值)还是 Callable(有返回值)?使用 Future 来获取异步任务的结果。

3. 同步与锁

  • 同步最小的代码块:使用 synchronized 时,只锁住必要的部分,以减少线程争用。

  • 优先使用 java.util.concurrent.locks:对于更复杂的场景,ReentrantLock 比 synchronized 更灵活,支持尝试锁定、定时锁定、可中断锁定等。

  • 了解 Java 内存模型 (JMM):理解 volatile 关键字的作用(保证可见性、禁止指令重排)和局限性(不保证原子性)。它是轻量级的同步机制。

4. 测试与调试

  • 并发代码难以测试:一些错误可能在测试中运行一万次都不出现一次。

  • 将并发代码与非并发代码隔离:以便于针对并发部分进行重点测试。

  • 尝试诱发失败

    • 在多核处理器上运行测试。

    • 调整线程池大小(比处理器数量多或少),以增加上下文切换和资源争用的概率。

    • 使用 CountDownLatch 等工具“胁迫”代码以产生竞态条件。

总结:一份并发编程清单

  1. 首先考虑:我真的需要并发吗?复杂度是否值得带来的性能/结构收益?

  2. 遵守 SRP:隔离并发代码。

  3. 优先选择不可变对象

  4. 优先使用并发库 (java.util.concurrent),而不是自己造轮子。

  5. 优先使用 Executor 框架,而不是手动管理线程。

  6. 尽量缩小同步范围,减少争用。

  7. 编写测试,并尝试让测试失败,以证明你的同步是有效的。