繁体   English   中英

如何实现线程安全的延迟初始化?

[英]How to implement thread-safe lazy initialization?

有哪些推荐的方法来实现线程安全的延迟初始化? 例如,

// Not thread-safe
public Foo getInstance(){
    if(INSTANCE == null){
        INSTANCE = new Foo();
    }

    return INSTANCE;
}

如果您使用的是Apache Commons Lang ,那么您可以使用ConcurrentInitializer 的一种变体,如LazyInitializer

例子:

ConcurrentInitializer<Foo> lazyInitializer = new LazyInitializer<Foo>() {

        @Override
        protected Foo initialize() throws ConcurrentException {
            return new Foo();
        }
    };

您现在可以安全地获取 Foo(仅初始化一次):

Foo instance = lazyInitializer.get();

如果您使用Google 的 Guava

Supplier<Foo> fooSupplier = Suppliers.memoize(new Supplier<Foo>() {
    public Foo get() {
        return new Foo();
    }
});

然后通过Foo f = fooSupplier.get();调用它

从 Suppliers.memoize javadoc

返回一个供应商,它缓存在第一次调用 get() 期间检索到的实例,并在后续调用 get() 时返回该值。 返回的供应商是线程安全的 委托的 get() 方法最多会被调用一次 如果委托是之前调用 memoize 创建的实例,则直接返回它。

对于单例,有一个优雅的解决方案,将任务委托给 JVM 代码进行静态初始化。

public class Something {
    private Something() {
    }

    private static class LazyHolder {
            public static final Something INSTANCE = new Something();
    }

    public static Something getInstance() {
            return LazyHolder.INSTANCE;
    }
}

http://en.wikipedia.org/wiki/Initialization_on_demand_holder_idiom

以及 Crazy Bob Lee 的这篇博文

http://blog.crazybob.org/2007/01/lazy-loading-singletons.html

这可以通过使用AtomicReference作为实例持有者以无锁方式完成:

// in class declaration
private AtomicReference<Foo> instance = new AtomicReference<>(null);  

public Foo getInstance() {
   Foo foo = instance.get();
   if (foo == null) {
       foo = new Foo();                       // create and initialize actual instance
       if (instance.compareAndSet(null, foo)) // CAS succeeded
           return foo;
       else                                   // CAS failed: other thread set an object 
           return instance.get();             
   } else {
       return foo;
   }
}

这里的主要缺点是多个线程可以同时实例化两个或多个Foo对象,并且只有一个会被设置好,所以如果实例化需要 I/O 或其他共享资源,这种方法可能不合适。

另一方面,这种方法是无锁和无等待的:如果第一个进入此方法的线程卡住了,则不会影响其他线程的执行。

最简单的方法是使用静态内部持有人类:

public class Singleton {

    private Singleton() {
    }

    public static Singleton getInstance() {
        return Holder.INSTANCE;
    }

    private static class Holder {
        private static final Singleton INSTANCE = new Singleton();
    }
}
class Foo {
  private volatile Helper helper = null;
  public Helper getHelper() {
    if (helper == null) {
      synchronized(this) {
        if (helper == null) {
          helper = new Helper();
        }
      }
    }
  return helper;
}

这称为双重检查! 检查这个http://jeremymanson.blogspot.com/2008/05/double-checked-locking.html

如果您在项目中使用 lombok,则可以使用此处描述的功能。

您只需创建一个字段,用@Getter(lazy=true)对其进行注释并添加初始化,如下所示: @Getter(lazy=true) private final Foo instance = new Foo();

您只需要使用 getter 来引用字段(请参阅 lombok文档中的注释),但在大多数情况下,这正是我们所需要的。

考虑延迟初始化,我希望得到一个“几乎真实”的对象,它只是装饰尚未初始化的对象。

当调用第一个方法时,装饰接口内的实例将被初始化。

* 由于Proxy的使用,发起的对象必须实现传递的接口。

* 与其他解决方案的不同之处在于从使用开始的封装。 您开始直接使用DataSource ,就像它已初始化一样。 它将在第一个方法调用时初始化。

用法:

DataSource ds = LazyLoadDecorator.create(dsSupplier, DataSource.class)

幕后花絮:

public class LazyLoadDecorator<T> implements InvocationHandler {

