并发:如何设计线程安全的类

《Java并发编程实战》第四章"对象的组合"深入探讨了如何通过合理设计类的结构、组合线程安全组件及应用设计模式,来构建线程安全的复杂对象。这一章的核心是将"线程安全"的责任分解到类的各个组成部分,而非在每个方法中单独处理同步,从而降低并发编程的复杂度。

一、设计线程安全类的基础:状态管理

线程安全的本质是对"共享可变状态"的安全访问控制。设计线程安全类的第一步是明确"状态"的范围和特性:

  1. 状态的定义

    一个对象的状态是其所有实例变量(字段)的值的总和,包括:

    • 自身声明的字段(如int count);
    • 引用对象的字段(如List<String> list中的元素,属于该对象状态的一部分)。
      若对象的状态不被多个线程共享,或状态是不可变的,则天然线程安全。
  2. 状态的可变性与共享性

    • 不可变状态 :用final修饰且构造后无法修改的状态(如StringBigInteger),无需同步即可安全共享。
    • 可变状态 :若被多个线程共享,必须通过同步机制(锁、volatile、原子类等)保证访问的原子性、可见性和有序性。
  3. 状态的所有权

    • 若一个类"拥有"其状态(即状态是私有且未发布的),则该类可完全控制状态的访问方式;
    • 若状态被发布(如通过getter返回引用),则需确保外部访问不会破坏线程安全(如返回不可修改的视图)。

二、核心设计模式:实例封闭(Instance Confinement)

实例封闭是通过封装将状态"限制"在类内部,仅允许通过类的方法访问,从而简化线程安全的实现。其核心是"私有性+锁的独占性"。

1. 封闭的实现方式
  • 私有字段 :用private修饰所有状态变量,禁止外部直接访问。
  • 内部锁 :类内部通过同一把锁(通常是this或私有锁对象)协调所有对状态的访问。
  • 禁止发布:不向外部暴露状态的引用(或仅暴露不可修改的视图)。
2. 示例:封闭HashSet实现线程安全集合
java 复制代码
public class SafeList {
    // 状态被封闭在类内部(私有)
    private final Set<String> set = new HashSet<>();
    
    // 所有访问都通过同步方法,使用this作为锁
    public synchronized void add(String s) {
        set.add(s);
    }
    
    public synchronized boolean contains(String s) {
        return set.contains(s);
    }
}
  • HashSet本身非线程安全,但被封闭在SafeList内部后,所有访问都通过synchronized方法(同一把锁),因此SafeList是线程安全的。
  • 即使HashSet的实现有变化,只要SafeList的同步机制不变,线程安全性就不受影响(封装的优势)。
3. 私有锁对象模式

为避免使用this作为锁(可能被外部代码滥用),可使用私有锁对象:

java 复制代码
public class PrivateLockList {
    private final Set<String> set = new HashSet<>();
    private final Object lock = new Object(); // 私有锁对象
    
    public void add(String s) {
        synchronized (lock) { // 使用私有锁
            set.add(s);
        }
    }
    
    public boolean contains(String s) {
        synchronized (lock) {
            return set.contains(s);
        }
    }
}
  • 私有锁避免了外部线程通过获取this锁来干扰类内部的同步,增强了封装性。

三、线程安全委托(Thread Safety Delegation)

若一个类的状态由多个线程安全的组件 (如ConcurrentHashMapAtomicInteger)构成,且组件间的状态无依赖关系,则该类的线程安全性可"委托"给这些组件,无需额外同步。

1. 委托的条件
  • 组件本身是线程安全的(如ConcurrentHashMapputget方法是线程安全的);
  • 组件的状态彼此独立(即操作一个组件时,无需考虑其他组件的状态)。
2. 示例:委托给ConcurrentHashMap的车辆追踪器
java 复制代码
public class DelegatingVehicleTracker {
    // 线程安全组件:ConcurrentHashMap
    private final ConcurrentMap<String, Point> locations;
    // 不可修改视图,防止外部修改内部状态
    private final Map<String, Point> unmodifiableMap;
    
    public DelegatingVehicleTracker(Map<String, Point> points) {
        // 初始化线程安全组件
        locations = new ConcurrentHashMap<>(points);
        unmodifiableMap = Collections.unmodifiableMap(locations);
    }
    
    // 获取所有位置:委托给ConcurrentHashMap的线程安全方法
    public Map<String, Point> getLocations() {
        return unmodifiableMap; // 返回不可修改视图,避免外部修改
    }
    
    // 获取单个位置:直接委托
    public Point getLocation(String id) {
        return locations.get(id);
    }
    
    // 更新位置:委托给ConcurrentHashMap的replace方法(原子操作)
    public void setLocation(String id, int x, int y) {
        if (locations.replace(id, new Point(x, y)) == null) {
            throw new IllegalArgumentException("无效ID");
        }
    }
}
  • locations是线程安全的ConcurrentHashMap,所有操作(getreplace)均委托给它,因此DelegatingVehicleTracker是线程安全的。
  • 返回unmodifiableMap而非locations本身,防止外部通过Map的修改方法(如put)破坏内部状态。
3. 当委托失效:状态依赖的情况

若组件间的状态存在依赖关系(如"检查再运行"操作需要跨组件校验),单纯的委托无法保证线程安全,必须额外加锁

示例:转账操作(跨账户状态依赖)

java 复制代码
// 线程安全的银行账户类(状态独立)
class BankAccount {
    private final AtomicInteger balance = new AtomicInteger(0);
    
    public void deposit(int amount) { balance.addAndGet(amount); }
    public boolean withdraw(int amount) {
        while (true) {
            int cur = balance.get();
            if (cur < amount) return false;
            if (balance.compareAndSet(cur, cur - amount)) return true;
        }
    }
    public int getBalance() { return balance.get(); }
}

