3

我正在寻找类似问题的答案,使用 AtomicReference.compareAndSet 设置对数据库调用结果的引用是否合适?但有不同的要求。

目标是只创建一次实例ObjectWithSideEffectConstructor以避免重复的副作用。施工必须发生在setUp(). 多个线程将调用setUp(). 同样会有一个tearDown()用于从对象中回收资源,这里省略。问题:实现目标的最佳实践是什么?

仅仅使用AtomicReference是不够的,因为会先执行构造函数,所以会产生副作用。

private static AtomicReference<ObjectWithSideEffectConstructor> ref =
  new AtomicReference<ObjectWithSideEffectConstructor>()

void setUp() {
  ref.compareAndSet(null, new ObjectWithSideEffectConstructor());
}

使用是否适合使用 AtomicReference.compareAndSet 来设置对数据库调用结果的引用的答案不会工作,因为volatile缺乏同步。会有多个线程进入的窗口if

private static volatile ObjectWithSideEffectConstructor obj;

void setUp() {
  if (obj == null) obj = new ObjectWithSideEffectConstructor();
}

简单的修复将是

private static ObjectWithSideEffectConstructor obj;
private static final Object monitor = new Object();

void setUp() {
  synchronized (monitor) {
    if (obj == null) obj = new ObjectWithSideEffectConstructor();
  }
}

同样,具有易失性监视器的 DCL 可能会提供更好的读取性能。但两者都需要某种程度的同步,因此预计性能会更差。

我们也可以使用FutureTask. 它更高效,因为一旦创建了对象,后续FutureTask.get()将返回而不会阻塞。但它肯定比synchronized.

private static final AtomicReference<FutureTask<ObjectWithSideEffectConstructor>> ref =
  new AtomicReference<FutureTask<ObjectWithSideEffectConstructor>>();

void setUp() {
  final FutureTask<ObjectWithSideEffectConstructor> future =
    new FutureTask<ObjectWithSideEffectConstructor>(
      new Callable<ObjectWithSideEffectConstructor>() {
        @Override
        public ObjectWithSideEffectConstructor call() throws InterruptedException {
          return new ObjectWithSideEffectConstructor();
        }
      }
    );
  if (ref.compareAndSet(null, future)) future.run();
  ref.get().get();
}

感谢您的建议。

4

6 回答 6

3

如果你在谈论单例线程安全 延迟初始化,这里有一个很酷的代码模式,它可以在没有任何同步代码的情况下完成 100% 线程安全的延迟初始化

public class MySingleton {

     private static class MyWrapper {
         static MySingleton INSTANCE = new MySingleton();
     }

     private MySingleton () {}

     public static MySingleton getInstance() {
         return MyWrapper.INSTANCE;
     }
}

这种编码模式被称为Initialization-on-demand holder idiom。它只会在getInstance()被调用时实例化单例,并且它是 100% 线程安全的!这是一个经典。

它之所以起作用,是因为类加载器有自己的同步来处理类的静态初始化:保证在使用类之前所有静态初始化都已完成,并且在此代码中,类仅在getInstance()方法内使用,所以类加载内部类。

于 2013-10-15T21:13:47.597 回答
1

始终对单例使用枚举类型,不仅可以优雅地强制执行单例,还可以防止常见的编程错误,例如单例从其超类继承 clone() 方法而程序员忘记用私有方法声明覆盖它。或者当您忘记覆盖可反序列化并允许程序员序列化您的单例,声明一个新实例,然后反序列化旧实例时。

或者,如果您使用静态工厂模式,您可以声明实例字段瞬态并使用 readresolve 方法。如果您可能在设计过程的后期改变主意是否应该是单例,这提供了灵活性。

信用:基于 J Bloch 的 Effective Java 的回答(第 3 项),这是一本每个 Java 程序员都应该阅读、拥有和经常参考的书......

于 2013-10-15T10:58:02.643 回答
1

使用 Enum 在 Java 5 或更高版本中实现 Singleton:

Enum 是线程安全的,通过 Enum 实现单例可确保您的单例即使在多线程环境中也只有一个实例。让我们看一个简单的实现:

public enum SingletonEnum 
{
    INSTANCE;
    public void doStuff()
    {
        System.out.println("Singleton using Enum");
    }
}

// How to use in other classes
public static void main(String[] args) 
{
    SingletonEnum.INSTANCE.doStuff();
}
于 2013-10-15T08:53:46.617 回答
0

我假设您只需要一个ObjectWithSideEffectConstructor。这里有一个问题,即 1)是否是您想要避免的副作用发生两次,或者 2)您只需要最终获得一致的(单例)参考。

无论哪种方式,synchronized都是一个很好的标准选择。当第一个线程处于设置状态时,它将阻止其他线程构建第二个实例。

如果您处于情况 1),则可能需要使用同步。如果启动后的性能很关键,您可以考虑在同步部分之前使用AtomicReference.get()快速路径,以便在启动完成后避免同步部分。

如果您处于情况 2)中,那么 - 从您的问题中并不清楚 - 存在构造的副作用,但您不关心复制它 - 只要客户端代码只有“看到”一个一致的单一参考。

在第二种情况下,您可以使用AtomicReference.get()它来检查它是否已经初始化,如果是则返回。然后线程将进入“竞赛部分”,在那里它们将构造(可能是多个)ObjectWithSideEffectConstructor。最后,会有一个compareAndSet只有一个线程设置单例..失败的线程回退到一个AtomicReference.get()以获取正确的单例。

在性能方面,一次调用AtomicReference比一个synchronized块更快——但我不确定,通过双重和三重检查和构建不需要的副作用对象,第二种方法是否会是。同样,一个简单的synchronized块可能更简单、更快。

我很想看看一些测量结果。

于 2013-10-12T00:13:58.113 回答
0

同步方法将是要走的路。如果您确实需要性能,则需要重组代码以进行单线程预初始化。使用任何其他形式都会导致单例模式中描述的副作用。

于 2013-10-12T00:28:59.007 回答
0

就其价值而言,FutureTask 方法实际上并不需要所有这些代码。AtomicReference不需要,也不应该同时调用 run() and get()。所以你可以稍微简化一下:

private static final Future<ObjectWithSideEffectConstructor> future =
  new FutureTask<>(
    new Callable<ObjectWithSideEffectConstructor>() {
      @Override
      public ObjectWithSideEffectConstructor call() throws InterruptedException {
        return new ObjectWithSideEffectConstructor();
      }
    }
  );

void setUp() {
  future.run(); // or future.get(), if you want to get any exception immediately
}

此外,在 Java 8 中,初始化表达式可以写得更简洁;以上可以简化为:

private static final Future<ObjectWithSideEffectConstructor> future =
  new FutureTask<>(ObjectWithSideEffectConstructor::new);

void setUp() {
  future.run(); // or future.get(), if you want to get any exception immediately
}
于 2016-04-22T19:03:03.203 回答