V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
labilixin
V2EX  ›  Java

《 Java 并发实战》中遇到的一个问题

  •  
  •   labilixin · 2023-08-02 16:52:51 +08:00 · 2069 次点击
    这是一个创建于 480 天前的主题,其中的信息可能已经有所发展或是发生改变。

    在《 java 并发实战》中 3.4.2 使用 volatile 发布不可变对象 章节中有一段代码理解不了,希望大家能不能给一个更通俗的解释。

        @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);
            }
        }
    
        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);
            }
        }
    

    这里原文解释说 VolatileCacheFactorizer 是线程安全的。但是我看起来 cache = new OneValueCache(i, factors)这行是线程不安全的。

    虽然 OneValueCache 是不可变对象,但是 VolatileCachedFactorizer 里的 cache 是有可能被多个线程同时写入的吧?

    希望大佬能帮忙解答一下,谢谢了。

    11 条回复    2023-08-04 16:49:15 +08:00
    Leo666666
        1
    Leo666666  
       2023-08-02 18:14:58 +08:00
    这里的 cache 是被 volatile 修饰的,当一个变量被 volatile 修饰时,它具有以下特性:
    1. 可见性:对一个 volatile 变量的写操作能立即被其他线程所看到,而其他线程的读操作也能得到最新值。
    2. 有序性:加入 volatile 修饰的变量在编译器和 CPU 层面会进行指令重排的优化,保证 volatile 变量之前的操作不会被编译器和 CPU 重排到 volatile 变量之后。
    QiWa
        2
    QiWa  
       2023-08-02 18:19:48 +08:00
    volatile 可以看成一个轻量 synchronized
    zhzy0077
        3
    zhzy0077  
       2023-08-02 18:45:46 +08:00 via Android
    变量赋值是原子的 多个线程同时写入最后一个为准
    mango88
        4
    mango88  
       2023-08-02 19:53:20 +08:00   ❤️ 1
    cache = new OneValueCache(i, factors) 写了之后,由于有 volatile 存在 另外一个线程执行 cache.getFactors(i) 立马可见
    你提到的 `cache 是有可能被多个线程多个线程同时写入的`
    是有可能的,
    但是从他的业务来看,虽然是不同的 cache 对象,但是不会引起业务错误,这应该是他想表达“线程安全”的意思
    lingalonely
        5
    lingalonely  
       2023-08-02 20:11:20 +08:00
    halozzz
        6
    halozzz  
       2023-08-02 20:12:44 +08:00 via iPhone
    注意 OneValueCache 里的操作都是 copy 的,不是直接赋值和返回值,这里的线程安全是指这个类两次初始化数据不会互相干扰,而不是只会初始化一次。
    halozzz
        7
    halozzz  
       2023-08-02 20:20:39 +08:00 via iPhone
    @lingalonely
    Nothing guarantees that two threads won't compute the factors from the same number. The only guarantee that this code offers is that, if the cache currently contains the factors for the requested number, these cached factors will be returned (and not factors for another number, or inconsistent data cause by a data race)
    race condition 依旧存在,不过能保证的是返回的值不会因为 race 存在而有不一样的结果
    Nerv
        8
    Nerv  
       2023-08-02 20:22:18 +08:00
    1. 引用写入具备原子性
    2. volatile 写入使引用具备可见性,OneValueCache 的属性是 final 的,final 的特殊规则说明其初始化之后对所有线程可见,因此属性访问也满足可见性,其总体满足可见性
    3. 即使被多个线程同时写入,导致的后果也就是多创建了几次对象(多余的对象会被 gc ),cache 存储的是最后一次被写入的值,不对后续 cache 的使用造成影响,满足一致性。
    lifespy
        9
    lifespy  
       2023-08-03 13:41:04 +08:00
    @Leo666666 #1 在 Java 中,使用 volatile 修饰的变量可以防止指令重排序。
    labilixin
        10
    labilixin  
    OP
       2023-08-03 15:36:04 +08:00
    感谢各位的解答,我最后的理解是,其实这个不是线程安全的。只不过他返回的值跟 cache 没关系。所以最后的结果是线程安全的。像 8 楼 @Nerv 说的一样,我也比较倾向于这种解释。

    感觉这个这本书里这个例子举的有点迷惑性。如果他是返回值跟 cache 有关系的话其实感觉会有可能返回意料之外的结果的。
    ZiChun
        11
    ZiChun  
       2023-08-04 16:49:15 +08:00
    ChatGPT 如是说:
    在这段代码中,虽然看起来 cache = new OneValueCache(i, factors) 这一行在没有同步机制的情况下,似乎是线程不安全的,但实际上由于 volatile 关键字和 OneValueCache 对象的不可变性,使得 VolatileCachedFactorizer 在多线程环境中仍然是线程安全的。即使多个线程试图同时创建新的 OneValueCache 对象并赋值给 cache ,但因为 volatile 关键字的内存可见性特性,所有线程都会读取到最新的 cache 值。另外,由于 OneValueCache 对象的不变性,即使 cache 被新的 OneValueCache 对象替换,其他线程获取的旧 OneValueCache 对象仍然是一致和有效的,因此并不会影响程序的正确性。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1030 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 18:52 · PVG 02:52 · LAX 10:52 · JFK 13:52
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.