مثال على انتهاك التعليق في Java

الوضع الميت في جافا هو حالة برمجية حيث يتم حجز اثنين أو أكثر من الخيوط إلى الأبد. تنشأ حالة الوضع الميت في جافا عند وجود خيطين على الأقل ومصادر اثنين أو أكثر. هنا قمت بكتابة برنامج بسيط سيتسبب في حدوث حالة الوضع الميت في جافا وبعد ذلك سنرى كيفية تحليلها.

الوضع الميت في جافا

لنلقي نظرة على برنامج بسيط حيث سأقوم بإنشاء حالة وضع ميت في خيوط جافا.

package com.journaldev.threads;

public class ThreadDeadlock {

    public static void main(String[] args) throws InterruptedException {
        Object obj1 = new Object();
        Object obj2 = new Object();
        Object obj3 = new Object();
    
        Thread t1 = new Thread(new SyncThread(obj1, obj2), "t1");
        Thread t2 = new Thread(new SyncThread(obj2, obj3), "t2");
        Thread t3 = new Thread(new SyncThread(obj3, obj1), "t3");
        
        t1.start();
        Thread.sleep(5000);
        t2.start();
        Thread.sleep(5000);
        t3.start();
        
    }

}

class SyncThread implements Runnable{
    private Object obj1;
    private Object obj2;

    public SyncThread(Object o1, Object o2){
        this.obj1=o1;
        this.obj2=o2;
    }
    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        System.out.println(name + " acquiring lock on "+obj1);
        synchronized (obj1) {
         System.out.println(name + " acquired lock on "+obj1);
         work();
         System.out.println(name + " acquiring lock on "+obj2);
         synchronized (obj2) {
            System.out.println(name + " acquired lock on "+obj2);
            work();
        }
         System.out.println(name + " released lock on "+obj2);
        }
        System.out.println(name + " released lock on "+obj1);
        System.out.println(name + " finished execution.");
    }
    private void work() {
        try {
            Thread.sleep(30000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

في البرنامج أعلاه ، يقوم SyncThread بتنفيذ واجهة Runnable ويعمل على كائنين عن طريق الحصول على قفل على كل واحد منهما على التوالي باستخدام كتلة متزامنة. في الطريقة الرئيسية ، لدي ثلاثة خيوط تعمل لـ SyncThread وهناك مورد مشترك بين كل من الخيوط. يتم تشغيل الخيوط بطريقة تتيح لها الحصول على قفل على الكائن الأول ولكن عندما تحاول الحصول على قفل على الكائن الثاني ، فإنها تدخل حالة الانتظار لأنه قد تم بالفعل قفله بواسطة خيط آخر. يشكل هذا تبعية دورية للمورد بين الخيوط مما يتسبب في حالة الوضع الميت. عند تنفيذ البرنامج أعلاه ، يتم إنشاء الناتج التالي ولكن البرنامج لا ينتهي أبدًا بسبب حالة الوضع الميت في خيوط جافا.

t1 acquiring lock on java.lang.Object@6d9dd520
t1 acquired lock on java.lang.Object@6d9dd520
t2 acquiring lock on java.lang.Object@22aed3a5
t2 acquired lock on java.lang.Object@22aed3a5
t3 acquiring lock on java.lang.Object@218c2661
t3 acquired lock on java.lang.Object@218c2661
t1 acquiring lock on java.lang.Object@22aed3a5
t2 acquiring lock on java.lang.Object@218c2661
t3 acquiring lock on java.lang.Object@6d9dd520

هنا يمكننا تحديد حالة الوضع الميت بوضوح من الناتج ولكن في تطبيقات الحياة الواقعية من الصعب جدًا العثور على حالة الوضع الميت وتصحيحها.

كيفية اكتشاف العالقة في جافا

لكشف العالقة في جافا ، نحتاج إلى النظر في تفريغ موضوع جافا للتطبيق ، في المشاركة السابقة شرحت كيف يمكننا توليد تفريغ الموضوع باستخدام VisualVM profiler أو باستخدام أداة jstack. هنا تفريغ الموضوع للبرنامج أعلاه.

2012-12-27 19:08:34
Full thread dump Java HotSpot(TM) 64-Bit Server VM (23.5-b02 mixed mode):

"Attach Listener" daemon prio=5 tid=0x00007fb0a2814000 nid=0x4007 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"DestroyJavaVM" prio=5 tid=0x00007fb0a2801000 nid=0x1703 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"t3" prio=5 tid=0x00007fb0a204b000 nid=0x4d07 waiting for monitor entry [0x000000015d971000]
   java.lang.Thread.State: BLOCKED (on object monitor)
	at com.journaldev.threads.SyncThread.run(ThreadDeadlock.java:41)
	- waiting to lock <0x000000013df2f658> (a java.lang.Object)
	- locked <0x000000013df2f678> (a java.lang.Object)
	at java.lang.Thread.run(Thread.java:722)

"t2" prio=5 tid=0x00007fb0a1073000 nid=0x4207 waiting for monitor entry [0x000000015d209000]
   java.lang.Thread.State: BLOCKED (on object monitor)
	at com.journaldev.threads.SyncThread.run(ThreadDeadlock.java:41)
	- waiting to lock <0x000000013df2f678> (a java.lang.Object)
	- locked <0x000000013df2f668> (a java.lang.Object)
	at java.lang.Thread.run(Thread.java:722)

"t1" prio=5 tid=0x00007fb0a1072000 nid=0x5503 waiting for monitor entry [0x000000015d86e000]
   java.lang.Thread.State: BLOCKED (on object monitor)
	at com.journaldev.threads.SyncThread.run(ThreadDeadlock.java:41)
	- waiting to lock <0x000000013df2f668> (a java.lang.Object)
	- locked <0x000000013df2f658> (a java.lang.Object)
	at java.lang.Thread.run(Thread.java:722)

"Service Thread" daemon prio=5 tid=0x00007fb0a1038000 nid=0x5303 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread1" daemon prio=5 tid=0x00007fb0a1037000 nid=0x5203 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread0" daemon prio=5 tid=0x00007fb0a1016000 nid=0x5103 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Signal Dispatcher" daemon prio=5 tid=0x00007fb0a4003000 nid=0x5003 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Finalizer" daemon prio=5 tid=0x00007fb0a4800000 nid=0x3f03 in Object.wait() [0x000000015d0c0000]
   java.lang.Thread.State: WAITING (on object monitor)
	at java.lang.Object.wait(Native Method)
	- waiting on <0x000000013de75798> (a java.lang.ref.ReferenceQueue$Lock)
	at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:135)
	- locked <0x000000013de75798> (a java.lang.ref.ReferenceQueue$Lock)
	at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:151)
	at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:177)

