【问题标题】:Can this class be thread safe lazy loading, or is it as unsafe as "double checked locking"?这个类可以是线程安全的延迟加载,还是像“双重检查锁定”一样不安全?
【发布时间】:2020-07-09 02:55:19
【问题描述】:

示例代码如下

public class Lazy<T> implements Supplier<T> {

    public Lazy(Supplier<T> supplier) {
        this.supplier = Objects.requireNonNull(supplier);
    }

    Supplier<T> supplier;

    T value;

    @Override
    public T get() {
        if (supplier != null) {
            synchronized (this) {
                if (supplier != null) {
                    value = supplier.get();
                    supplier = null;
                }
            }
        }
        return value;
    }
}

我担心如果“供应商”是一个构造函数。 “supplier=null”可以在对象初始化之前执行。可能会出现类似于“双重检查锁定损坏”的错误。

"supplier.get()==null" 在这个类中可能为真。所以我不检查值是否为空

如果它是线程不安全的,我应该在“供应商”字段之前添加“volatile”吗? 如果它是线程安全的,为什么?

【问题讨论】:

  • 使用其他变量而不是设置supplier=null不是更好吗?
  • get 是一个实例方法,因此调用它的唯一方法是通过Lazy 的实例,因此您需要调用@987654324 的构造函数@,因此为什么不制作Supplier final 并摆脱if (supplier != null) { 检查?为什么要同步get结果 而不是supplier 本身?否则T 必须是易失性的,是的。
  • 谢谢你的提醒。我写了错误的示例代码。我已经更正了内容

标签: java thread-safety


【解决方案1】:

这有点复杂,但请阅读this。简而言之,没有 volatile,这就坏了。整个构造可以大大简化:

public class Lazy<T> implements Supplier<T> {
    
     private final Supplier<T> supplier;

     volatile boolean computed = false;

     T value;

     public Lazy(Supplier<T> supplier) {
          this.supplier = Objects.requireNonNull(supplier);
     }

     @Override
     public T get() {
          if (!computed) {
                synchronized (this) {
                   if (!computed) {
                        value = supplier.get();
                        computed = true;
                   }
                }
           }

        return value;
     }

}

【讨论】:

  • @user10339780 由 在代码中做出适当反应以应对这种情况。 “行不通”究竟是什么意思?发生这种情况时,您可能想抛出异常吗?我无法读心
  • 希望第一次调用Supplier后缓存的结果不需要重复计算。如果 "supplier.get() == null",Supplier.get 会在你的代码中被重复调用。
  • @user10339780 正确。所以你想说你也想缓存一个null 值吗?这意味着null 完全可以存储在value 中并且只计算一次?这将是一个相当奇怪的不变量
  • 只希望能简单的使用“懒供应商”,不用判断返回值是否为空
  • @user10339780 我想这就是您故意引入供应商空值检查的原因。更好的方法是引入另一个变量,见编辑。但老实说,我不知道这会给来电者带来什么,他们现在如何以有意义的方式使用 null
【解决方案2】:

问题是,理论上,您无法控制供应商.get() 将返回什么。它可能为空,每次都可能是不同的值,等等。因此,我声称这段代码不是线程安全的。另请注意,“供应商”只有在他们这样做时才会为空:

new Lazy(null)

否则它永远不会为空。这种情况你不妨在构造函数中抛出异常

    public Lazy(Supplier<T> supplier) {
        if (supplier == null) {
            throw new IllegalArgumentException("'supplier' must not be null");
        }
        this.supplier = supplier;
    }

我不确定您要在这里实现什么。如果您想懒惰地初始化“值”,只需一次,您可以执行以下操作:

if (value == null) {
    synchronize (this) {
        // test again to ensure no other thread initialized as we acquired monitor
        if (value == null) {
            value = supplier.get();
        }
    }
}

return value;

【讨论】:

  • 谢谢你的提醒。我写了错误的示例代码。我已经更正了内容
  • 好的,现在有了你的新代码,供应商在get()方法中永远不会为空,因为get()方法在构造函数完成之前永远不会被执行。这是因为为了让线程能够使用“lazySupplier.get()”,首先它必须将完全构造的“Lazy”实例分配给引用“lazySupplier”,然后才能调用“lazySupplier.get” ()”。这意味着,构造函数必须始终在实例中的任何方法被调用之前完成。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-08-13
  • 1970-01-01
相关资源
最近更新 更多