クラス初期化によるデッドロック

11843 ワード

1.デッドロックはどのように発生したのか
クラスの初期化は隠れた操作であり、仮想マシンが主導して完成している.開発者がクラスのロードメカニズムを理解していないと、クラスの初期化が何なのか分からないかもしれない.クラスの初期化については、Java仮想マシンクラスのロードメカニズムを参照してください.詳細は説明されています.クラスの初期化にはいくつかの重要な機能があります.
  • クラスの初期化プロセスは、実際にはクラスコンストラクタメソッド()を実行するプロセスである.
  • サブクラスの初期化が完了すると、仮想機会はその親クラスの初期化が完了することを保証する.
  • マルチスレッド環境では、仮想マシンが()メソッドを実行すると自動的にロックされます.

  • Javaでは、デッドロックはマルチスレッド環境で発生するに違いありません.複数のスレッドが同時に互いに持つリソースが必要で、自分のリソースが解放されず、他の人のリソースが得られず、循環依存をもたらし、さらにそこにブロックされ続け、デッドロックになる.
    2.デッドロックが発生した場合
    2.1クラス初期化相互依存
    最も明らかなのは、2つのクラスが異なるスレッドで初期化され、互いに依存していることです.例を見てみましょう.
    public class Test { 
    
        public static class A {
    
            static {
                System.out.println("class A init.");
                B b = new B();
            }   
            
            public static void test() {
                System.out.println("method test called in class A");
            }
        }
        
        public static class B {
            
            static {
                System.out.println("class B init.");
                A a = new A();
            }
            
            public static void test() {
                System.out.println("method test called in class B");
            }
        }
        
        public static void main(String[] args) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    A.test();
                }       
            }).start();
                
            new Thread(new Runnable() {
                @Override
                public void run() {
                    B.test();
                }       
            }).start();
        }   
    }
    

    実行結果は次のとおりです.
    class A init.
    class B init.
    
    

    1番目のスレッドがA.test()を実行すると、クラスAが初期化され、そのスレッドがA.classのロックを取得し、2番目のスレッドがB.test()を実行すると、クラスBが初期化され、そのスレッドがB.classのロックを取得する.Aが初期化中にコードB b=new B()を実行すると、クラスBはまだ初期化が完了していないことが判明し、クラスB.classのロックを取得しようとする.クラスBは初期化時にコードA a=new A()を実行し、クラスAも初期化が完了していないことを発見し、クラスA.classのロックを取得しようとしたが、A.classロックが占有されているため、スレッドがブロックされ、ロックの解放を待つ.同じように最初のスレッドがブロックされ、B.classロックの解放を待つと、ループ依存性が発生し、デッドロックが形成される.
    上のコードを次のように実行すると、どのような結果になりますか?
    public static void main(String[] args) {
        A.test();
        B.test();
    }
    

    一見、Aが初期化されるとBに依存し、Bが初期化されるとAに依存し、デッドロックを起こすようですが、実際にはそうではありません.A、Bの2つのクラスの初期化はすべて同じスレッドで実行され、Aを初期化すると、そのスレッドはA.classロックを取得し、Bを初期化するとB.classロックを取得し、Bを初期化する際にはAを必要とするが、この2つの初期化はいずれも同じスレッドで実行され、このスレッドは同時にこの2つのロックを取得するので、ロックリソースのプリエンプトは発生しない.最終的な実行結果は次のとおりです.
    class A init.
    class B init.
    method test called in class A
    method test called in class B
    

    2.2子、親初期化デッドロック
    1つ目のケースに比べて、このような状況によるデッドロックはより隠れていますが、実質的には同じ原因で、具体的な例を見てみましょう.
    public class Test { 
    
        public static class Parent {
            static {
                System.out.println("Parent init.");
            }
    
            public static final Parent EMPTY = new Child();
            
            public static void test() {
                System.out.println("test called in class Parent.");
            }
            
        }
        
        public static class Child extends Parent {      
            static {
                System.out.println("Child init.");
            }
        }
        
        public static void main(String[] args) {
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    Child c = new Child();
                }       
            });
            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    Parent.test();
                }       
            });
            t1.start();
            t2.start();
        }   
    }
    

    実行結果:
    Parent init.
    

    デッドロックの原因を分析します.1.スレッドt 1が実行されるとChildクラスの初期化がトリガーされ、スレッドt 2が実行されるとParentクラスの初期化がトリガーされる.2.スレッドt 1に続くChildを持つ.classロック、t 2はParentを持つ.classロックは、t 1の初期化には親パラメータを初期化する必要がありますが、クラスパラメータには「public static final Parent EMPTY=new Child();」という定数定義があります.このようなパラメータは初期化時にChildを初期化する必要がある.3.このようにスレッドt 1がParentを初期化する、Parentを取得しようとする.classロック、スレッドt 2はChildを初期化し、Childを取得しようとする.classロックは、互いにリソースを解放できないため、デッドロックをもたらします.
    3.デッドロックによる血液検査
    かつて開発されたAndroidプロジェクトでは、オープンソースのORMデータベースフレームワークlitepalを採用してデータベース操作を行った結果、アプリケーションがオンラインになった後、時々カード死が発生するというユーザーのフィードバックがしばしばあった.その後、自分のテストを経て、偶発的にカードが死ぬ現象もありますが、少しも規則がなく、バグの位置を特定することができず、ユーザーから苦情を受けて罵倒されたのは惨めで、開発者を焦らせました.その後、携帯電話のanrファイルをエクスポートし、よく分析したところ、litepalデータベースにデッドロックが発生したため、anrが現れたことが分かった.(注:litepal自体は使いやすいAndroid ORMデータベースフレームワークで、ほとんどの場合使いやすいですが、ここでは私たちの使用シーンを説明します.)
    "main" tid=1 :
      | group="main" sCount=1 dsCount=0 obj=0x757e6598 self=0xab361100
      | sysTid=17006 nice=0 cgrp=default sched=0/0 handle=0xf7210b50
      | state=S schedstat=( 731900052 38102591 941 ) utm=53 stm=20 core=6 HZ=100
      | stack=0xff0dc000-0xff0de000 stackSize=8MB
      | held mutexes=
      at org.litepal.crud.DataSupport.findFirst(DataSupport.java:-1)
      - waiting to lock <0x005e5028> (a java.lang.Class) held by thread 27
      at ......
    
    
    "RxCachedThreadScheduler-2" tid=27 :
      | group="main" sCount=1 dsCount=0 obj=0x12e751c0 self=0xab9ae8a8
      | sysTid=17097 nice=0 cgrp=default sched=0/0 handle=0xdbb46930
      | state=S schedstat=( 548637659 14253750 564 ) utm=50 stm=4 core=3 HZ=100
      | stack=0xdba44000-0xdba46000 stackSize=1038KB
      | held mutexes=
      kernel: (couldn't read /proc/self/task/17097/stack)
      native: #00 pc 00016998  /system/lib/libc.so (syscall+28)
      native: #01 pc 000f5e73  /system/lib/libart.so (_ZN3art17ConditionVariable4WaitEPNS_6ThreadE+82)
      native: #02 pc 002ae8b3  /system/lib/libart.so (_ZN3art7Monitor4LockEPNS_6ThreadE+394)
      native: #03 pc 002b140f  /system/lib/libart.so (_ZN3art7Monitor12MonitorEnterEPNS_6ThreadEPNS_6mirror6ObjectE+266)
      native: #04 pc 002e5747  /system/lib/libart.so (_ZN3art10ObjectLockINS_6mirror6ObjectEEC2EPNS_6ThreadENS_6HandleIS2_EE+22)
      native: #05 pc 00139bab  /system/lib/libart.so (_ZN3art11ClassLinker15InitializeClassEPNS_6ThreadENS_6HandleINS_6mirror5ClassEEEbb.part.593+90)
      native: #06 pc 0013aa97  /system/lib/libart.so (_ZN3art11ClassLinker17EnsureInitializedEPNS_6ThreadENS_6HandleINS_6mirror5ClassEEEbb+82)
      native: #07 pc 002bd76d  /system/lib/libart.so (_ZN3artL18Class_classForNameEP7_JNIEnvP7_jclassP8_jstringhP8_jobject+292)
      native: #08 pc 0024eca9  /system/framework/arm/boot.oat (Java_java_lang_Class_classForName__Ljava_lang_String_2ZLjava_lang_ClassLoader_2+132)
      at java.lang.Class.classForName!(Native method)
      - waiting to lock <0x0229fe4b> (a java.lang.Class<......database.announcementinfo>) held by thread 36
      at java.lang.Class.forName(Class.java:324)
      at java.lang.Class.forName(Class.java:285)
    
    
    "RxCachedThreadScheduler-4" tid=36 :
      | group="main" sCount=1 dsCount=0 obj=0x12c3ce80 self=0xab8ab088
      | sysTid=17229 nice=0 cgrp=default sched=0/0 handle=0xdab2b930
      | state=S schedstat=( 56642965 8922138 61 ) utm=4 stm=1 core=6 HZ=100
      | stack=0xdaa29000-0xdaa2b000 stackSize=1038KB
      | held mutexes=
      kernel: (couldn't read /proc/self/task/17229/stack)
      native: #00 pc 00016998  /system/lib/libc.so (syscall+28)
      native: #01 pc 000f5e73  /system/lib/libart.so (_ZN3art17ConditionVariable4WaitEPNS_6ThreadE+82)
      native: #02 pc 002ae8b3  /system/lib/libart.so (_ZN3art7Monitor4LockEPNS_6ThreadE+394)
      native: #03 pc 002b140f  /system/lib/libart.so (_ZN3art7Monitor12MonitorEnterEPNS_6ThreadEPNS_6mirror6ObjectE+266)
      native: #04 pc 002e5747  /system/lib/libart.so (_ZN3art10ObjectLockINS_6mirror6ObjectEEC2EPNS_6ThreadENS_6HandleIS2_EE+22)
      native: #05 pc 00139165  /system/lib/libart.so (_ZN3art11ClassLinker11VerifyClassEPNS_6ThreadENS_6HandleINS_6mirror5ClassEEE+336)
      native: #06 pc 00139c0d  /system/lib/libart.so (_ZN3art11ClassLinker15InitializeClassEPNS_6ThreadENS_6HandleINS_6mirror5ClassEEEbb.part.593+188)
      native: #07 pc 0013aa97  /system/lib/libart.so (_ZN3art11ClassLinker17EnsureInitializedEPNS_6ThreadENS_6HandleINS_6mirror5ClassEEEbb+82)
      native: #08 pc 002cdb8b  /system/lib/libart.so (_ZN3artL23Constructor_newInstanceEP7_JNIEnvP8_jobjectP13_jobjectArray+134)
      native: #09 pc 0024f0cd  /system/framework/arm/boot.oat (Java_java_lang_reflect_Constructor_newInstance___3Ljava_lang_Object_2+96)
      at java.lang.reflect.Constructor.newInstance!(Native method)
      - waiting to lock <0x005e5028> (a java.lang.Class) held by thread 27
      at com.google.gson.internal.ConstructorConstructor$3.construct(ConstructorConstructor.java:-1)
      at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:-1)
      at com.google.gson.internal.bind.TypeAdapterRuntimeTypeWrapper.read(TypeAdapterRuntimeTypeWrapper.java:-1)
      at com.google.gson.internal.bind.CollectionTypeAdapterFactory$Adapter.read(CollectionTypeAdapterFactory.java:-1)
      at com.google.gson.internal.bind.CollectionTypeAdapterFactory$Adapter.read(CollectionTypeAdapterFactory.java:-1)
      at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.read(ReflectiveTypeAdapterFactory.java:-1)
      at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:-1)
      at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.read(ReflectiveTypeAdapterFactory.java:-1)
      at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:-1)
      at com.google.gson.Gson.fromJson(Gson.java:-1)
      at com.google.gson.Gson.fromJson(Gson.java:-1)
      at com.google.gson.Gson.fromJson(Gson.java:-1)
    
    

    ここではanrファイルの を り りました. から かるように、スレッドt 1はDataSupportを している.findFirst()メソッドの 、DataSupportが です.Classロック、DataSupport.classロックはスレッドt 27によって されるため、t 1はずっとブロックされており、t 1はメインスレッドであるため、メインスレッドがブロックされるためanr が する.スレッドt 27を てみると、AnnouncementInfoが です.classロックは、スレッドt 36によって される. にスレッドt 36を ると、DataSupportロックが であることが かった.ここを ると、 にデッドロックが していることがわかります.
    DataSupportはlitepalフレームワークで されたデータベース ベースクラスです.AnnouncementInfoは たちが したデータテーブルクラスです.DataSupportクラスから する があります. を てみましょう.
    //     AnnouncementInfo    
    public class AnnouncementInfo extends DataSupport {
        //       
    }
    

    DataSupportのfindFirst()メソッドの :
    public static synchronized  T findFirst(Class modelClass);    
    

    たちのアプリケーションでは、データベースを する に び し を するいくつかの なるデータテーブルが されています.AnnouncementInfoデータテーブルのクエリの として、 は のように かれています.
    AnnouncementInfo data = DataSupport.findFirst(AnnouncementInfo.class);
    

    このように するのは ありませんが、データベーステーブルを で し、 のサブスレッドでAnnouncementInfoクラスを すると、 が します.メインスレッドはDataSupportを する.findFirstメソッドの 、DataSupportクラスが されていないことが した は、まずDataSupportの を みる.classロックは、ロックが された にのみ されます.2.あるサブスレッドがデータベース にDataSupportクラスの をトリガし、 にAnnouncementInfoクラスに していることが するが、AnnouncementInfoクラスはこの で されていないため、AnnouncementInfoを しようとする.クラスロックはクラスを します.3. に、あるサブスレッドがGsonライブラリ jsonデータを いてAnnouncementInfoオブジェクトインスタンスを すると、AnnouncementInfoクラスの がトリガーされるが、AnnouncementInfoクラスの には のDataSupportを する があり、2 のステップでDataSupportクラスの がブロックされている.これによりサイクル が じ, スレッドのブロックがanrを き こす.
    4.デッドロック
    の では、クラスの にデッドロックが したことを っています.サブクラスは に し、 は にサブクラスに するため、このような を するために、データベース クラスを にプライマリ・スレッドですべて する を しました.アプリケーションの り で、 のように しました.
    Class c1 = Class.forName("AnnouncementInfo");
    Class c2 = Class.forName("......");
    ......
    

    これにより、アプリケーションが すると、すべてのデータベース クラスが され、データベースを で すると、 のデッドロックは と しません.
    5.まとめ
    に、コードにデッドロックが すると、 にマルチスレッド では、 に が です.しかし,デッドロックの な を すれば, の ではほぼ できる.
    JAvaクラスロードメカニズムシリーズの :
  • Java マシンクラスロードメカニズム
  • Java Classファイル
  • によるデッドロック
  • Javaクラスローダ