在前面的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对象逸出,所以这里也是线程安全的。
L'objet cache n'a qu'une seule opération d'écriture (création d'un nouvel objet cache) dans service(), et les autres sont des opérations de lecture. Ceci est cohérent avec le scénario d'application volatile et garantit que l'objet cache est visible. d'autres fils de discussion. Il y aura des problèmes avec la lecture simultanée.
Le résultat renvoyé est l'objet facteurs. Les facteurs sont des variables locales et ne provoquent pas la fuite de l'objet cache, il est donc également thread-safe ici.
Ma compréhension personnelle :
(1). L'objet cache n'a qu'une seule opération d'écriture dans service(), mais plusieurs threads effectueront cette opération d'écriture. Par exemple, après que le thread A ait introduit le paramètre a1 pour exécuter la méthode de service, le cache contient number=a, lastFactors=[a] ; puis le thread a2 entre dans la méthode de service et introduit a, et exécute BigInteger[] Factors = cache. getFactors(i); pour obtenir Lorsque le thread passe aux données mises en cache [a] pour le jugement, un thread C arrive et introduit le paramètre c pour exécuter la méthode. En fait, le cache à ce moment est c et [c]. Théoriquement, la valeur lue par le thread B est déjà expirée. . . C'est simplement en raison de la signification commerciale de la « mise en cache » que cette valeur expirée ne provoquera pas d'erreur de programme. . . En d’autres termes, la sécurité des threads de cet exemple est désormais adaptée à cette activité. . .
(2).L’immuabilité de OneValueCache a également des limites. Une fois qu'un tableau de type BigInteger[] a accepté des paramètres externes, utilisez Arrays.copyOf pour empêcher la valeur de changer après l'initialisation. Mais s'il ne s'agit pas d'un type BigInteger mais d'un autre type, vous devez vous assurer que le type est également un objet immuable.
En bref, j'ai l'impression que pour un débutant comme moi, le cadenas reste toujours le meilleur à utiliser. Après tout, l’optimisation du programme repose sur l’absence de BUG. S’il y a des failles cachées dues à un malentendu, alors nous serons à genoux.