Thread-safe Singleton in Java using Double Checked Locking Idiom
Singleton Pattern is one of the famous design patterns from the Gang of Four. Even though nowadays it is considered as an anti-pattern, it has served us well in the past. In the Singleton pattern, a class has just one instance throughout its lifetime and that instance is shared between multiple clients. Singleton class has two responsibilities, first to ensure that only instance of the class gets created and second, to provide a method getInstance() so that everyone can get access to that single instance, I mean, global access.
One of the issues, faced by the Singelton design pattern in the multi-threading program is to ensure that just one instance of the class gets created, even if multiple clients called the getInstance() method same time.
Many programmers solved this problem by making the whole getInstance() method synchronized, which results in poor performance because every time a thread enters a synchronization method, it acquires the lock and while it’s been inside the method, no other thread is allowed to enter, even if they are not creating an instance and just accessing already created instance.
How do you solve this problem? Well, you can use Double-checked locking idiom, which you learn in this article. Btw, if you are curious about why Singleton is considered an anti-pattern, I highly recommend the book Games Programming Patterns by Robert Nystrom, though examples are in C++, it’s one of the most readable books on design patterns, you won’t feel bored or cheated.
Threadsafe Singleton using Double Checked Locking Idiom
Double checked locking idiom solves this problem by allowing you to eat your cake and have it as well, it ensures synchronization is used only when an instance of Singleton is created when the getInstance() method is called first time and all other times, the same instance is returned without any synchronization overhead.
As the name suggests, it’s double-checked, which means it checked two times whether _instnace (the singleton instance) is initialized or not, one without synchronization and the other with synchronization.
This double-check ensures that locking is only used when an instance is null i.e. when the first time someone calls getInstance() , all subsequent calls will see _instnace not null hence they will not enter into the synchronization block.
Here is the code of thread-safe singleton pattern using double-checked locking idiom:
/** * Thread Safe Singleton in Java using Double checked locking. * @author WINDOWS 8 * */ public class Singleton < private static volatile Singleton _instance; /** * Double checked locking code on Singleton * @return Singelton instance */ public static Singleton getInstance() < if (_instance == null) < synchronized (Singleton.class) < if (_instance == null) < _instance = new Singleton(); > > > return _instance; > >
How Double Checked Locking Idiom Works
To illustrate the point, how this idiom prevents from two instances being created when two thread simultaneously calls the getInstance() method, let’s see the theory. Suppose, thread T1 calls getInstance() very the first time and sees that _instance is null then it will go inside synchronization block and that point of time it paused.
Now, thread T2 calls getInstance() and it will also see _instance variable null, but it cannot go inside synchronization block because the lock is held by Thread T1, which is inside the synchronization block. Now, thread T1 wake up and creates a new instance of singleton and come out of synchronized block.
After this when thread T2 goes inside synchronized block, it again checks whether _instance is null and this time check fails because _instnace is no more null. So thread T2 comes out of the synchronized block without creating another instance. Further calls to this method return from the first check only.
By the way, double-checked locking idiom was broken before Java 5. It was possible for a thread to see a half initialized instance which will fail the first null check, resulting in returning a half-initialized Singleton. That’s why it’s absolutely critical to make _instnace a volatile variable. The Java memory model updates and happens-before makes double-checked locking works again.
That’s all about how to create thread-safe Singleton in Java, but this is not the only way to create the thread-safe singleton. You can use Enum as Singleton then Java itself will guarantee that only one instance will be created even in the case of multiple threads trying to access it at the same time.
Alternatively, you can also eagerly initialized the Singleton, in that case, it would be initialized on static initializer block at the time of class loading in a thread-safe manner. If you decide to use double-checked locking idiom to make Singleton creation thread-safe, don’t forget the volatile modifier.
Further Reading
If you want to learn more about safe publication techniques in concurrency and other concurrency gotchas, read Java Concurrency in Practice by Brian Goetz, one of the most respected books on Java concurrency.
And, if you want to read more about design patterns, particularly about Singleton patterns with examples from game development, try Game design patterns by Robert Nystrom, one of the interesting books I read this year.
Ещё раз (надеюсь, последний) про double-checked locking
Статей про double-checked locking на Хабре было столько, что казалось бы ещё одна — и Хабр лопнет. Вот только по Java неплохие публикации: Реализация Singleton в JAVA, Правильный Singleton в Java, А как же всё-таки работает многопоточность? Часть II: memory ordering или вот замечательный пост от TheShade (слава web-archive!). В наши дни, наверно, каждый Java-разработчик слышал, что если используешь DCL, будь добр объявить переменную volatile. Найти сегодня в коде известных опенсорсных проектов DCL без volatile довольно трудно, но оказалось, что проблемы ещё не полностью решены. Поэтому я добавлю небольшую заметку по теме с примерами из реальных проектов.
Иногда складывается ощущение, что программисты не включают мозги и не пытаются понять, как что работает, а просто следуют простым и понятным правилам вроде «объяви переменную volatile, используй DCL, и всё будет хорошо». К сожалению, такой подход в программировании не всегда работает.
Особенность DCL-паттерна в том, что момент публикации объекта — это операция volatile-записи, а не выход из секции синхронизации. Поэтому именно volatile-запись должна производиться после полной инициализации объекта.
Вот, к примеру, такой код обнаружился в проекте ICU4J — TZDBTimeZoneNames#prepareFind:
private static volatile TextTrieMap TZDB_NAMES_TRIE = null; private static void prepareFind() < if (TZDB_NAMES_TRIE == null) < synchronized(TZDBTimeZoneNames.class) < if (TZDB_NAMES_TRIE == null) < // loading all names into trie TZDB_NAMES_TRIE = new TextTrieMap(true); Set mzIDs = TimeZoneNamesImpl._getAvailableMetaZoneIDs(); for (String mzID : mzIDs) < . TZDB_NAMES_TRIE.put(std, stdInf); . >> > > >
Разработчик написал volatile, потому что где-то слышал, что так надо, но, видимо, не понял, зачем. По факту публикация объекта TZDB_NAMES_TRIE состоялась в момент volatile-записи: после этого вызовы prepareFind в других потоках будут сразу выходить без синхронизации. При этом после публикации производится множество дополнительных шагов по инициализации.
Данный метод используется при поиске часового пояса, и этот поиск вполне можно сломать. В нормальных условиях new TZDBTimeZoneNames(ULocale.ENGLISH).find(«GMT», 0, EnumSet.allOf(NameType.class)) должен выдавать один результат. Выполним этот код в 1000 потоков:
import java.util.ArrayList; import java.util.EnumSet; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import com.ibm.icu.impl.TZDBTimeZoneNames; import com.ibm.icu.text.TimeZoneNames.NameType; import com.ibm.icu.util.ULocale; public class ICU4JTest < public static void main(String. args) throws InterruptedException < final TZDBTimeZoneNames names = new TZDBTimeZoneNames(ULocale.ENGLISH); final AtomicInteger notFound = new AtomicInteger(); final AtomicInteger found = new AtomicInteger(); Listthreads = new ArrayList<>(); for(int i=0; i >; thread.start(); threads.add(thread); > for(Thread thread : threads) thread.join(); System.out.println("Not found: "+notFound); System.out.println("Found: "+found); > >
Exception in thread "Thread-383" java.util.ConcurrentModificationException at java.util.LinkedList$ListItr.checkForComodification(LinkedList.java:953) at java.util.LinkedList$ListItr.next(LinkedList.java:886) at com.ibm.icu.impl.TextTrieMap$Node.findMatch(TextTrieMap.java:255) at com.ibm.icu.impl.TextTrieMap.find(TextTrieMap.java:100) at com.ibm.icu.impl.TextTrieMap.find(TextTrieMap.java:89) at com.ibm.icu.impl.TZDBTimeZoneNames.find(TZDBTimeZoneNames.java:133) at a.ICU4JTest$1.run(ICU4JTest.java:23) Exception in thread "Thread-447" java.lang.ArrayIndexOutOfBoundsException: 1 at com.ibm.icu.impl.TextTrieMap$Node.matchFollowing(TextTrieMap.java:316) at com.ibm.icu.impl.TextTrieMap$Node.findMatch(TextTrieMap.java:260) at com.ibm.icu.impl.TextTrieMap.find(TextTrieMap.java:100) at com.ibm.icu.impl.TextTrieMap.find(TextTrieMap.java:89) at com.ibm.icu.impl.TZDBTimeZoneNames.find(TZDBTimeZoneNames.java:133) at a.ICU4JTest$1.run(ICU4JTest.java:23) Not found: 430 Found: 568
Почти половина потоков ничего не нашла, пара потоков вообще упала с исключением. Да, в реальном приложении такое маловероятно, но если сценарий с высокой конкуррентностью авторов не интересует, тогда можно вообще обойтись без volatile и DCL.
private final Object myInitLock = new Object(); private volatile JPanel myPanels; private EditorsSplitters mySplitters; private void initUI() < if (myPanels == null) < synchronized (myInitLock) < if (myPanels == null) < myPanels = new JPanel(new BorderLayout()); myPanels.setOpaque(false); myPanels.setBorder(new MyBorder()); mySplitters = new EditorsSplitters(this, myDockManager, true); myPanels.add(mySplitters, BorderLayout.CENTER); >> > > public JComponent getComponent() < initUI(); return myPanels; >public EditorsSplitters getMainSplitters()
Тут авторы даже сделали приватный lock-объект для надёжности, но код всё равно сломан. Конечно, может быть ничего страшного не произойдёт, если начать пользоваться myPanels с неустановленным border, но тут проблема серьёзнее: DCL выполняется на одной переменной (myPanels), а инициализируется две (ещё и mySplitters), причём опять же volatile-запись происходит перед полной инициализацией. В результате getMainSplitters() при конкуррентном доступе вполне может вернуть null.
Исправляются такие вещи очень легко: надо сохранить объект в локальную переменную, с её помощью вызвать все необходимые методы для инициализации, а уже потом записать volatile-поле.
Ещё пара подозрительных мест:
Tomcat DBCP2 BasicDataSource: возможно увидеть объект dataSource, у которого не установлен logWriter.
Apache Wicket Application#getSessionStore(): возможно увидеть sessionStore с незарегистрированным listener’ом.
Здесь вряд ли возможны реальные проблемы, но всё равно стоит писать аккуратнее.
Я добавил небольшую эвристическую проверку в FindBugs, которая предупреждает о таких ситуациях. Однако она может сработать не всегда, поэтому лучше полагайтесь на свою голову.