I felt the need for lazy initialization in custom appender and started to look at options.
In this blog i will share things that i tried.
One of the thing that came to my mind was Singleton approach but now it is known fact that singleton causes problem with testing and make it impossible to extend it, so approach of mixing concurrency & object construction is not that good.
Incase if singleton is required then it is better to use Dependency Injection framework rather than spoiling your application code.
Lets get back to lazy initialization/eval.
Some programming language like scala/swift etc has support for lazy, so no custom code is required to do this but in java space we still have to write thread safe code to get it right.
Lets look at some options we have in java and what type of performance we get.
- Brute force using Synchronized
This is the most simple and inefficient one, scala is using this approach. Scala one is available @ ScalaLazy.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class SingleLock<V> implements Lazy<V> { | |
private Callable<V> codeBlock; | |
private V value; | |
public SingleLock(Callable<V> codeBlock) { | |
this.codeBlock = codeBlock; | |
} | |
@Override | |
public synchronized V get() { | |
if (value == null) { | |
setValue(); | |
} | |
return value; | |
} | |
private void setValue() { | |
try { | |
value = codeBlock.call(); | |
} catch (Exception e) { | |
throw new RuntimeException(e); | |
} | |
} | |
} |
- Double lock
This is little complex to write and gives good performance.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class DoubleLock<V> implements Lazy<V> { | |
private Callable<V> codeBlock; | |
private V value; | |
private volatile boolean loaded; | |
public DoubleLock(Callable<V> codeBlock) { | |
this.codeBlock = codeBlock; | |
} | |
@Override | |
public V get() { | |
if (!loaded) { | |
synchronized (this) { | |
if (!loaded) { | |
setValue(); | |
loaded = true; | |
} | |
} | |
} | |
return value; | |
} | |
private void setValue() { | |
try { | |
value = codeBlock.call(); | |
} catch (Exception e) { | |
throw new RuntimeException(e); | |
} | |
} | |
} |
This approach is simple to write and gives good performance.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class LazyFutureTask<V> implements Lazy<V> { | |
private final FutureTask<V> futureTask; | |
public LazyFutureTask(Callable<V> codeBlock) { | |
this.futureTask = new FutureTask<>(codeBlock); | |
} | |
@Override | |
public V get() { | |
futureTask.run(); | |
return getValue(); | |
} | |
private V getValue() { | |
try { | |
return futureTask.get(); | |
} catch (Exception e) { | |
throw new RuntimeException(e); | |
} | |
} | |
} |
Double lock approach gives the best performance and brute force one is worst. I did quick bench mark for 1 Million calls using different number of thread.
Single lock performance is very bad, lets have look at the number by removing single lock to see how Double Lock & Future Task performed.
These benchmark are done very quickly but detailed benchmark numbers should be close.
Code for this blog post is available @ github
|