【问题标题】:"Java concurrency in practice" - cached thread-safe number factorizer (Listing 2.8)“Java 并发实践”——缓存的线程安全数字分解器(清单 2.8)
【发布时间】:2012-08-20 08:23:24
【问题描述】:

在下面的代码中(复制自Java Concurrency in Practice Chapter 2, section 2.5, Listing 2.8):

@ThreadSafe
public class CachedFactorizer implements Servlet {
    @GuardedBy("this") private BigInteger lastNumber;
    @GuardedBy("this") private BigInteger[] lastFactors;
    @GuardedBy("this") private long hits;
    @GuardedBy("this") private long cacheHits;

    public synchronized long getHits() { return hits; }

    public synchronized double getCacheHitRatio() {
        return (double) cacheHits / (double) hits;
    }

    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = null;
        synchronized (this) {
            ++hits;
            if (i.equals(lastNumber)) {
                ++cacheHits;
                factors = lastFactors.clone(); // questionable line here
            }
        }
        if (factors == null) {
            factors = factor(i);
            synchronized (this) {
                lastNumber = i;
                lastFactors = factors.clone(); // and here
            }
        }
        encodeIntoResponse(resp, factors);
    }
}

为什么要克隆factors、lastFactors 数组?不能简单写成factors = lastFactors;和lastFactors = factors;吗?就因为factors是一个局部变量,然后传给encodeIntoResponse,可以修改吗?

希望问题很清楚。谢谢。

【问题讨论】:

  • 这么多年过去了,你的问题得到答案了吗?

标签: java thread-safety clone local-variables


【解决方案1】:

这称为防御性复制。数组和其他对象一样是对象,所以

 factors = lastFactors

会将 lastFactos 的引用分配给因子,反之亦然。所以任何人都可以在你的控制之外覆盖你的状态。举个例子:

private void filterAndRemove(BigInteger[] arr);
private void encodeIntoResponse(..., BigInteger[] factors) {
   filterAndRemove(factors);
}

使用我们的理论赋值 filterAndRemove 也会影响原来的 lastFactorials。

【讨论】:

    【解决方案2】:

    从基础猜测的答案:如果您打算修改对象,则需要克隆,并且您不想修改原始对象,在您的情况下,factors = lastFactors.clone(); 已完成,因为您不想修改 lastFactors相反,您克隆它并将其发送到encodeIntoResponse(resp, factors);,其中可能包含修改它的代码。

    【讨论】:

    • 谢谢。如果我错了,请纠正我,但没有代码可以修改 lastFactor 或 factor 中保存的数组。因此,lastFactors 中保存的数组不会发生并发修改。对lastNumber,lastFactors atomic 进行分配需要同步,但不能阻止lastFactors 的并发修改。所以这个问题是开放的(对我来说)。
    【解决方案3】:

    克隆数组的唯一原因是阻止(在这种情况下并发)修改数组元素。但是,在这种情况下,假设没有其他方法修改 lastFactors 引用的数组,这在本例中看起来是不可能的。存储在factors 和lastFactors 中的数组都是由factor 创建并以完整状态返回的,它们的引用分配在同步块中,这将导致它们被安全发布。

    除非encodeIntoResponse 修改其factors 参数的元素,否则在我看来对clone 的调用是不必要的。

    【讨论】:

    • 嗯...在对代码进行更仔细的审查并重新阅读@khachik cmets 之后,我认为@david-harkness 的上述答案是最正确的:因为lastFactors 是安全的发布了,而且发布后没有修改,看来克隆是没有必要的。
    【解决方案4】:

    我同意本书的作者可以更好地解释本书的该部分。

    确实,为了正确实现线程安全,您必须使用同一个锁同步read和write操作;在上面的代码中,为了尽量减少同步量,作者决定在不同步的情况下执行encodeIntoResponse(...):因为encodeIntoResponse(...)方法读取factors引用的数组的内容然后作者将其克隆到一个新的数组中。

    注意:虽然factors 确实是一个局部变量,但一般来说,它仍然需要被克隆,因为同一个数组是由同步和非同步代码读取的,如果我们传递引用(没有克隆)到lastFactors和encodeIntoResponse(...)。

    但是,正如@khachik 在问题中和@david-harkness 在回复中正确指出的那样,在这种特定情况下,clone 调用是不必要的,因为lastFactors 是安全发布的,并且在之后不会修改它的出版。

    【讨论】:

      【解决方案5】:

      如果将factors = lastFactors.clone();改为factors = lastFactors;,factors和lastFactors都指向同一个对象,factors不再是一个局部变量,它变成了一个共享可变状态。

      假设有三个请求,请求 A、B、C。请求 A 和 B 发送的数量是 10,但请求 C 发送的数量是 20。如果发生以下执行顺序并且您更改了,事情可能会出错factors = lastFactors.clone(); 到 factors = lastFactors;。

      1. servlet服务器收到请求A,整个service方法被执行,现在lastNumber是10,lastFactors是[1, 2, 5, 10]。
      2. servlet 服务器同时接收到请求 B 和 C,首先处理请求 B,但在退出第一个 synchronized 块后(现在对于请求 B,factors 是 [1, 2, 5, 10],这是正确的),请求 C 被处理。
      3. 对于请求C,整个service方法被执行,它将lastFactors从[1, 2, 5, 10]更改为[1, 2, 4, 5, 10, 20],因为factorslastFactors指向同一个对象, factors 现在也是 [1, 2, 4, 5, 10, 20]。 请求 B 的响应应该是 [1, 2, 5, 10],但现在是 [1, 2, 4, 5, 10, 20]。

      【讨论】:

      • 在 3 中,当您执行 lastFactors = factor 时,它只会修改 lastFactor 引用,而不是其他线程中的因子引用。他们都指向同一个对象,现在 lastFactors 指向一个新对象
      猜你喜欢
      • 2011-03-15
      • 2018-11-06
      • 1970-01-01
      • 1970-01-01
      • 2017-10-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多