《Java并发编程实战》第四章"对象的组合"深入探讨了如何通过合理设计类的结构、组合线程安全组件及应用设计模式,来构建线程安全的复杂对象。这一章的核心是将"线程安全"的责任分解到类的各个组成部分,而非在每个方法中单独处理同步,从而降低并发编程的复杂度。
一、设计线程安全类的基础:状态管理
线程安全的本质是对"共享可变状态"的安全访问控制。设计线程安全类的第一步是明确"状态"的范围和特性:
-
状态的定义
一个对象的状态是其所有实例变量(字段)的值的总和,包括:
- 自身声明的字段(如
int count
); - 引用对象的字段(如
List<String> list
中的元素,属于该对象状态的一部分)。
若对象的状态不被多个线程共享,或状态是不可变的,则天然线程安全。
- 自身声明的字段(如
-
状态的可变性与共享性
- 不可变状态 :用
final
修饰且构造后无法修改的状态(如String
、BigInteger
),无需同步即可安全共享。 - 可变状态 :若被多个线程共享,必须通过同步机制(锁、
volatile
、原子类等)保证访问的原子性、可见性和有序性。
- 不可变状态 :用
-
状态的所有权
- 若一个类"拥有"其状态(即状态是私有且未发布的),则该类可完全控制状态的访问方式;
- 若状态被发布(如通过
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)
若一个类的状态由多个线程安全的组件 (如ConcurrentHashMap
、AtomicInteger
)构成,且组件间的状态无依赖关系,则该类的线程安全性可"委托"给这些组件,无需额外同步。
1. 委托的条件
- 组件本身是线程安全的(如
ConcurrentHashMap
的put
、get
方法是线程安全的); - 组件的状态彼此独立(即操作一个组件时,无需考虑其他组件的状态)。
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
,所有操作(get
、replace
)均委托给它,因此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:
javapublic class EscapeInConstructor { public EscapeInConstructor() { // 构造未完成时,将this发布到外部 EventBus.register(this); } }
其他线程可能在
EscapeInConstructor
实例构造完成前就收到事件并调用其方法,导致访问到未初始化的字段。 -
发布内部类实例 :
内部类持有外部类的
this
引用,若在外部类构造时发布内部类实例,会间接导致外部类this
逸出。
2. 安全发布的方式
确保对象在完全初始化后再发布,且发布过程对其他线程可见:
-
通过静态初始化器发布 :JVM保证静态初始化的对象在被任何线程访问前完成初始化。
javapublic class StaticInitExample { public static final ImmutableObject INSTANCE = new ImmutableObject(); }
-
用volatile或AtomicReference发布 :
volatile
保证引用的可见性和初始化的安全性。javaprivate static volatile ImmutableObject instance;
-
用同步方法/块发布 :如通过
synchronized
的工厂方法发布。javaprivate static ImmutableObject instance; public synchronized static ImmutableObject getInstance() { if (instance == null) { instance = new ImmutableObject(); } return instance; }
-
使用final字段 :
final
修饰的字段在构造函数完成后会被安全发布,其他线程能看到其正确值。
五、线程安全的容器与装饰器
第四章还讨论了如何利用现有容器类构建线程安全的对象,核心是"委托给线程安全的容器"或"用装饰器添加同步"。
-
同步容器(Synchronized Containers)
如
Vector
、Hashtable
,或通过Collections.synchronizedXxx()
包装的容器(如synchronizedList
)。-
原理:对容器的每个方法加锁(使用容器自身作为锁),保证单个方法的线程安全。
-
局限性:复合操作(如"迭代""检查再添加")仍需外部加锁,否则可能出现竞态条件。
javaList<String> list = Collections.synchronizedList(new ArrayList<>()); // 复合操作需手动加锁 synchronized (list) { for (String s : list) { ... } // 迭代需锁保护 }
-
-
并发容器(Concurrent Containers)
如
ConcurrentHashMap
、CopyOnWriteArrayList
,专为高并发设计:- 采用细粒度锁或无锁算法(如
ConcurrentHashMap
的分段锁),支持更高的并发度。 - 部分操作(如
putIfAbsent
)本身是原子的,无需外部同步。 - 迭代时不抛出
ConcurrentModificationException
(弱一致性迭代器)。
- 采用细粒度锁或无锁算法(如
六、总结:构建线程安全类的实践原则
- 明确状态范围:识别所有构成对象状态的变量(包括引用对象的字段)。
- 优先封装与封闭:通过实例封闭将状态限制在类内部,用内部锁协调访问。
- 善用线程安全组件 :尽量委托给线程安全的组件(如
ConcurrentHashMap
、AtomicInteger
),减少手动同步。 - 处理状态依赖:当组件状态存在依赖时,需额外加锁保证复合操作的原子性。
- 安全发布对象 :避免对象逸出,确保在完全初始化后再发布,使用
final
、volatile
或同步机制保证可见性。
通过这些原则,可将复杂对象的线程安全问题分解为更小的、可管理的部分,从而降低并发编程的难度,提高代码的可靠性。