杰瑞科技汇

Java并发编程有哪些核心设计原则与模式?

核心设计原则

在深入具体的模式之前,理解这些底层的设计原则至关重要,它们是指导我们如何思考并发问题的“指导思想”。

Java并发编程有哪些核心设计原则与模式?-图1
(图片来源网络,侵删)

不可变性原则

  • 核心思想:一个对象在被创建之后,其状态(内部数据)就不能再被修改,所有对“修改”的操作,都会返回一个全新的对象。
  • 为什么重要
    • 线程安全:由于状态不可变,天生就不存在并发修改的问题,多个线程可以同时读取一个不可变对象,而无需任何同步措施。
    • 简化设计:无需考虑锁、同步等复杂机制,极大地降低了程序的复杂性和出错的可能性。
  • 实现方式
    • 将类声明为 final,防止被继承。
    • 所有字段声明为 private final
    • 不提供任何修改字段的方法(如 set 方法)。
    • 确保在构造函数中完成所有初始化,并且不逸出 this 引用(防止在对象完全构造好之前被其他线程看到)。
  • Java 中的例子
    • String 类:所有修改操作(如 substring, replace)都返回新的 String 实例。
    • 包装类(Integer, Long 等)。
    • 枚举类型。

最小化锁的范围原则

  • 核心思想:只对必须同步的代码块加锁,并且这个代码块要尽可能小,避免对整个方法加锁,除非整个方法体的所有操作都必须是原子的。
  • 为什么重要
    • 提高并发性:锁的粒度越小,同一时间能获取锁的线程就越多,程序的并发吞吐量越高。
    • 减少锁竞争:大范围的锁会导致线程长时间等待,增加上下文切换的开销,降低性能。
  • 反面示例
    // 错误:对整个方法加锁,即使方法中只有一行代码需要同步
    public synchronized void badMethod() {
        // 很多不涉及共享状态的代码...
        sharedList.add(item); // 只有这里需要同步
        // 更多不涉及共享状态的代码...
    }
  • 正确示例
    // 正确:只对需要同步的代码块加锁
    public void goodMethod() {
        // 很多不涉及共享状态的代码...
        synchronized (lock) { // 使用一个专门的对象作为锁,而不是 this 或方法参数
            sharedList.add(item);
        }
        // 更多不涉及共享状态的代码...
    }

避免锁住可变对象原则

  • 核心思想:永远不要用一个外部可变对象作为同步锁,如果锁对象本身可以被其他代码修改,那么它的 hashCode()equals() 方法可能会改变,导致 synchronized 块失效。

  • 为什么重要:如果锁对象被修改,可能会导致同一个线程无法再次进入它之前加锁的代码块,或者更糟,导致两个不同的对象被误认为是同一个锁,从而引发死锁或数据竞争。

  • 正确做法

    • 为每个需要同步的类或代码块创建一个私有的、不可变的 final 锁对象。

      Java并发编程有哪些核心设计原则与模式?-图2
      (图片来源网络,侵删)
      public class MyClass {
      // 私有、不可变的锁对象
      private final Object lock = new Object();
      public void doSomething() {
          synchronized (lock) {
              // ...
          }
      }
      }

线程封闭原则

  • 核心思想:将数据限制在单个线程内,只有创建该数据的线程才能访问它,这样一来,就不存在多线程共享数据的问题,自然也就无需同步。
  • 为什么重要:这是实现线程安全最简单、最有效的方法之一,它将并发问题从“多线程如何协作”简化为“单线程如何处理”。
  • 实现方式
    • 方法封闭:数据作为局部变量存在,只在方法内部使用。
    • 线程局部:使用 ThreadLocal 变量,为每个线程都创建一个独立的副本。
  • Java 中的例子
    • ThreadLocal:经典的实现,例如在 SimpleDateFormat 的使用中,为了避免其非线程安全性,可以为每个线程创建一个 ThreadLocal<SimpleDateFormat> 实例。

