הקושי בהסתבכות ב-Java הוא מצב תכנותי בו שני או יותר תהליכים נתקעים לעד. המצב של חסימת Java מתרחש עם לפחות שני תהליכים ושני או יותר משאבים. כאן כתבתי תוכנית פשוטה שתגרום למצב חסימה ב-Java ואז נראה איך לנתח אותו.
חסימה ב-Java
בואו נסתכל על תוכנית פשוטה בה אני אצור חסימה בתהליכי 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 ועובד על שני אובייקטים על ידי קבלת נעילה על כל אחד מהם בנפרד באמצעות בלוק synchronized. בשיטת main, יש לי שלושה תהליכים רצים עבור SyncThread ויש משאב משותף בין כל אחד מהתהליכים. התהליכים רצים בצורה שבה הם יכולים לרכוש נעילה על האובייקט הראשון, אך כאשר הם מנסים לרכוש נעילה על האובייקט השני, הם נכנסים למצב המתנה מכיוון שהוא ננעל כבר על ידי תהליך אחר. זה יוצר תלות מחזורית עבור המשאב בין התהליכים שגורמת לחסימה. כאשר אני מפעיל את התוכנית לעיל, הנה הפלט שנוצר אך התוכנית לא מסתיימת מכיוון שיש חסימה בתהליכי Java.
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
כאן ניתן לזהות בבירור את מצב החסימה מהפלט אך ביישומים בחיים אמיתיים קשה מאוד למצוא את מצב החסימה ולאתר אותו.
איך לזהות סגירה ב-Java
כדי לזהות סגירה ב-Java, עלינו להסתכל על דאמפ התהליכים של Java של היישום, בפוסט הקודם תיארתי איך ניתן ליצור דאמפ של התהליכים באמצעות פרופיילר VisualVM או באמצעות הכלי 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". לאחר שאנו אנליזנו את המצב של הסגירה וגילינו את התהליכים הגורמים לסגירה, עלינו לבצע שינויים בקוד כדי למנוע את מצב הסגירה.
איך למנוע סגירה ב-Java
אלה הן כמה מההנחיות שבאמצעותן ניתן למנוע את רוב מצבי הסגירה.
-
להימנע מנעילות מקוננות: זהו הסיבה הנפוצה ביותר לחסימות סמויות, יש להימנע מנעילת משאב אחר אם כבר נעולים משאבים אחרים. כמעט בלתי אפשרי להיתקע במצב של חסימה סמויה אם אתה עובד עם נעילה של אובייקט אחד בלבד. לדוגמה, הנה עוד מימוש של שיטת הרצה ()run) בלי נעילה מקוננת והתוכנית רצה בהצלחה בלעדי חסימות סמויות.
public void run() { String name = Thread.currentThread().getName(); System.out.println(name + " מבצע נעילה על " + obj1); synchronized (obj1) { System.out.println(name + " נעילה על " + obj1 + " הושגה"); עבודה(); } System.out.println(name + " שחרר נעילה על " + obj1); System.out.println(name + " מבצע נעילה על " + obj2); synchronized (obj2) { System.out.println(name + " נעילה על " + obj2 + " הושגה"); עבודה(); } System.out.println(name + " שחרר נעילה על " + obj2); System.out.println(name + " הסתיים בביצוע."); }
-
נעל רק מה שנדרש: עליך לרכוש נעילה רק על המשאבים שעליך לעבוד עליהם, לדוגמה, בתוכנית למעלה אני נועל את כל משאב האובייקט, אך אם אנו רוצים לנעול רק על תחום מסוים שלו, אז עלינו לנעול רק את השדה הספציפי ולא את האובייקט המלא.
-
הימנע מהמתנה לא סופית: ייתכן שתקבל נעילה אם שני תהליכים ממתינים זה לזה לסיים לא סופית באמצעות join של תהליך. אם התהליך שלך צריך לחכות לתהליך אחר לסיים, תמיד טוב להשתמש ב-join עם הזמן המרבי שבו ברצונך לחכות שהתהליך יסיים.
זהו כל הדברים לגבי נעילה בתהליכי Java.
Source:
https://www.digitalocean.com/community/tutorials/deadlock-in-java-example