ภาพรวมโดยย่อของคุณลักษณะของการโต้ตอบกับเธรด ก่อนหน้านี้ เราได้ดูว่าเธรดซิงโครไนซ์กันอย่างไร คราวนี้เราจะเจาะลึกถึงปัญหาที่อาจเกิดขึ้นเมื่อเธรดโต้ตอบกัน และพูดคุยเกี่ยวกับวิธีหลีกเลี่ยง นอกจากนี้เรายังมีลิงก์ที่เป็นประโยชน์สำหรับการศึกษาเชิงลึกอีกด้วย
ตัวอย่างที่ยอดเยี่ยมสามารถพบได้ที่นี่: " Java - Thread Starvation and Fairness " ตัวอย่างนี้แสดงวิธีการทำงานของเธรดใน Starvation และการเปลี่ยนแปลงเล็กน้อยจาก Thread.sleep เป็น Thread.wait สามารถกระจายโหลดอย่างเท่าเทียมกันได้อย่างไร
คงจะดีกว่าถ้าวิดีโอนี้ไม่ได้บอกอะไรเกี่ยวกับเรื่องนี้ ฉันจะทิ้งลิงก์ไปยังวิดีโอไว้ คุณสามารถอ่าน " Java - การทำความเข้าใจสิ่งที่เกิดขึ้นก่อนความสัมพันธ์ "
การแนะนำ
ดังนั้นเราจึงรู้ว่ามีเธรดใน Java ซึ่งคุณสามารถอ่านได้ในรีวิว “ Thread Can't Spoil Java: Part I - Threads ” และเธรดนั้นสามารถซิงโครไนซ์ซึ่งกันและกันได้ ซึ่งเราจัดการในการรีวิว “ เธรดไม่สามารถสปอย Java ” สปอยล์: ตอนที่ II - การซิงโครไนซ์ " ถึงเวลาที่จะพูดถึงว่าเธรดมีปฏิสัมพันธ์กันอย่างไร พวกเขาแบ่งปันทรัพยากรร่วมกันอย่างไร? จะมีปัญหาอะไรเกิดขึ้นบ้าง?การหยุดชะงัก
ปัญหาที่เลวร้ายที่สุดคือการหยุดชะงัก เมื่อเธรดตั้งแต่สองเธรดขึ้นไปรอกันและกันตลอดไป สิ่งนี้เรียกว่า Deadlock ลองยกตัวอย่างจากเว็บไซต์ Oracle จากคำอธิบายแนวคิดของ " Deadlock ":public class Deadlock {
static class Friend {
private final String name;
public Friend(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public synchronized void bow(Friend bower) {
System.out.format("%s: %s has bowed to me!%n",
this.name, bower.getName());
bower.bowBack(this);
}
public synchronized void bowBack(Friend bower) {
System.out.format("%s: %s has bowed back to me!%n",
this.name, bower.getName());
}
}
public static void main(String[] args) {
final Friend alphonse = new Friend("Alphonse");
final Friend gaston = new Friend("Gaston");
new Thread(() -> alphonse.bow(gaston)).start();
new Thread(() -> gaston.bow(alphonse)).start();
}
}
การหยุดชะงักที่นี่อาจไม่ปรากฏขึ้นในครั้งแรก แต่ถ้าการทำงานของโปรแกรมของคุณติดขัด ก็ถึงเวลาที่ต้องรันjvisualvm
: หากมีการติดตั้งปลั๊กอินใน JVisualVM (ผ่านเครื่องมือ -> ปลั๊กอิน) เราจะเห็นว่าการหยุดชะงักเกิดขึ้นที่ใด:
"Thread-1" - Thread t@12
java.lang.Thread.State: BLOCKED
at Deadlock$Friend.bowBack(Deadlock.java:16)
- waiting to lock <33a78231> (a Deadlock$Friend) owned by "Thread-0" t@11
เธรด 1 กำลังรอการล็อคจากเธรด 0 เหตุใดสิ่งนี้จึงเกิดขึ้น Thread-1
เริ่มดำเนินการและดำเนินการตามวิธีFriend#bow
การ มันถูกทำเครื่องหมายด้วยคำหลักsynchronized
นั่นคือเรารับจอภาพthis
ด้วย ที่ทางเข้าวิธีการ เราได้รับลิงก์ไปยังอีกวิธีFriend
หนึ่ง ตอนนี้ เธรดThread-1
ต้องการดำเนินการวิธีการกับอีกวิธีหนึ่งFriend
ดังนั้นจึงได้รับการล็อคจากเขาเช่นกัน แต่หากเธรดอื่น (ในกรณีนี้Thread-0
) จัดการเพื่อเข้าสู่วิธีการ แสดงbow
ว่าการล็อคไม่ว่างและThread-1
กำลังรออยู่Thread-0
และในทางกลับกัน การปิดกั้นนั้นแก้ไม่ได้จึงตายนั่นคือตาย ทั้งการจับความตาย (ซึ่งไม่สามารถปล่อยออกมาได้) และอุปสรรคที่ไม่อาจหลบหนีได้ ในหัวข้อการหยุดชะงัก คุณสามารถชมวิดีโอ: " Deadlock - Concurrency #1 - Java ขั้นสูง "
ไลฟ์ล็อค
หากมี Deadlock แล้วจะมี Livelock หรือไม่? ใช่ มี) Livelock คือ เธรดดูเหมือนจะมีชีวิตภายนอก แต่ในขณะเดียวกันก็ไม่สามารถทำอะไรได้เลย เพราะ... ไม่สามารถปฏิบัติตามเงื่อนไขที่พวกเขาพยายามทำงานต่อไปได้ โดยพื้นฐานแล้ว Livelock นั้นคล้ายกับการหยุดชะงัก แต่เธรดจะไม่ "ค้าง" ในระบบเพื่อรอจอภาพ แต่มักจะทำอะไรบางอย่างอยู่เสมอ ตัวอย่างเช่น:import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class App {
public static final String ANSI_BLUE = "\u001B[34m";
public static final String ANSI_PURPLE = "\u001B[35m";
public static void log(String text) {
String name = Thread.currentThread().getName(); //like Thread-1 or Thread-0
String color = ANSI_BLUE;
int val = Integer.valueOf(name.substring(name.lastIndexOf("-") + 1)) + 1;
if (val != 0) {
color = ANSI_PURPLE;
}
System.out.println(color + name + ": " + text + color);
try {
System.out.println(color + name + ": wait for " + val + " sec" + color);
Thread.currentThread().sleep(val * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Lock first = new ReentrantLock();
Lock second = new ReentrantLock();
Runnable locker = () -> {
boolean firstLocked = false;
boolean secondLocked = false;
try {
while (!firstLocked || !secondLocked) {
firstLocked = first.tryLock(100, TimeUnit.MILLISECONDS);
log("First Locked: " + firstLocked);
secondLocked = second.tryLock(100, TimeUnit.MILLISECONDS);
log("Second Locked: " + secondLocked);
}
first.unlock();
second.unlock();
} catch (InterruptedException e) {
e.printStackTrace();
}
};
new Thread(locker).start();
new Thread(locker).start();
}
}
ความสำเร็จของโค้ดนี้ขึ้นอยู่กับลำดับที่ตัวกำหนดเวลาเธรด Java เริ่มต้นเธรด ถ้ามันเริ่มก่อนThead-1
เราจะได้ Livelock:
Thread-1: First Locked: true
Thread-1: wait for 2 sec
Thread-0: First Locked: false
Thread-0: wait for 1 sec
Thread-0: Second Locked: true
Thread-0: wait for 1 sec
Thread-1: Second Locked: false
Thread-1: wait for 2 sec
Thread-0: First Locked: false
Thread-0: wait for 1 sec
...
ดังที่เห็นได้จากตัวอย่าง ทั้งสองเธรดสลับกันพยายามจับล็อคทั้งสอง แต่ล้มเหลว ยิ่งกว่านั้นพวกเขาไม่ได้หยุดชะงักนั่นคือทุกอย่างดูดีสำหรับพวกเขาและพวกเขาก็กำลังทำงานของพวกเขา จากข้อมูลของ JVisualVM เราเห็นช่วงเวลาสลีปและระยะเวลาการพัก (นี่คือเมื่อเธรดพยายามครอบครองการล็อค และจะเข้าสู่สถานะการพัก ตามที่เราได้กล่าวไว้ก่อนหน้านี้เมื่อพูดถึงการซิงโครไนซ์เธรด ) ในหัวข้อ livelock คุณสามารถดูตัวอย่าง: " Java - Thread Livelock "
ความอดอยาก
นอกเหนือจากการบล็อก (deadlock และ livelock) ยังมีปัญหาอีกประการหนึ่งเมื่อทำงานกับมัลติเธรด - ความอดอยากหรือ "ความอดอยาก" ปรากฏการณ์นี้แตกต่างจากการบล็อกตรงที่เธรดไม่ถูกบล็อก แต่มีทรัพยากรไม่เพียงพอสำหรับทุกคน ดังนั้น แม้ว่าบางเธรดจะใช้เวลาดำเนินการทั้งหมด แต่เธรดอื่นๆ ก็ไม่สามารถดำเนินการได้:https://www.logicbig.com/
สภาพการแข่งขัน
เมื่อทำงานกับมัลติเธรดจะเกิด "สภาวะการแข่งขัน" ปรากฏการณ์นี้อยู่ในความจริงที่ว่าเธรดแบ่งปันทรัพยากรบางอย่างระหว่างกันและโค้ดถูกเขียนในลักษณะที่ไม่ได้จัดเตรียมไว้สำหรับการดำเนินการที่ถูกต้องในกรณีนี้ ลองดูตัวอย่าง:public class App {
public static int value = 0;
public static void main(String[] args) {
Runnable task = () -> {
for (int i = 0; i < 10000; i++) {
int oldValue = value;
int newValue = ++value;
if (oldValue + 1 != newValue) {
throw new IllegalStateException(oldValue + " + 1 = " + newValue);
}
}
};
new Thread(task).start();
new Thread(task).start();
new Thread(task).start();
}
}
รหัสนี้อาจไม่สร้างข้อผิดพลาดในครั้งแรก และอาจมีลักษณะเช่นนี้:
Exception in thread "Thread-1" java.lang.IllegalStateException: 7899 + 1 = 7901
at App.lambda$main$0(App.java:13)
at java.lang.Thread.run(Thread.java:745)
อย่างที่คุณเห็น ในขณะที่กำลังได้รับมอบหมายnewValue
มีบางอย่างผิดพลาดและnewValue
ยังมีอีกมาก เธรดบางส่วนในสภาพการแข่งขันสามารถเปลี่ยนแปลงได้value
ระหว่างสองทีมนี้ ดังที่เราเห็น การแข่งขันระหว่างเธรดได้ปรากฏขึ้น ทีนี้ลองจินตนาการดูว่าการไม่ทำผิดพลาดแบบเดียวกันนี้กับธุรกรรมทางการเงินนั้นสำคัญขนาดไหน... สามารถดูตัวอย่างและไดอะแกรมได้ที่นี่: “ โค้ดเพื่อจำลองสภาพการแข่งขันในเธรด Java ”
ระเหย
เมื่อพูดถึงปฏิสัมพันธ์ของเธรด คำหลักนั้นควรค่าแก่การสังเกตเป็นvolatile
พิเศษ ลองดูตัวอย่างง่ายๆ:
public class App {
public static boolean flag = false;
public static void main(String[] args) throws InterruptedException {
Runnable whileFlagFalse = () -> {
while(!flag) {
}
System.out.println("Flag is now TRUE");
};
new Thread(whileFlagFalse).start();
Thread.sleep(1000);
flag = true;
}
}
สิ่งที่น่าสนใจที่สุดคือมีโอกาสสูงที่มันจะไม่ทำงาน เธรดใหม่จะไม่เห็นการflag
เปลี่ยนแปลง ในการแก้ไขปัญหานี้flag
คุณจะต้องระบุคำหลัก สำหรับ volatile
ฟิลด์ อย่างไรและทำไม? การดำเนินการทั้งหมดดำเนินการโดยโปรเซสเซอร์ แต่ต้องเก็บผลการคำนวณไว้ที่ไหนสักแห่ง เพื่อจุดประสงค์นี้จึงมีหน่วยความจำหลักและแคชฮาร์ดแวร์บนโปรเซสเซอร์ แคชโปรเซสเซอร์เหล่านี้เปรียบเสมือนหน่วยความจำชิ้นเล็กๆ สำหรับการเข้าถึงข้อมูลได้เร็วกว่าการเข้าถึงหน่วยความจำหลัก แต่ทุกอย่างก็มีข้อเสียเช่นกัน: ข้อมูลในแคชอาจไม่เป็นปัจจุบัน (ดังตัวอย่างด้านบนเมื่อไม่ได้อัปเดตค่าแฟล็ก) ดังนั้นคีย์เวิร์ดvolatile
จะบอก JVM ว่าเราไม่ต้องการแคชตัวแปรของเรา ซึ่งจะทำให้คุณสามารถเห็นผลลัพธ์ที่แท้จริงในทุกเธรด นี่เป็นสูตรที่ง่ายมาก ในหัวข้อนี้volatile
ขอแนะนำอย่างยิ่งให้อ่านคำแปลของ " JSR 133 (Java Memory Model) FAQ " ฉันยังแนะนำให้คุณอ่านเพิ่มเติมเกี่ยวกับวัสดุ “ Java Memory Model ” และ “ Java Volatile Keyword ” นอกจากนี้ สิ่งสำคัญคือต้องจำไว้ว่าvolatile
สิ่งนี้เกี่ยวกับการมองเห็น ไม่ใช่เกี่ยวกับอะตอมมิกของการเปลี่ยนแปลง หากเราใช้โค้ดจาก "สภาพการแข่งขัน" เราจะเห็นคำใบ้ใน IntelliJ Idea: การตรวจสอบ (การตรวจสอบ) นี้ถูกเพิ่มใน IntelliJ Idea โดยเป็นส่วนหนึ่งของปัญหาIDEA-61117ซึ่งแสดงอยู่ในบันทึกประจำรุ่นเมื่อปี 2010
อะตอมมิกซิตี
การดำเนินการของอะตอมเป็นการดำเนินการที่ไม่สามารถแบ่งแยกได้ ตัวอย่างเช่น การดำเนินการกำหนดค่าให้กับตัวแปรเป็นแบบอะตอมมิก น่าเสียดายที่การเพิ่มขึ้นไม่ใช่การดำเนินการแบบอะตอมมิก เนื่องจาก การเพิ่มขึ้นต้องใช้การดำเนินการมากถึงสามรายการ: รับค่าเก่า เพิ่มหนึ่งรายการเข้าไป และบันทึกค่า เหตุใดอะตอมมิกซิตีจึงมีความสำคัญ ในตัวอย่างที่เพิ่มขึ้น หากสภาวะการแข่งขันเกิดขึ้น ทรัพยากรที่ใช้ร่วมกัน (เช่น ค่าที่ใช้ร่วมกัน) อาจเปลี่ยนแปลงทันทีเมื่อใดก็ได้ นอกจากนี้ สิ่งสำคัญคือโครงสร้าง 64 บิตต้องไม่ใช่อะตอมมิกด้วย เช่นlong
และdouble
. คุณสามารถอ่านเพิ่มเติมได้ที่นี่: " ตรวจสอบความเป็นอะตอมมิกเมื่ออ่านและเขียนค่า 64 บิต " ตัวอย่างของปัญหาเกี่ยวกับอะตอมมิกซิตีสามารถดูได้ในตัวอย่างต่อไปนี้:
public class App {
public static int value = 0;
public static AtomicInteger atomic = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
for (int i = 0; i < 10000; i++) {
value++;
atomic.incrementAndGet();
}
};
for (int i = 0; i < 3; i++) {
new Thread(task).start();
}
Thread.sleep(300);
System.out.println(value);
System.out.println(atomic.get());
}
}
คลาสพิเศษสำหรับการทำงานกับอะตอมInteger
จะแสดงให้เราเห็น 30,000 เสมอ แต่value
จะมีการเปลี่ยนแปลงเป็นครั้งคราว มีภาพรวมโดยย่อในหัวข้อ " บทนำเกี่ยวกับตัวแปรอะตอมใน Java " Atomic ขึ้นอยู่กับอัลกอริธึมการเปรียบเทียบและสลับ คุณสามารถอ่านเพิ่มเติมได้ในบทความเรื่องHabré " การเปรียบเทียบอัลกอริธึมที่ปราศจากการล็อค - CAS และ FAA โดยใช้ตัวอย่างของ JDK 7 และ 8 " หรือบน Wikipedia ในบทความเกี่ยวกับ " การเปรียบเทียบกับการแลกเปลี่ยน "
http://jeremymanson.blogspot.com/2008/11/what-volatile-means-in-java.html
เกิดขึ้นก่อน
มีสิ่งที่น่าสนใจและลึกลับเกิดขึ้นก่อน เมื่อพูดถึงกระแส ก็ควรค่าแก่การอ่านเช่นกัน ความสัมพันธ์ Happens Before ระบุลำดับที่จะเห็นการดำเนินการระหว่างเธรด มีการตีความและการตีความมากมาย หนึ่งในรายงานล่าสุดในหัวข้อนี้คือรายงานนี้:ผลลัพธ์
ในการทบทวนนี้ เราได้ดูคุณลักษณะของการโต้ตอบกับเธรด เราได้หารือเกี่ยวกับปัญหาที่อาจเกิดขึ้นและวิธีการตรวจจับและกำจัดปัญหาเหล่านั้น รายการวัสดุเพิ่มเติมในหัวข้อ:- อีกครั้งเกี่ยวกับการล็อคที่มีการตรวจสอบซ้ำ
- คำถามที่พบบ่อย JSR 133 (โมเดลหน่วยความจำ Java) (การแปล)
- Java ขั้นสูง - การทำงานพร้อมกัน (Yuri Tkach)
- แนวคิดการทำงานพร้อมกันใน Java โดย Douglas Hawkins (2017)
GO TO FULL VERSION