优先使用并发工具而非 synchronized

  • 核心思想:在 Java 5 之后,java.util.concurrent 包提供了大量更高级、更强大、性能更好的并发工具,在大多数情况下,应该优先使用它们,而不是直接使用底层的 synchronized 关键字。
  • 为什么重要
    • 功能更强大:提供了更丰富的同步语义,如读写锁、信号量、倒计时门闩等。
    • 性能更好:很多工具(如 ReentrantLock)支持公平锁、非公平锁,并且能提供更好的吞吐量。
    • 使用更灵活:可以响应中断、尝试获取锁(tryLock)、设置超时等。
  • 常用工具
    • java.util.concurrent.locks 包:ReentrantLock, ReentrantReadWriteLock
    • java.util.concurrent.atomic 包:AtomicInteger, AtomicReference 等,基于 CAS 机制,性能极高。
    • java.util.concurrent 包:CountDownLatch, CyclicBarrier, Semaphore, ConcurrentHashMap, BlockingQueue 等。

核心设计模式

设计模式是解决特定问题的成熟方案,在并发编程领域,有一些经典的模式。

不可变对象模式

  • 模式描述:如前所述,创建一个一旦创建就不可修改的对象,所有“修改”操作都返回一个新对象。
  • 应用场景
    • 需要在多线程间安全地共享对象。
    • 作为 Map 的键或 Set 的元素,因为它们的哈希值不会变,非常高效。
    • 缓存、配置信息等。
  • Java 例子
    • java.lang.String
    • java.lang.Integer 及其所有包装类。
    • 所有 Java 枚举类型。

线程局部存储模式

  • 模式描述:为使用它的每个线程都维护一个独立的变量副本,每个线程只能看到和修改自己的副本,互不干扰。
  • 应用场景
    • 有状态的工具类,如 SimpleDateFormatRandom
    • 保存用户会话信息、事务上下文等。
  • Java 例子
    • java.lang.ThreadLocal 是该模式的直接实现。

主动对象模式

  • 模式描述:将方法调用和方法执行分离,当一个线程调用一个对象的方法时,它并不是立即执行该方法,而是将这个调用请求放入一个队列中,然后立即返回,该对象内部有一个专用的线程,它会从队列中取出请求并执行,这使得调用方线程和方法执行方线程可以完全解耦。
  • 应用场景
    • 耗时的 I/O 或计算操作,不希望阻塞调用方线程。
    • 需要精细控制任务执行顺序和优先级的场景。
  • Java 例子
    • java.util.concurrent.ExecutorServiceFuture 模式是该思想的一种体现,你提交一个任务(Runnable/Callable),得到一个 Future 对象,可以稍后获取结果,而提交线程不会被阻塞。
    • CompletableFuture 是更现代、更强大的实现,支持函数式组合和异步编排。

线程池模式

  • 模式描述:创建一组可重用的线程(线程池),而不是为每个任务都创建和销毁一个新线程,任务被提交到线程池,由池中的空闲线程来执行。
  • 应用场景
    • 需要处理大量短期异步任务的场景(如 Web 服务器请求处理)。
    • 避免频繁创建和销毁线程带来的性能开销。
    • 控制并发线程数量,防止系统资源耗尽。
  • Java 例子
    • java.util.concurrent.ExecutorService 是线程池的顶层接口。
    • java.util.concurrent.Executors 提供了创建各种预配置线程池的工厂方法,如 newFixedThreadPool, newCachedThreadPool, newScheduledThreadPool

Balking 模式(犹豫/观望模式)

  • 模式描述:当某个操作因为对象的状态而不适合执行时,就放弃执行,直接返回,简单说就是“我只在合适的时候干活,不合适就算了”。

  • 应用场景

    • 某个操作只需要执行一次,比如初始化。
    • 文件保存操作,当文件内容没有发生变化时,无需重复保存。
  • 结构

    Java并发编程有哪些核心设计原则与模式?-图3
    (图片来源网络,侵删)
    • 一个条件判断(if 语句)。
    • 一个 synchronized 块,确保条件判断和后续操作的原子性。
  • 示例代码

    public class SaverThread extends Thread {
        private final Data data;
        public SaverThread(Data data) {
            this.data = data;
        }
        public void run() {
            try {
                while (true) {
                    // 检查是否有变化
                    if (data.isChanged()) {
                        // 保存操作
                        data.save();
                        data.setChanged(false); // 保存后重置状态
                    }
                    Thread.sleep(1000);
                }
            } catch (InterruptedException e) {
                // ...
            }
        }
    }
    public class Data {
        private boolean changed = false;
        // ... 其他字段
        // synchronized 确保检查和修改的原子性
        public synchronized void change() {
            this.changed = true;
        }
        public synchronized boolean isChanged() {
            return this.changed;
        }
        public synchronized void save() {
            // ... 实际保存逻辑
        }
    }