// 转账操作需要跨账户协调,单纯委托失效
class UnsafeBankTransfer {
    // 问题:检查余额和转账不是原子操作,可能被其他线程打断
    public void transfer(BankAccount from, BankAccount to, int amount) {
        if (from.getBalance() >= amount) { // 检查(可能失效)
            from.withdraw(amount);         // 扣钱
            to.deposit(amount);            // 加钱
        }
    }
}

// 正确实现:用锁协调跨组件操作
class SafeBankTransfer {
    private final Object lock = new Object(); // 全局锁
    
    public void transfer(BankAccount from, BankAccount to, int amount) {
        synchronized (lock) { // 同一把锁保证检查+转账的原子性
            if (from.getBalance() >= amount) {
                from.withdraw(amount);
                to.deposit(amount);
            }
        }
    }
}
  • BankAccount是线程安全的(委托给AtomicInteger),但transfer操作依赖两个账户的状态,必须加锁保证原子性,否则可能出现"余额检查通过后,扣钱前被其他线程取走资金"的问题。

四、发布与逸出:确保对象安全发布

发布 指将对象暴露给当前作用域之外的线程(如存储到静态变量、传递给其他类的方法)。逸出指对象在未完全初始化时被发布,导致其他线程访问到不完整状态。

1. 常见的逸出场景
  • 构造函数中发布this

    java 复制代码
    public class EscapeInConstructor {
        public EscapeInConstructor() {
            // 构造未完成时,将this发布到外部
            EventBus.register(this); 
        }
    }

    其他线程可能在EscapeInConstructor实例构造完成前就收到事件并调用其方法,导致访问到未初始化的字段。

  • 发布内部类实例

    内部类持有外部类的this引用,若在外部类构造时发布内部类实例,会间接导致外部类this逸出。

2. 安全发布的方式

确保对象在完全初始化后再发布,且发布过程对其他线程可见:

  • 通过静态初始化器发布 :JVM保证静态初始化的对象在被任何线程访问前完成初始化。

    java 复制代码
    public class StaticInitExample {
        public static final ImmutableObject INSTANCE = new ImmutableObject();
    }
  • 用volatile或AtomicReference发布volatile保证引用的可见性和初始化的安全性。

    java 复制代码
    private static volatile ImmutableObject instance;
  • 用同步方法/块发布 :如通过synchronized的工厂方法发布。

    java 复制代码
    private static ImmutableObject instance;
    public synchronized static ImmutableObject getInstance() {
        if (instance == null) {
            instance = new ImmutableObject();
        }
        return instance;
    }
  • 使用final字段final修饰的字段在构造函数完成后会被安全发布,其他线程能看到其正确值。

五、线程安全的容器与装饰器

第四章还讨论了如何利用现有容器类构建线程安全的对象,核心是"委托给线程安全的容器"或"用装饰器添加同步"。

  1. 同步容器(Synchronized Containers)

    VectorHashtable,或通过Collections.synchronizedXxx()包装的容器(如synchronizedList)。

    • 原理:对容器的每个方法加锁(使用容器自身作为锁),保证单个方法的线程安全。

    • 局限性:复合操作(如"迭代""检查再添加")仍需外部加锁,否则可能出现竞态条件。

      java 复制代码
      List<String> list = Collections.synchronizedList(new ArrayList<>());
      // 复合操作需手动加锁
      synchronized (list) {
          for (String s : list) { ... } // 迭代需锁保护
      }
  2. 并发容器(Concurrent Containers)

    ConcurrentHashMapCopyOnWriteArrayList,专为高并发设计:

    • 采用细粒度锁或无锁算法(如ConcurrentHashMap的分段锁),支持更高的并发度。
    • 部分操作(如putIfAbsent)本身是原子的,无需外部同步。
    • 迭代时不抛出ConcurrentModificationException(弱一致性迭代器)。

六、总结:构建线程安全类的实践原则

  1. 明确状态范围:识别所有构成对象状态的变量(包括引用对象的字段)。
  2. 优先封装与封闭:通过实例封闭将状态限制在类内部,用内部锁协调访问。
  3. 善用线程安全组件 :尽量委托给线程安全的组件(如ConcurrentHashMapAtomicInteger),减少手动同步。
  4. 处理状态依赖:当组件状态存在依赖时,需额外加锁保证复合操作的原子性。
  5. 安全发布对象 :避免对象逸出,确保在完全初始化后再发布,使用finalvolatile或同步机制保证可见性。

通过这些原则,可将复杂对象的线程安全问题分解为更小的、可管理的部分,从而降低并发编程的难度,提高代码的可靠性。

相关推荐
码熔burning2 小时前
从 new 到 GC:一个Java对象的内存分配之旅
java·开发语言·jvm
考虑考虑3 小时前
图片翻转
java·后端·java ee
十六点五3 小时前
Java NIO的底层原理
java·开发语言·python
猿究院-赵晨鹤3 小时前
Java I/O 模型:BIO、NIO 和 AIO
java·开发语言
叽哥3 小时前
Kotlin学习第 5 课:Kotlin 面向对象编程:类、对象与继承
android·java·kotlin
叽哥3 小时前
Kotlin学习第 6 课:Kotlin 集合框架:操作数据的核心工具
android·java·kotlin
心月狐的流火号3 小时前
Spring Bean 生命周期详解——简单、清晰、全面、实用
java·spring
little_xianzhong3 小时前
步骤流程中日志记录方案(类aop)
java·开发语言
半桔3 小时前
【STL源码剖析】二叉世界的平衡:从BST 到 AVL-tree 和 RB-tree 的插入逻辑
java·数据结构·c++·算法·set·map