Quelles sont les approches recommandées pour réaliser thread-safe initialisation paresseuse? Par exemple,
// Not thread-safe
public Foo getInstance(){
if(INSTANCE == null){
INSTANCE = new Foo();
}
return INSTANCE;
}
Pour les singletons, il existe une solution élégante en déléguant la tâche au code JVM pour l'initialisation statique.
public class Something {
private Something() {
}
private static class LazyHolder {
public static final Something INSTANCE = new Something();
}
public static Something getInstance() {
return LazyHolder.INSTANCE;
}
}
voir
http://en.wikipedia.org/wiki/Initialization_on_demand_holder_idiom
et ce blog de Crazy Bob Lee
http://blog.crazybob.org/2007/01/lazy-loading-singletons.html
Si vous utilisez Apache Commons Lang , vous pouvez utiliser l'une des variantes de ConcurrentInitializer comme LazyInitializer .
Exemple:
lazyInitializer = new LazyInitializer<Foo>() {
@Override
protected Foo initialize() throws ConcurrentException {
return new Foo();
}
};
Vous pouvez maintenant obtenir Foo en toute sécurité (initialisé une seule fois):
Foo instance = lazyInitializer.get();
Si vous utilisez la goyave de Google :
Supplier<Foo> fooSupplier = Suppliers.memoize(new Supplier<Foo>() {
public Foo get() {
return new Foo();
}
});
Appelez-le ensuite par Foo f = fooSupplier.get();
De Suppliers.memoize javadoc :
Renvoie un fournisseur qui met en cache l'instance récupérée lors du premier appel à get () et renvoie cette valeur lors des appels suivants à get (). Le fournisseur retourné est thread-safe . La méthode get () du délégué sera invoquée au plus une fois . Si le délégué est une instance créée par un appel antérieur à la mémorisation, elle est retournée directement.
Cela peut être fait sans verrouillage en utilisant AtomicReference
comme détenteur d'instance:
// 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;
}
}
Le principal inconvénient ici est que plusieurs threads peuvent instancier simultanément deux ou plusieurs objets Foo
, et qu'un seul aura la chance d'être configuré, donc si l'instanciation nécessite des E/S ou une autre ressource partagée, cette méthode peut ne pas convenir .
De l'autre côté, cette approche est sans verrouillage et wait-free: si un thread qui est entré en premier dans cette méthode est bloqué, cela n'affectera pas l'exécution des autres.
Le moyen le plus simple consiste à utiliser une classe de support interne statique:
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;
}
C'est ce qu'on appelle la double vérification! Vérifiez cela http://jeremymanson.blogspot.com/2008/05/double-checked-locking.html
Si vous utilisez lombok dans votre projet, vous pouvez utiliser une fonctionnalité décrite ici .
Vous venez de créer un champ, de l'annoter avec @Getter(lazy=true)
et d'ajouter l'initialisation, comme ceci: @Getter(lazy=true) private final Foo instance = new Foo();
Vous devrez référencer le champ uniquement avec getter (voir les notes dans lombok docs ), mais dans la plupart des cas, c'est ce dont nous avons besoin.
Voici une autre approche basée sur la sémantique de l'exécuteur unique.
La solution complète avec un tas d'exemples d'utilisation peut être trouvée sur github ( https://github.com/ManasjyotiSharma/Java_lazy_init ). En voici l'essentiel:
La sémantique "One Time Executor" comme son nom l'indique a les propriétés ci-dessous:
L'encapsuleur fournit une méthode d'exécution qui se comporte comme:
La sortie mise en cache est accessible en toute sécurité depuis l'extérieur du contexte d'initialisation.
Cela peut être utilisé aussi bien pour l'initialisation que pour la désinitialisation non idempotente.
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();
}
}
Voici un exemple d'utilisation:
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);
}
}
Pour quelques exemples supplémentaires (par exemple l'initialisation singleton qui nécessite certaines données disponibles uniquement au moment de l'exécution, donc incapable de les instancier dans un bloc statique), veuillez vous référer au lien github mentionné ci-dessus.
En pensant à l'initialisation paresseuse, je m'attendrais à obtenir un objet "presque réel" qui décore simplement l'objet encore non initialisé.
Lorsque la première méthode est invoquée, l'instance dans l'interface décorée sera initialisée.
* En raison de l'utilisation du proxy, l'objet initié doit implémenter l'interface transmise.
* La différence avec les autres solutions est l'encapsulation de l'initiation à l'utilisation. Vous commencez à travailler directement avec DataSource
comme s'il avait été initialisé. Il sera initialisé à l'invocation de la première méthode.
tilisation:
DataSource ds = LazyLoadDecorator.create(dsSupplier, DataSource.class)
Dans les coulisses:
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));
}
}
Placez le code dans un bloc synchronized
avec un verrou approprié. Il existe d'autres techniques hautement spécialisées, mais je suggère d'éviter celles-ci, sauf si cela est absolument nécessaire.
Vous avez également utilisé la casse SHOUTY, qui tend à indiquer un static
mais une méthode d'instance. S'il est vraiment statique, je vous suggère de vous assurer qu'il n'est en aucun cas modifiable. S'il est juste coûteux de créer une statique immuable, le chargement de classe est de toute façon paresseux. Vous souhaiterez peut-être le déplacer vers une classe différente (éventuellement imbriquée) pour retarder la création au dernier moment possible absolu.
Selon ce que vous essayez de réaliser:
Si vous souhaitez que tous les threads partagent la même instance, vous pouvez synchroniser la méthode. Ce sera suffisant
Si vous souhaitez créer une INSTANCE distincte pour chaque thread, vous devez utiliser Java.lang.ThreadLocal