线程安全性

要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对共享的(Shared)可变的(Mutable)状态的访问。

  • “共享”意味着变量可以由多个线程同时访问

  • 而“可变”则意味着变量的值在其生命周期内可以发生变化

什么是线程安全性

当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。

正确性的含义是,某个类的行为与其规范完全一致。在良好的规范中通常会定义各种不变性条件(Invariant)约束对象的状态,以及定义各种后验条件(Postcondition)描述对象操作的结果

  1. 无状态对象一定是线程安全的。

  2. 当在无状态的类中添加一个状态时,如果该状态完全由线程安全的对象来管理,那么这个类仍然是线程安全的。

然而,当状态变量的数量由一个变为多个时,并不会像状态变量数量由零个变为一个那样简单。

竞态条件

最常见的竞态条件类型就是“先检查后执行(Check-Then-Act)”操作,即通过一个可能失效的观测结果来决定下一步的动作。

  • 首先观察到某个条件为真(例如文件 X 不存在),然后根据这个观察结果采用相应的动作(创建文件X)

  • 但事实上,在你观察到这个结果以及开始创建文件之间,观察结果可能变得无效(另一个线程在这期间创建了文件 X),从而导致各种问题(未预期的异常、数据被覆盖、文件被破坏等)。

示例:延迟初始化中的竞态条件

延迟初始化的目的是将对象的初始化操作推迟到实际被使用时才进行,同时要确保只被初始化一次。

public class LazyInitRace {
    private ExpensiveObject instance = null;
    public ExpensiveObject getInstance() {
        if (instance == null)
            instance = new ExpensiveObject();
        return instance;
    }
}

用锁来保护状态

示例:因式分解
public class CachedFactorizer implements Servlet {
    private BigInteger lastNumber;
    private BigInteger[] lastFactors;
    private long hits;
    private long cacheHits;
    
    public synchronized long getHits() { return hits; }
    public synchronized long getCacheHitRatio {
        return (double) cacheHits / hits;
    }
    
    public void service(ServletRequest request, ServletResponse response) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = null;
        synchronized(this) {
            ++hits;
            if (i.equals(lastNumber)) {
                cacheHits++;
                factors = lastFactors.clone();
            }
        }
        if (factors == null) {
            factors = factor(i);
            synchronized(this) {
                lastNumber = i;
                lasteFactories = factors.clone();
            }
        }
        encodeIntoReponse(response, factors);
    }
}

最后更新于