"Reference Handler" daemon prio=5 tid=0x00007fb0a4002000 nid=0x3e03 in Object.wait() [0x000000015cfbd000]
   java.lang.Thread.State: WAITING (on object monitor)
	at java.lang.Object.wait(Native Method)
	- waiting on <0x000000013de75320> (a java.lang.ref.Reference$Lock)
	at java.lang.Object.wait(Object.java:503)
	at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:133)
	- locked <0x000000013de75320> (a java.lang.ref.Reference$Lock)

"VM Thread" prio=5 tid=0x00007fb0a2049800 nid=0x3d03 runnable 

"GC task thread#0 (ParallelGC)" prio=5 tid=0x00007fb0a300d800 nid=0x3503 runnable 

"GC task thread#1 (ParallelGC)" prio=5 tid=0x00007fb0a2001800 nid=0x3603 runnable 

"GC task thread#2 (ParallelGC)" prio=5 tid=0x00007fb0a2003800 nid=0x3703 runnable 

"GC task thread#3 (ParallelGC)" prio=5 tid=0x00007fb0a2004000 nid=0x3803 runnable 

"GC task thread#4 (ParallelGC)" prio=5 tid=0x00007fb0a2005000 nid=0x3903 runnable 

"GC task thread#5 (ParallelGC)" prio=5 tid=0x00007fb0a2005800 nid=0x3a03 runnable 

"GC task thread#6 (ParallelGC)" prio=5 tid=0x00007fb0a2006000 nid=0x3b03 runnable 

"GC task thread#7 (ParallelGC)" prio=5 tid=0x00007fb0a2006800 nid=0x3c03 runnable 

"VM Periodic Task Thread" prio=5 tid=0x00007fb0a1015000 nid=0x5403 waiting on condition 

JNI global references: 114


Found one Java-level deadlock:
=============================
"t3":
  waiting to lock monitor 0x00007fb0a1074b08 (object 0x000000013df2f658, a java.lang.Object),
  which is held by "t1"