    private final Object syncLock = new Object();
    protected volatile T inner;
    private Supplier<T> supplier;

    private LazyLoadDecorator(Supplier<T> supplier) {
        this.supplier = supplier;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (inner == null) {
            synchronized (syncLock) {
                if (inner == null) {
                    inner = load();
                }
            }
        }
        return method.invoke(inner, args);
    }

    protected T load() {
        return supplier.get();
    }

    @SuppressWarnings("unchecked")
    public static <T> T create(Supplier<T> factory, Class<T> clazz) {
        return (T) Proxy.newProxyInstance(LazyLoadDecorator.class.getClassLoader(),
                new Class[] {clazz},
                new LazyLoadDecorator<>(factory));
    }
}

这是另一种基于一次性执行器语义的方法。

包含大量使用示例的完整解决方案可以在 github ( https://github.com/ManasjyotiSharma/java_lazy_init ) 上找到。 这是它的关键:

顾名思义,“一次性执行器”语义具有以下属性:

  1. 包装函数 F 的包装器对象。在当前上下文中,F 是一个函数/lambda 表达式,它包含初始化/反初始化代码。
  2. 包装器提供了一个执行方法,其行为如下:

    • 第一次调用 execute 时调用函数 F 并缓存 F 的输出。
    • 如果 2 个或更多线程调用并发执行,则只有一个“进入”,其他线程阻塞,直到“进入”的一个完成。
    • 对于 execute 的所有其他/未来调用,它不会调用 F 而只是返回先前缓存的输出。
  3. 可以从初始化上下文之外安全地访问缓存的输出。

这也可用于初始化以及非幂等去初始化。

import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;

/**
 * When execute is called, it is guaranteed that the input function will be applied exactly once. 
 * Further it's also guaranteed that execute will return only when the input function was applied
 * by the calling thread or some other thread OR if the calling thread is interrupted.
 */

public class OneTimeExecutor<T, R> {  
  private final Function<T, R> function;
  private final AtomicBoolean preGuard;
  private final CountDownLatch postGuard;
  private final AtomicReference<R> value;

  public OneTimeExecutor(Function<T, R> function) {
    Objects.requireNonNull(function, "function cannot be null");
    this.function = function;
    this.preGuard = new AtomicBoolean(false);
    this.postGuard = new CountDownLatch(1);
    this.value = new AtomicReference<R>();
  }

  public R execute(T input) throws InterruptedException {
    if (preGuard.compareAndSet(false, true)) {
      try {
        value.set(function.apply(input));
      } finally {
        postGuard.countDown();
      }
    } else if (postGuard.getCount() != 0) {
      postGuard.await();
    }
    return value();
  }

  public boolean executed() {
    return (preGuard.get() && postGuard.getCount() == 0);
  }

  public R value() {
    return value.get();
  }

}  

这是一个示例用法:

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;

/*
 * For the sake of this example, assume that creating a PrintWriter is a costly operation and we'd want to lazily initialize it.
 * Further assume that the cleanup/close implementation is non-idempotent. In other words, just like initialization, the 
 * de-initialization should also happen once and only once.
 */
public class NonSingletonSampleB {
  private final OneTimeExecutor<File, PrintWriter> initializer = new OneTimeExecutor<>(
    (File configFile) -> {
      try { 
        FileOutputStream fos = new FileOutputStream(configFile);
        OutputStreamWriter osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8);
        BufferedWriter bw = new BufferedWriter(osw);
        PrintWriter pw = new PrintWriter(bw);
        return pw;
      } catch (IOException e) {
        e.printStackTrace();
        throw new RuntimeException(e);
      }
    }
  );  

  private final OneTimeExecutor<Void, Void> deinitializer = new OneTimeExecutor<>(
    (Void v) -> {
      if (initializer.executed() && null != initializer.value()) {
        initializer.value().close();
      }
      return null;
    }
  );  

  private final File file;

  public NonSingletonSampleB(File file) {
    this.file = file;
  }

  public void doSomething() throws Exception {
    // Create one-and-only-one instance of PrintWriter only when someone calls doSomething().  
    PrintWriter pw = initializer.execute(file);

    // Application logic goes here, say write something to the file using the PrintWriter.
  }

  public void close() throws Exception {
    // non-idempotent close, the de-initialization lambda is invoked only once. 
    deinitializer.execute(null);
  }

}

有关更多示例(例如,单例初始化需要一些仅在运行时可用的数据,因此无法在静态块中实例化),请参阅上面提到的 github 链接。

使用 Java 8,我们可以实现具有线程安全性的延迟初始化。 如果我们有 Holder 类并且它需要一些繁重的资源,那么我们可以像这样延迟加载繁重的资源。

public class Holder {
    private Supplier<Heavy> heavy = () -> createAndCacheHeavy();

