web-dev-qa-db-ja.com

スレッドセーフな遅延初期化を実装する方法は?

thread-safe遅延初期化を達成するための推奨されるアプローチは何ですか?例えば、

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

    return INSTANCE;
}
42
mre

シングルトンの場合、静的初期化のためにタスクを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

クレイジーボブリーのこのブログ投稿

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

53
Peter Tillemans

Apache Commons Langを使用している場合は、 ConcurrentInitializer のようなバリエーションを使用できます LazyInitializer

例:

_lazyInitializer = new LazyInitializer<Foo>() {

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

Fooを安全に取得できるようになりました(1回だけ初期化されます):

_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()の呼び出しでその値を返すサプライヤを返します。返されるサプライヤはthread-safeです。デリゲートのget()メソッドは、最大で1回呼び出されます。デリゲートが以前のmemoizeの呼び出しによって作成されたインスタンスである場合、それは直接返されます。

47
Kenston Choi

これは、 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;
   }
}

ここでの主な欠点は、複数のスレッドが同時に2つ以上のFooオブジェクトをインスタンス化できることであり、セットアップするのは幸運な1つだけなので、インスタンス化にI/Oまたは別の共有リソースが必要な場合、このメソッドは適切ではない可能性があります。

反対側では、このアプローチはlock-freeおよびwait-free:最初にこのメソッドに入ったスレッドがスタックしても、他のスレッドの実行には影響しません。

28
Alex Salauyou

最も簡単な方法は、静的な内部ホルダークラスを使用することです。

public class Singleton {

    private Singleton() {
    }

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

    private static class Holder {
        private static final Singleton INSTANCE = new Singleton();
    }
}
9
JB Nizet
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

3
narek.gevorgyan

プロジェクトでlombokを使用する場合、 here で説明されている機能を使用できます。

次のように、フィールドを作成し、@Getter(lazy=true)で注釈を付けて初期化を追加するだけです:@Getter(lazy=true) private final Foo instance = new Foo();

ゲッターでのみフィールドを参照する必要があります(lombok docs の注を参照)が、ほとんどの場合、それが必要です。

3
int21h

ワンタイムエグゼキュータセマンティクスに基づくもう1つのアプローチを次に示します。

多数の使用例を含む完全なソリューションは、githubにあります( https://github.com/ManasjyotiSharma/Java_lazy_init )。その要点は次のとおりです。

名前が示す「ワンタイムエグゼキューター」セマンティックには、以下のプロパティがあります。

  1. 関数Fをラップするラッパーオブジェクト。現在のコンテキストFは、初期化/初期化解除コードを保持する関数/ラムダ式です。
  2. ラッパーは、次のように動作する実行メソッドを提供します。

    • Executeが初めて呼び出されたときに関数Fを呼び出し、Fの出力をキャッシュします。
    • 2つ以上のスレッドが同時に実行される場合、1つだけが「取得」され、他のスレッドは「取得」されるまでブロックされます。
    • 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リンクを参照してください。

1

遅延初期化について考えると、まだ初期化されていないオブジェクトを装飾するだけの「ほぼ実際の」オブジェクトが得られると思います。

最初のメソッドが呼び出されると、装飾されたインターフェイス内のインスタンスが初期化されます。

*プロキシの使用のため、開始されたオブジェクトは渡されたインターフェイスを実装する必要があります。

*他のソリューションとの違いは、使用からの開始のカプセル化です。初期化されたかのように、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));
    }
}
1

適切なロックを使用して、コードをsynchronizedブロックに入れます。他にも高度に専門的な手法がいくつかありますが、どうしても必要な場合を除き、それらを避けることをお勧めします。

また、staticではなくインスタンスメソッドを示す傾向があるSHOUTYケースを使用しました。それが本当に静的なものである場合は、どのような方法でも変更できないことを確認することをお勧めします。静的な不変の作成が高価な場合、クラスの読み込みはとにかく怠laです。それを別の(ネストされている可能性のある)クラスに移動して、可能な限り最後まで作成を遅らせることができます。

あなたが達成しようとするものに応じて:

すべてのスレッドで同じインスタンスを共有する場合、メソッドを同期させることができます。これで十分です

スレッドごとに個別のインスタンスを作成する場合は、Java.lang.ThreadLocalを使用する必要があります。

0
WeMakeSoftware