"t1":
  waiting to lock monitor 0x00007fb0a1010f08 (object 0x000000013df2f668, a java.lang.Object),
  which is held by "t2"
"t2":
  waiting to lock monitor 0x00007fb0a1012360 (object 0x000000013df2f678, a java.lang.Object),
  which is held by "t3"

Java stack information for the threads listed above:
===================================================
"t3":
	at com.journaldev.threads.SyncThread.run(ThreadDeadlock.java:41)
	- waiting to lock <0x000000013df2f658> (a java.lang.Object)
	- locked <0x000000013df2f678> (a java.lang.Object)
	at java.lang.Thread.run(Thread.java:722)
"t1":
	at com.journaldev.threads.SyncThread.run(ThreadDeadlock.java:41)
	- waiting to lock <0x000000013df2f668> (a java.lang.Object)
	- locked <0x000000013df2f658> (a java.lang.Object)
	at java.lang.Thread.run(Thread.java:722)
"t2":
	at com.journaldev.threads.SyncThread.run(ThreadDeadlock.java:41)
	- waiting to lock <0x000000013df2f678> (a java.lang.Object)
	- locked <0x000000013df2f668> (a java.lang.Object)
	at java.lang.Thread.run(Thread.java:722)

Found 1 deadlock.

يظهر إخراج تفريغ الموضوع بوضوح الوضع المحتجز والمواضيع والموارد المشاركة التي تتسبب في الوضع المحتجز. لتحليل العالقة ، نحتاج إلى البحث عن المواضيع بحالة BLOCKED ثم الموارد التي تنتظر القفل عليها. كل مورد لديه معرف فريد يمكننا من خلاله معرفة الموضوع الذي يحمل القفل على الكائن. على سبيل المثال ، تُعلق الموضوع “t3” لقفل 0x000000013df2f658 ولكنها محتجزة بالفعل بواسطة الموضوع “t1”. بمجرد تحليل الوضع المحتجز واكتشاف المواضيع التي تسبب العالقة ، نحتاج إلى إجراء تغييرات في الكود لتجنب العالقة.

كيفية تجنب العالقة في جافا

هذه بعض الإرشادات التي يمكننا استخدامها لتجنب معظم حالات العالقة.

  • تجنب الأقفال المتداخلة: هذا هو السبب الأكثر شيوعًا لحدوث العراقيل، تجنب قفل مورد آخر إذا كنت تحتفظ بقفل واحد بالفعل. من الصعب تقريبًا حدوث حالة العراقيل إذا كنت تعمل مع قفل كائن واحد فقط. على سبيل المثال ، هنا تنفيذ آخر لطريقة run() بدون قفل متداخل والبرنامج يعمل بنجاح بدون حدوث حالة العراقيل.

        public void run() {
            String name = Thread.currentThread().getName();
            System.out.println(name + " يقوم بالحصول على قفل على " + obj1);
            synchronized (obj1) {
                System.out.println(name + " حصل على قفل على " + obj1);
                work();
            }
            System.out.println(name + " قام بإطلاق قفل على " + obj1);
            System.out.println(name + " يقوم بالحصول على قفل على " + obj2);
            synchronized (obj2) {
                System.out.println(name + " حصل على قفل على " + obj2);
                work();
            }
            System.out.println(name + " قام بإطلاق قفل على " + obj2);
    
            System.out.println(name + " انتهى التنفيذ.");
        }
    
  • اغلق فقط ما هو مطلوب: يجب عليك أن تحصل على قفل فقط على الموارد التي تحتاج إليها للعمل ، على سبيل المثال في البرنامج أعلاه أنا أغلق الموارد الكاملة للكائن ولكن إذا كنا مهتمين فقط بأحد حقوله ، فيجب علينا أن نغلق فقط ذلك الحقل المحدد وليس الكائن بأكمله.

  • تجنب الانتظار لفترة غير معينة: يمكن أن يحدث انتقالا متبادلا إذا كانت هناك خيوطان في انتظار بعضهما البعض لإنهاء بشكل لا محدود باستخدام انضمام الخيط. إذا كان خيطك يجب أن ينتظر خيطًا آخر للانتهاء ، فمن الأفضل دائمًا استخدام الانضمام مع الحد الأقصى للوقت الذي تريد فيه الانتظار حتى ينتهي الخيط.

هذا كل شيء عن انتقالات الموت في خيوط جافا.

Source:
https://www.digitalocean.com/community/tutorials/deadlock-in-java-example