ภาพรวมโดยย่อของรายละเอียดการโต้ตอบของเธรด ก่อนหน้านี้ เราดูวิธีการซิงโครไนซ์เธรดกับอีกอันหนึ่ง ครั้งนี้เราจะเจาะลึกปัญหาที่อาจเกิดขึ้นเมื่อเธรดโต้ตอบกัน และเราจะพูดถึงวิธีหลีกเลี่ยงปัญหาเหล่านั้น เราจะให้ลิงก์ที่มีประโยชน์สำหรับการศึกษาเชิงลึกเพิ่มเติม
คุณสามารถดูตัวอย่างที่ยอดเยี่ยมได้ที่นี่: Java - Thread Starvation and Fairness ตัวอย่างนี้แสดงสิ่งที่เกิดขึ้นกับเธรดระหว่างการอดอาหาร และการเปลี่ยนแปลงเพียงเล็กน้อยจาก
การแนะนำ
ดังนั้นเราจึงรู้ว่า Java มีเธรด คุณสามารถอ่านเกี่ยวกับเรื่องนี้ได้ในบทวิจารณ์เรื่องBetter together: Java and the Thread class ส่วนที่ 1 — เธรดของการดำเนินการ และเราได้สำรวจข้อเท็จจริงที่ว่าเธรดสามารถซิงโครไนซ์กับเธรดอื่นได้ในบทวิจารณ์เรื่องBetter together: Java and the Thread class ส่วนที่ II — การซิงโครไนซ์ ได้เวลาพูดคุยเกี่ยวกับวิธีที่เธรดโต้ตอบกัน พวกเขาแบ่งปันทรัพยากรร่วมกันอย่างไร? ปัญหาที่อาจเกิดขึ้นที่นี่?การหยุดชะงัก
ปัญหาที่น่ากลัวที่สุดคือการหยุดชะงัก การหยุดชะงักคือเมื่อเธรดสองเธรดหรือมากกว่ากำลังรอเธรดอื่นชั่วนิรันดร์ เราจะนำตัวอย่างจากหน้าเว็บ Oracle ที่อธิบายการหยุดชะงัก :
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 bowed to me!%n",
this.name, bower.getName());
bower.bowBack(this);
}
public synchronized void bowBack(Friend bower) {
System.out.format("%s: %s 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
, และในทางกลับกัน. นี่เป็นทางตันที่ไม่สามารถแก้ไขได้ และเราเรียกว่าการหยุดชะงัก เช่นเดียวกับเงื้อมมือแห่งความตายที่ไม่สามารถปลดปล่อยได้ การหยุดชะงักคือการปิดกั้นซึ่งกันและกันซึ่งไม่สามารถทำลายได้ สำหรับคำอธิบายอื่นเกี่ยวกับ การ หยุดชะงัก คุณสามารถดูวิดีโอนี้: อธิบายการหยุดชะงักและ 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
ความอดอยาก
นอกจากการหยุดชะงักและไลฟ์ล็อกแล้ว ยังมีปัญหาอื่นที่อาจเกิดขึ้นระหว่างการทำงานแบบมัลติเธรดได้ นั่นคือความอดอยาก ปรากฏการณ์นี้แตกต่างจากการบล็อกรูปแบบก่อนหน้านี้ตรงที่เธรดไม่ถูกบล็อก — มีเพียงทรัพยากรไม่เพียงพอ เป็นผลให้ในขณะที่บางเธรดใช้เวลาในการดำเนินการทั้งหมด แต่เธรดอื่นไม่สามารถเรียกใช้ได้:https://www.logicbig.com/
Thread.sleep()
เป็นThread.wait()
ช่วยให้คุณกระจายโหลดได้เท่าๆ กัน
สภาพการแข่งขัน
ในมัลติเธรด มีสิ่งเช่น "สภาวะการแข่งขัน" ปรากฏการณ์นี้เกิดขึ้นเมื่อเธรดใช้ทรัพยากรร่วมกัน แต่โค้ดถูกเขียนในลักษณะที่ไม่รับประกันการแบ่งปันที่ถูกต้อง ลองดูตัวอย่าง:
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
คำหลัก ฉันขอแนะนำให้คุณอ่านบทความนี้ สำหรับข้อมูลเพิ่มเติม ฉันแนะนำให้คุณอ่าน Java Memory ModelและJava Volatile Keyword นอกจากนี้ สิ่งสำคัญคือต้องจำไว้ว่าสิ่งนั้นvolatile
เกี่ยวกับการมองเห็น ไม่ใช่เกี่ยวกับปรมาณูของการเปลี่ยนแปลง เมื่อดูรหัสในส่วน "เงื่อนไขการแข่งขัน" เราจะเห็นคำแนะนำเครื่องมือใน IntelliJ IDEA: การตรวจสอบนี้ถูกเพิ่มไปยัง IntelliJ IDEA โดยเป็นส่วนหนึ่งของปัญหาIDEA-61117ซึ่งระบุไว้ในบันทึกย่อประจำรุ่นย้อนกลับไปในปี 2010
ปรมาณู
ปฏิบัติการปรมาณูเป็นปฏิบัติการที่ไม่สามารถแบ่งแยกได้ ตัวอย่างเช่น การดำเนินการกำหนดค่าให้กับตัวแปรจะต้องเป็นแบบปรมาณู ขออภัย การดำเนินการเพิ่มไม่ใช่ระดับปรมาณู เนื่องจากการเพิ่มขึ้นต้องการการดำเนินการ CPU มากถึงสามรายการ: รับค่าเก่า เพิ่มหนึ่งค่า จากนั้นบันทึกค่า ทำไมปรมาณูจึงสำคัญ? ด้วยการดำเนินการเพิ่ม หากมีสภาวะการแย่งชิง ทรัพยากรที่ใช้ร่วมกัน (เช่น มูลค่าที่ใช้ร่วมกัน) อาจเปลี่ยนแปลงทันทีเมื่อใดก็ได้ นอกจากนี้ การดำเนินการเกี่ยวกับโครงสร้าง 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());
}
}
คลาส พิเศษAtomicInteger
จะให้เงินเรา 30,000 เสมอ แต่value
จะเปลี่ยนไปเรื่อยๆ มีภาพรวมสั้นๆ ของหัวข้อนี้: Introduction to Atomic Variables in Java อัลกอริทึม "เปรียบเทียบและแลกเปลี่ยน" เป็นหัวใจสำคัญของคลาสอะตอม คุณสามารถอ่านเพิ่มเติมได้ที่นี่ในการเปรียบเทียบอัลกอริทึมที่ปราศจากการล็อค - CAS และ FAA ในตัวอย่างของ JDK 7 และ 8หรือใน บทความ เปรียบเทียบและแลกเปลี่ยนบนวิกิพีเดีย
http://jeremymanson.blogspot.com/2008/11/what-volatile-means-in-java.html
เกิดขึ้นก่อน
มีแนวคิดที่น่าสนใจและลึกลับที่เรียกว่า "เกิดขึ้นก่อน" ในส่วนหนึ่งของการศึกษาหัวข้อของคุณ คุณควรอ่านเกี่ยวกับเรื่องนี้ ความสัมพันธ์ที่จะเกิดขึ้นก่อนแสดงลำดับการดำเนินการระหว่างเธรด มีการตีความและให้ข้อคิดมากมาย นี่คือหนึ่งในงานนำ เสนอ ล่าสุดในหัวข้อนี้: Java "Happens-Before" Relationshipsสรุป
ในการตรวจสอบนี้ เราได้สำรวจลักษณะเฉพาะบางประการของการโต้ตอบของเธรด เราได้หารือถึงปัญหาที่อาจเกิดขึ้นตลอดจนวิธีการระบุและกำจัดปัญหาเหล่านั้น รายการวัสดุเพิ่มเติมในหัวข้อ:- ตรวจสอบการล็อคสองครั้ง
- คำถามที่พบบ่อยเกี่ยวกับ JSR 133 (รุ่นหน่วยความจำ Java)
- IQ 35: จะป้องกันการหยุดชะงักได้อย่างไร?
- แนวคิดการทำงานพร้อมกันใน Java โดย Douglas Hawkins (2017)
GO TO FULL VERSION