もう一度(できれば最後の)ダブルチェックロックについて

Habréでのダブルチェックロックに関する記事が非常に多かったので、もう1つと思われます。Habrは破裂します。 Javaに関する優れた出版物をいくつか紹介します。Javaでのシングルトンの 実装 、Javaでのシングルトンの修正マルチスレッドはどのように機能しますか? パートII:メモリの注文またはここはTheShadeからの素晴らしい投稿ですglory web-archive! )。 最近では、おそらくすべてのJava開発者が、DCLを使用する場合は、変数volatileを宣言するように親切になると聞いています。 今日よく知られているオープンソースプロジェクトのコードで揮発性のないDCLを見つけることは非常に難しいですが、問題はまだ完全に解決されていないことが判明しました。 したがって、実際のプロジェクトの例とともに、トピックに関する小さなメモを追加します。



プログラマは頭をオンにせず、それがどのように機能するかを理解しようとせず、「揮発性変数を宣言し、DCLを使用すると、すべてがうまくいく」などの単純で理解しやすいルールに従うだけの感覚を感じることがあります。 残念ながら、プログラミングへのこのアプローチは常に機能するとは限りません。



DCLパターンの特徴は、オブジェクトが公開される瞬間が揮発性の書き込み操作であり、同期セクションの終了ではないことです。 したがって、オブジェクトの初期化が完了した後にvolatile-recordを作成する必要があります。



ここで、たとえば、そのようなコードはICU4Jプロジェクトで見つかりました-TZDBTimeZoneNames#prepareFind

private static volatile TextTrieMap<TZDBNameInfo> 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<TZDBNameInfo>(true); Set<String> mzIDs = TimeZoneNamesImpl._getAvailableMetaZoneIDs(); for (String mzID : mzIDs) { ... TZDB_NAMES_TRIE.put(std, stdInf); ... } } } } }
      
      





開発者はvolatileを作成しました。どこかでそれが必要だと聞いたのに、どうしてか理解できなかったからです。 実際、TZDB_NAMES_TRIEオブジェクトの公開は、揮発性レコードの時点で行われました。この後、他のスレッドのprepareFind呼び出しは、同期せずにすぐに終了します。 さらに、公開後、初期化するために多くの追加手順が実行されます。



この方法は、タイムゾーンを検索するときに使用され、この検索は壊れる可能性があります。 通常の状態では、 new TZDBTimeZoneNames(ULocale.ENGLISH).find("GMT", 0, EnumSet.allOf(NameType.class))



は1つの結果を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(); List<Thread> threads = new ArrayList<>(); for(int i=0; i<1000; i++) { Thread thread = new Thread() { @Override public void run() { int resultSize = names.find("GMT", 0, EnumSet.allOf(NameType.class)).size(); if(resultSize == 0) notFound.incrementAndGet(); else if(resultSize == 1) found.incrementAndGet(); else throw new AssertionError(); } }; 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をまったく使用しなくてもかまいません。



IntelliJ IDEAの別の例-FileEditorManagerImpl#initUI

  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() { initUI(); return mySplitters; }
      
      





ここでは、作成者は信頼性のためにプライベートロックオブジェクトを作成しましたが、コードはまだ壊れています。 もちろん、境界線を設定せずにmyPanelsを使用し始めても、何も悪いことは起こらないかもしれませんが、問題はより深刻です:DCLは1つの変数(myPanels)で実行され、2つが初期化され(mySplitters)、再び完全な初期化の前に揮発性記録が発生します。 その結果、競合するアクセス権を持つgetMainSplitters()はnullを返す可能性があります。



これらの問題は非常に簡単に修正されます。オブジェクトをローカル変数に保存し、それを使用して初期化に必要なすべてのメソッドを呼び出してから、volatileフィールドを書き込む必要があります。



さらに不審な場所:

Tomcat DBCP2 BasicDataSource :logWriterがインストールされていないdataSourceオブジェクトを見ることができます。

Apache Wicket Application#getSessionStore() :未登録のリスナーでsessionStoreを表示できます。



実際の問題が発生する可能性は低いですが、さらに正確に記述する必要があります。



FindBugsにこのような状況を警告するヒューリスティックチェックを少し追加しました 。 ただし、常に機能するとは限りませんので、頭に頼ってください。



All Articles