Read-Write Lock 模式(读写锁模式)

  • 模式描述:一种特殊的锁,它区分读操作和写操作,允许多个读线程同时访问共享资源,但写操作是独占的(写线程会阻塞所有其他读线程和写线程)。
  • 应用场景
    • 读多写少的场景,一个配置信息被频繁读取,但很少被修改。
    • 能极大地提高并发读的性能。
  • Java 例子
    • java.util.concurrent.locks.ReentrantReadWriteLock

Guarded Suspension 模式(守护悬挂模式)

  • 模式描述:当一个线程在某个条件未满足时,它会挂起(等待),直到其他线程使该条件满足后,它才会被唤醒并继续执行。
  • 应用场景
    • 生产者-消费者模型,消费者线程在没有数据时会等待,生产者线程在数据队列满时会等待。
    • 任何需要等待某个事件发生的场景。
  • Java 例子
    • java.lang.Object.wait() / notify() / notifyAll() 是其基础实现。
    • java.util.concurrent.BlockingQueue(如 LinkedBlockingQueue)是对该模式的完美封装,使用起来非常简单。

高级模式与思想

Copy-on-Write 模式(写时复制)

  • 模式描述:当需要修改一个集合时,不直接在原集合上进行修改,而是创建该集合的一个副本,在副本上进行修改,最后用这个新副本替换掉旧引用,读操作则无需加锁,直接访问集合。
  • 应用场景
    • 读多写少的集合操作场景。
    • 遍历操作远多于修改操作的场景,因为它可以避免遍历时加锁。
  • 缺点:写操作的成本较高,因为需要复制整个集合。
  • Java 例子
    • java.util.concurrent.CopyOnWriteArrayList
    • java.util.concurrent.CopyOnWriteArraySet

Future 模式

  • 模式描述:一个对象代表一个异步计算的结果,你可以启动一个计算,然后立即去做其他事情,之后可以通过 Future 对象来获取计算的结果(如果还没算好,获取操作会阻塞)。
  • 应用场景
    • 执行一个耗时的计算,但不想阻塞主流程。
    • 并行执行多个独立任务,最后汇总结果。
  • Java 例子
    • java.util.concurrent.Future 接口。
    • java.util.concurrent.CompletableFuture 是其增强版,支持链式调用、组合和异常处理,功能非常强大。
原则/模式 核心思想 适用场景 Java 示例
不可变性原则 对象创建后状态不变 多线程共享数据、Map 键 String, Integer
最小化锁范围 只同步必要的代码块 提高并发性能 synchronized (lock) { ... }
避免锁住可变对象 使用私有、不可变的锁对象 防止锁失效 private final Object lock = new Object();
线程封闭原则 数据只在一个线程内可见 简化并发设计 ThreadLocal
优先使用并发工具 使用高级工具而非 synchronized 获得更好性能和功能 ReentrantLock, ConcurrentHashMap
线程局部存储模式 每个线程有独立数据副本 有状态工具类、会话管理 ThreadLocal
主动对象模式 调用与执行分离 异步任务、解耦 ExecutorService, CompletableFuture
线程池模式 重用线程,避免频繁创建/销毁 大量短期异步任务 ThreadPoolExecutor
Balking 模式 状态不满足则放弃执行 单次初始化、条件性保存 if (isReady()) { doWork(); }
读写锁模式 读共享,写独占 读多写少场景 ReentrantReadWriteLock
Guarded Suspension 条件不满足则等待 生产者-消费者 BlockingQueue, wait/notify
Copy-on-Write 写时复制,读不加锁 读远多于写 CopyOnWriteArrayList
Future 模式 异步获取计算结果 非阻塞调用耗时任务 Future, CompletableFuture

掌握这些原则和模式,并能在实际项目中灵活运用,是衡量一个 Java 开发者并发编程能力的重要标准。没有银弹,选择哪种方案需要根据具体的业务场景、性能要求和代码复杂度进行权衡。

分享:
扫描分享到社交APP
上一篇
下一篇