    private synchronized Heavy createAndCacheHeavy() {

        class HeavyFactory implements Supplier<Heavy> {
            private final Heavy heavyInstance = new Heavy();

            @Override
            public Heavy get() {
                return heavyInstance;
            }
        }
        if (!HeavyFactory.class.isInstance(heavy)) {
            heavy = new HeavyFactory();
        }
        return heavy.get();
    }

    public Heavy getHeavy() {
        return heavy.get();
    }
}

public class Heavy {
    public Heavy() {
        System.out.println("creating heavy");
    }
}

基于@Alexsalauyou的这个答案,我认为是否有可能实现一个不调用多个实例的解决方案。

原则上,我的解决方案可能会慢一点(非常非常少),但它对处理器和垃圾收集器肯定更友好。

这个想法是你必须首先使用一个容器,它可以保存一个“int”值加上你想要实例化的泛型。

static class Container<T> {
   final int i; 
   final T val; 
   //constructor here
}

出于并发目的,让这个容器的字段是最终的。

LazyInit<T> class 必须具有此容器的 AtomicReference。

AtomicReference<Container<T>> ref;

LazyInit 必须将阶段过程定义为私有 static int 常量:

private static final int NULL_PHASE = -1, CREATING_PHASE = 0, CREATED = 1;

private final Container<T> NULL = new Container<>(NULL_PHASE, null),
CREATING = new Container<>(CREATING_PHASE, null);

NULL 和 CREATING 容器可以制作为 static 并升级到 <?> 以使事情更轻,然后可以使用铸造私有 static 方法来获取它们。

AtomicReference 必须初始化为 NULL:

private final AtomicReference<Container<T>> ref = new AtomicReference<>(getNull());

最后 get() 方法看起来像这样:

@Override 
public T get() { 
    Container<T> prev;
    while ((prev = ref.get()).i < CREATED) {
        if (ref.compareAndSet(getNull(), getCreating())) {
            T res = builder.get();
            ref.set(new Container<>(CREATED, res));
            return res;
        }
    }
    return prev.value;
} 

将代码放在带有一些合适锁的synchronized块中。 还有一些其他高度专业的技术,但除非绝对必要,否则我建议避免使用这些技术。

您还使用了 SHOUTY 案例,它往往表示static但实例方法。 如果它真的是静态的,我建议您确保它不会以任何方式可变。 如果创建静态不可变只是代价高昂,那么类加载无论如何都是惰性的。 您可能希望将其移动到不同的(可能是嵌套的)类,以将创建延迟到绝对的最后一刻。

取决于您尝试实现的目标:

如果希望所有线程共享同一个实例,可以使方法同步。 这将足够

如果你想为每个线程创建一个单独的实例,你应该使用 java.lang.ThreadLocal

尝试定义获取同步实例的方法:

public synchronized Foo getInstance(){
   if(INSTANCE == null){
    INSTANCE = new Foo();
  }

  return INSTANCE;
 }

或者使用一个变量:

private static final String LOCK = "LOCK";
public synchronized Foo getInstance(){
  synchronized(LOCK){
     if(INSTANCE == null){
       INSTANCE = new Foo();
     }
  }
  return INSTANCE;
 }

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM