《Java并发编程实战》第三章:对象共享机制深度解析
引言:多线程环境下的可见性挑战
在单线程环境中,当向变量写入值后,可以立即读取该值。但在多线程环境中,如果没有使用适当的同步机制,无法保证一个线程对变量的修改能够被其他线程看到。这种可见性问题(Visibility)是Java并发编程中最微妙且容易忽视的问题之一。
public class NoVisibility {
private static boolean ready;
private static int number;
private static class ReaderThread extends Thread {
public void run() {
while (!ready)
Thread.yield();
System.out.println(number);
}
}
public static void main(String[] args) {
new ReaderThread().start();
number = 42;
ready = true;
}
}
上述程序可能产生三种结果:程序永不终止、输出0,或正确输出42。JVM在后台进行的优化(如重排序、变量常量化等)是导致这种现象的根本原因。
内存可见性机制深度剖析
1. 陈旧数据(Stale Data)问题
陈旧数据是指线程读取到的值是过时的数据,这可能导致数据结构损坏、计算不准确甚至无限循环。
2. 64位非原子操作的特殊性
对于64位变量(如long类型),如果没有同步或volatile关键字,不仅可能读取到陈旧数据,还可能读取到"撕裂值"(torn read)——高32位来自一个值,低32位来自另一个值。
volatile关键字的正确使用
volatile是Java中一种轻量级的同步机制,它只保证可见性,不保证原子性。
// 正确使用volatile的场景
public class StatusFlag {
private volatile boolean shutdownRequested;
public void shutdown() { shutdownRequested = true; }
public boolean isShutdownRequested() { return shutdownRequested; }
}
使用volatile变量的条件:
- 写入操作不依赖于变量的当前值
- 变量不参与与其他状态变量的复合操作
- 访问变量时不需要其他原因的锁保护
对象发布与逃逸(Publication & Escape)
对象发布的三种危险方式
// 1. 通过公共静态字段发布
public static Set<Secret> knownSecrets;
// 2. 从非私有方法返回私有字段引用
class UnsafeStates {
private String[] states = {"AK", "AL"};
public String[] getStates() { return states; } // 危险!
}
// 3. 发布内部类引用
public class ThisEscape {
public ThisEscape(EventSource source) {
source.registerListener(new EventListener() {
public void onEvent(Event e) {
doSomething(e); // 可以访问外部类的this引用
}
});
}
}
安全构造实践
避免在构造过程中让this引用逃逸的正确做法:
public class SafeListener {
private final EventListener listener;
private SafeListener() {
listener = new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
};
}
public static SafeListener newInstance(EventSource source) {
SafeListener safe = new SafeListener();
source.registerListener(safe.listener);
return safe;
}
}
线程限制(Thread Confinement)策略
栈限制(Stack Confinement)
public class AnimalPairs {
// 线程安全的方法 - 使用栈限制
public int countPairs(List<Animal> animals) {
// animalsCopy被限制在当前线程的栈中
List<Animal> animalsCopy = new ArrayList<>(animals);
int pairs = 0;
for (int i = 0; i < animalsCopy.size(); i++) {
for (int j = i + 1; j < animalsCopy.size(); j++) {
if (animalsCopy.get(i).isPotentialMate(animalsCopy.get(j)))
pairs++;
}
}
return pairs;
}
}
ThreadLocal的应用
// 使用ThreadLocal维护线程特定的数据库连接
public class ConnectionManager {
private static ThreadLocal<Connection> connectionHolder =
new ThreadLocal<Connection>() {
@Override
protected Connection initialValue() {
return DriverManager.getConnection(DB_URL);
}
};
public static Connection getConnection() {
return connectionHolder.get();
}
public static void removeConnection() {
connectionHolder.remove();
}
}
不可变性(Immutability)设计模式
不可变对象的定义条件
一个对象是不可变的,当且仅当:
- 创建后其状态不能被修改
- 所有字段都是final的
- 正确构造(this引用在构造期间没有逃逸)
@Immutable
public final class ThreeStooges {
private final Set<String> stooges = new HashSet<>();
public ThreeStooges() {
stooges.add("Moe");
stooges.add("Larry");
stooges.add("Curly");
}
public boolean isStooge(String name) {
return stooges.contains(name);
}
}
final字段的特殊保证
final字段具有"初始化安全性"保证,这是Java内存模型为不可变对象提供的特殊保障。
安全发布(Safe Publication)机制
安全发布惯用法
对象被正确发布的条件:
- 从静态初始化器初始化
- 引用存储到volatile变量或AtomicReference中
- 引用存储到正确构造对象的final字段中
- 引用存储到由锁正确保护的字段中
可变对象的安全发布模式
// 使用volatile发布不可变对象
@ThreadSafe
public class VolatileCachedFactorizer implements Servlet {
private volatile OneValueCache cache = new OneValueCache(null, null);
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = cache.getFactors(i);
if (factors == null) {
factors = factor(i);
cache = new OneValueCache(i, factors); // 安全发布
}
encodeIntoResponse(resp, factors);
}
}
@Immutable
class OneValueCache {
private final BigInteger lastNumber;
private final BigInteger[] lastFactors;
public OneValueCache(BigInteger i, BigInteger[] factors) {
lastNumber = i;
lastFactors = Arrays.copyOf(factors, factors.length);
}
public BigInteger[] getFactors(BigInteger i) {
if (lastNumber == null || !lastNumber.equals(i))
return null;
return Arrays.copyOf(lastFactors, lastFactors.length);
}
}
对象共享安全策略总结
| 对象类型 | 发布要求 | 访问要求 | 典型应用场景 |
|---|---|---|---|
| 线程限制对象 | 无特殊要求 | 只能在拥有线程中修改 | 局部变量、ThreadLocal |
| 共享只读对象 | 安全发布 | 无需同步 | 不可变对象、配置数据 |
| 共享线程安全对象 | 安全发布 | 无需额外同步 | ConcurrentHashMap、AtomicInteger |
| 受保护对象 | 安全发布 | 需要持有特定锁 | 自定义同步对象 |
最佳实践与性能考量
1. 优先使用不可变对象
// 使用不可变对象避免同步
public class ImmutablePoint {
private final int x;
private final int y;
public ImmutablePoint(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() { return x; }
public int getY() { return y; }
// 返回新实例而不是修改现有实例
public ImmutablePoint move(int deltaX, int deltaY) {
return new ImmutablePoint(x + deltaX, y + deltaY);
}
}
2. 有效不可变对象的使用
// 将可变对象转换为有效不可变对象
public class EffectiveImmutable {
private final Map<String, String> config;
public EffectiveImmutable(Map<String, String> config) {
// 防御性复制
this.config = Collections.unmodifiableMap(new HashMap<>(config));
}
public String getConfig(String key) {
return config.get(key);
}
}
3. 性能优化策略
结论与实战建议
Java并发编程中的对象共享机制是一个需要精心设计的领域。通过理解可见性、安全发布、线程限制和不可变性等核心概念,可以构建出既正确又高效的并发程序。
关键要点总结:
- 可见性优先:总是假设多线程环境下的可见性问题会发生
- 安全发布:确保对象在发布时的状态一致性
- 不可变性:尽可能使用不可变对象简化并发控制
- 明确策略:为每个共享对象选择明确的线程安全策略
通过遵循这些原则和实践,可以显著降低并发程序的复杂度,提高代码的可维护性和性能。记住,在并发编程中,预防问题远比修复问题更重要。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



