在前面的UnsafeCachingFactorizer类中,我们尝试用两个AtomicReferences变量来保存最新的数值及其因数分解结果,但这种方式并非是线程安全的,因为我们无法以原子方式来同时读取或更新这两个相关的值。同样,用volatile类型的变量来保存这些值也不是线程安全的。然而,在某些情况下,不可变对象能提供一种弱形式的原子性。
因式分解Servlet将执行两个原子操作:更新缓存的结果,以及通过判断缓存中的数值是否等于请求的数值来决定是否直接读取缓存中的因数分解结果。每当需要对一组相关数据以原子方式执行某个操作时,就可以考虑创建一个不可变的类来包含这些数据,例如程序清单3-12中的OneValueCache。
@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;
else
return Arrays.copyOf(lastFactors, lastFactors.length);
}
}
对于在访问和更新多个相关变量时出现的竞争条件问题,可以通过将这些变量全部保存在一个不可变对象中来消除。如果是一个可变的对象,那么就必须使用锁来确保原子性。如果是一个不可变对象,那么当线程获得了对该对象的引用后,就不必担心另一个线程会修改对象的状态。如果要更新这些变量,那么可以创建一个新的容器对象,但其他使用原有对象的线程仍然会看到对象处于一致的状态。
程序清单3-13中的VolatileCachedFactorizer使用了OneValueCache来保存缓存的数值及其因数。当一个线程将volatile类型的cache设置为引用一个新的OneValueCache时,其他线程就会立即看到新缓存的数据。
@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) {
factorfactors = factor(i);
cache = new OneValueCache(i, factors);
}
encodeIntoResponse(resp, factors);
}
}
与cache相关的操作不会相互干扰,因为OneValueCache是不可变的,并且在每条相应的代码路径中只会访问它一次。
通过使用包含多个状态变量的容器对象来维持不变性条件,并使用一个volatile类型的引用来确保可见性,使得VolatileCachedFactorizer在没有显式地使用锁的情况下仍然是线程安全的。
程序清单3-13中存在『先检查后执行』(Check-Then-Act)的竞态条件。
OneValueCache类的不可变性仅保证了对象的原子性。
volatile仅保证可见性,无法保证线程安全性。
综上,对象的不可变性+volatile可见性,并不能解决竞态条件的并发问题,所以原文的这段结论是错误的。
疑惑已经解决了。
结论:
cache对象在service()中只有一处写操作(创建新的cache对象),其余都是读操作,这里符合volatile的应用场景,确保cache对象对其他线程的可见性,不会出现并发读的问题。返回的结果是factors对象,factors是局部变量,并未使cache对象逸出,所以这里也是线程安全的。
캐시 개체에는 service()에서 쓰기 작업(새 캐시 개체 생성)이 하나만 있고 나머지는 읽기 작업입니다. 이는 일시적인 애플리케이션 시나리오와 일치하며 캐시 개체가 표시되도록 합니다. 다른 스레드를 동시에 읽는 데 문제가 있습니다.
반환된 결과는 Factors 객체입니다. Factors는 지역 변수이고 캐시 객체가 이스케이프되도록 하지 않으므로 여기서도 스레드로부터 안전합니다.
개인적인 이해:
(1) 캐시 객체에는 service()에서 쓰기 작업이 하나만 있지만 여러 스레드가 이 쓰기 작업을 수행합니다. 예를 들어 스레드 A가 서비스 메서드를 실행하기 위해 매개변수 a1을 가져온 후 캐시에 number=a, lastFactors=[a]가 포함되고 스레드 a2가 서비스 메서드에 들어가서 a를 가져온 다음 BigInteger[] 요인 = 캐시를 실행합니다. getFactors(i); 스레드가 판단을 위해 캐시된 데이터 [a]로 전환하면 C 스레드가 와서 메서드를 실행하기 위해 매개변수 c를 가져옵니다. 실제로 이때 캐시는 c와 [c]이다. 이론적으로 스레드 B가 읽은 값은 이미 만료된 상태이다. . . 만료된 값이 프로그램 오류를 일으키지 않는 것은 "캐싱"의 비즈니스 의미 때문입니다. . . 즉, 이 예의 스레드 안전성이 이제 이 비즈니스에 적합하다는 것입니다. . .
(2).OneValueCache의 불변성에도 한계가 있습니다. BigInteger[] 유형의 배열이 외부 매개변수를 허용한 후 Arrays.copyOf를 사용하여 초기화 후 값이 변경되는 것을 방지합니다. 그러나 BigInteger 유형이 아닌 다른 유형인 경우 해당 유형도 불변 객체인지 확인해야 합니다.
요컨대 저 같은 초보자에게는 여전히 자물쇠가 가장 잘 사용되는 것 같습니다. 결국, 프로그램 최적화는 BUG가 없다는 것을 기반으로 합니다. 어느 쪽이든 오해로 인해 숨은 허점이 있다면 무릎을 꿇을 것이다.