JVM ได้รับอนุญาตให้สมมติว่าเธรดอื่นไม่เปลี่ยนpizzaArrived
ตัวแปรระหว่างลูป กล่าวอีกนัยหนึ่งก็คือสามารถยกการpizzaArrived == false
ทดสอบนอกลูปได้โดยปรับสิ่งนี้ให้เหมาะสม:
while (pizzaArrived == false) {}
ในสิ่งนี้:
if (pizzaArrived == false) while (true) {}
ซึ่งเป็นวงวนที่ไม่มีที่สิ้นสุด
เพื่อให้แน่ใจว่าการเปลี่ยนแปลงที่ทำโดยเธรดหนึ่งจะมองเห็นได้กับเธรดอื่น ๆ คุณต้องเพิ่มการซิงโครไนซ์ระหว่างเธรดเสมอ วิธีที่ง่ายที่สุดคือสร้างตัวแปรที่ใช้ร่วมกันvolatile
:
volatile boolean pizzaArrived = false;
การสร้างตัวแปรจะvolatile
รับประกันได้ว่าเธรดที่แตกต่างกันจะเห็นผลของการเปลี่ยนแปลงของกันและกัน สิ่งนี้จะป้องกัน JVM จากการแคชค่าpizzaArrived
หรือยกการทดสอบนอกลูป แต่จะต้องอ่านค่าของตัวแปรจริงทุกครั้ง
(อย่างเป็นทางการมากขึ้นคือvolatile
สร้างความสัมพันธ์ที่เกิดขึ้นก่อนระหว่างการเข้าถึงตัวแปรซึ่งหมายความว่างานอื่น ๆ ทั้งหมดที่เธรดทำก่อนส่งพิซซ่าจะปรากฏให้เห็นเธรดที่รับพิซซ่าแม้ว่าการเปลี่ยนแปลงอื่น ๆ เหล่านั้นจะไม่ใช่volatile
ตัวแปรก็ตาม)
วิธีการซิงโครไนซ์ใช้เป็นหลักในการใช้การยกเว้นซึ่งกันและกัน (ป้องกันสองสิ่งที่เกิดขึ้นในเวลาเดียวกัน) แต่ก็มีผลข้างเคียงเหมือนกันทั้งหมดที่volatile
มี การใช้เมื่ออ่านและเขียนตัวแปรเป็นอีกวิธีหนึ่งในการทำให้เธรดอื่นมองเห็นการเปลี่ยนแปลง:
class MyHouse {
boolean pizzaArrived = false;
void eatPizza() {
while (getPizzaArrived() == false) {}
System.out.println("That was delicious!");
}
synchronized boolean getPizzaArrived() {
return pizzaArrived;
}
synchronized void deliverPizza() {
pizzaArrived = true;
}
}
ผลกระทบของคำสั่งการพิมพ์
System.out
เป็นPrintStream
วัตถุ วิธีการPrintStream
ซิงโครไนซ์ดังนี้:
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}
การซิงโครไนซ์จะป้องกันไม่ให้pizzaArrived
ถูกแคชระหว่างลูป พูดอย่างเคร่งครัดเธรดทั้งสองต้องซิงโครไนซ์บนวัตถุเดียวกันเพื่อรับประกันว่าการเปลี่ยนแปลงของตัวแปรจะมองเห็นได้ (ตัวอย่างเช่นการเรียกprintln
หลังจากการตั้งค่าpizzaArrived
และเรียกอีกครั้งก่อนที่จะอ่านpizzaArrived
จะถูกต้อง) หากมีเธรดเดียวที่ซิงโครไนซ์กับอ็อบเจ็กต์เฉพาะ JVM จะได้รับอนุญาตให้เพิกเฉย ในทางปฏิบัติ JVM ไม่ฉลาดพอที่จะพิสูจน์ว่าเธรดอื่น ๆ จะไม่เรียกprintln
หลังจากการตั้งค่าpizzaArrived
ดังนั้นจึงถือว่าพวกเขาอาจ System.out.println
ดังนั้นจึงไม่สามารถแคชตัวแปรในช่วงห่วงถ้าคุณโทร นั่นเป็นเหตุผลว่าทำไมการวนซ้ำจึงทำงานเช่นนี้เมื่อมีคำสั่งพิมพ์แม้ว่าจะไม่ใช่การแก้ไขที่ถูกต้องก็ตาม
การใช้System.out
ไม่ใช่วิธีเดียวที่จะทำให้เกิดเอฟเฟกต์นี้ แต่เป็นวิธีที่ผู้คนค้นพบบ่อยที่สุดเมื่อพวกเขาพยายามแก้จุดบกพร่องว่าเหตุใดการวนซ้ำจึงไม่ทำงาน!
ปัญหาที่ใหญ่กว่า
while (pizzaArrived == false) {}
เป็นห่วงที่ไม่ว่างรอ เลวร้าย! ในขณะที่กำลังรอมันจะปล่อย CPU ซึ่งจะทำให้แอปพลิเคชั่นอื่น ๆ ทำงานช้าลงและเพิ่มการใช้พลังงานอุณหภูมิและความเร็วพัดลมของระบบ ตามหลักการแล้วเราต้องการให้เธรดลูปเข้าสู่โหมดสลีปในขณะที่รอดังนั้นจึงไม่ทำให้ CPU ค้าง
วิธีดำเนินการดังต่อไปนี้:
ใช้การรอ / แจ้งเตือน
วิธีแก้ปัญหาระดับต่ำคือการใช้วิธีการรอ / แจ้งเตือนของObject
:
class MyHouse {
boolean pizzaArrived = false;
void eatPizza() {
synchronized (this) {
while (!pizzaArrived) {
try {
this.wait();
} catch (InterruptedException e) {}
}
}
System.out.println("That was delicious!");
}
void deliverPizza() {
synchronized (this) {
pizzaArrived = true;
this.notifyAll();
}
}
}
ในรหัสเวอร์ชันนี้การเรียกเธรดแบบวนซ้ำwait()
ซึ่งทำให้เธรดเข้าสู่โหมดสลีป จะไม่ใช้รอบ CPU ใด ๆ ในขณะนอนหลับ หลังจากเธรดที่สองตั้งค่าตัวแปรมันจะเรียกnotifyAll()
ใช้เธรดใด ๆ / ทั้งหมดที่รออยู่บนอ็อบเจ็กต์นั้น นี่ก็เหมือนกับการที่คนทำพิซซ่ากดกริ่งประตูเพื่อให้คุณสามารถนั่งพักระหว่างรอแทนที่จะยืนงุ่มง่ามที่ประตู
เมื่อโทรรอ / แจ้งเตือนบนวัตถุคุณต้องล็อคการซิงโครไนซ์ของวัตถุนั้นไว้ซึ่งเป็นสิ่งที่รหัสข้างต้นทำ คุณสามารถใช้วัตถุใดก็ได้ที่คุณต้องการตราบเท่าที่ทั้งสองเธรดใช้วัตถุเดียวกัน: ที่นี่ฉันใช้this
(ตัวอย่างของMyHouse
) โดยปกติเธรดสองเธรดจะไม่สามารถเข้าสู่บล็อกที่ซิงโครไนซ์ของอ็อบเจ็กต์เดียวกันพร้อมกันได้ (ซึ่งเป็นส่วนหนึ่งของจุดประสงค์ของการซิงโครไนซ์) แต่ทำงานได้ที่นี่เนื่องจากเธรดจะคลายล็อกการซิงโครไนซ์ชั่วคราวเมื่ออยู่ในwait()
เมธอด
การปิดกั้นคิว
A BlockingQueue
ใช้เพื่อใช้คิวผู้ผลิต - ผู้บริโภค "ผู้บริโภค" นำรายการจากด้านหน้าของคิวและ "ผู้ผลิต" ผลักรายการที่ด้านหลัง ตัวอย่าง:
class MyHouse {
final BlockingQueue<Object> queue = new LinkedBlockingQueue<>();
void eatFood() throws InterruptedException {
Object food = queue.take();
System.out.println("Eating: " + food);
}
void deliverPizza() throws InterruptedException {
queue.put("A delicious pizza");
}
}
หมายเหตุ: put
และtake
วิธีการBlockingQueue
โยนInterruptedException
s ซึ่งได้รับการตรวจสอบข้อยกเว้นที่ต้องจัดการ ในโค้ดด้านบนเพื่อความเรียบง่ายข้อยกเว้นจะถูกเปลี่ยนใหม่ คุณอาจต้องการจับข้อยกเว้นในวิธีการและลองโทรใหม่หรือรับสายเพื่อให้แน่ใจว่าทำได้สำเร็จ นอกเหนือจากความน่าเกลียดจุดหนึ่งแล้วBlockingQueue
ยังใช้งานง่ายมาก
ไม่จำเป็นต้องมีการซิงโครไนซ์อื่น ๆ ที่นี่เนื่องจากการBlockingQueue
ตรวจสอบให้แน่ใจว่าเธรดทุกอย่างที่ทำก่อนที่จะวางไอเท็มในคิวนั้นจะมองเห็นเธรดที่นำไอเท็มเหล่านั้นออก
ผู้บริหาร
Executor
s ก็เหมือนสำเร็จรูปBlockingQueue
ที่สั่งงาน ตัวอย่าง:
ExecutorService executor = Executors.newSingleThreadExecutor();
Runnable eatPizza = () -> { System.out.println("Eating a delicious pizza"); };
Runnable cleanUp = () -> { System.out.println("Cleaning up the house"); };
executor.execute(eatPizza);
executor.execute(cleanUp);
สำหรับรายละเอียดดูสำหรับ doc Executor
, และExecutorService
Executors
การจัดการเหตุการณ์
การวนซ้ำในขณะที่รอให้ผู้ใช้คลิกบางสิ่งใน UI นั้นผิด ให้ใช้คุณลักษณะการจัดการเหตุการณ์ของชุดเครื่องมือ UI แทน ใน Swingตัวอย่างเช่น:
JLabel label = new JLabel();
JButton button = new JButton("Click me");
button.addActionListener((ActionEvent e) -> {
label.setText("Button was clicked");
});
เนื่องจากตัวจัดการเหตุการณ์ทำงานบนเธรดการจัดส่งเหตุการณ์การทำงานที่ยาวนานในตัวจัดการเหตุการณ์จะบล็อกการโต้ตอบอื่น ๆ กับ UI จนกว่างานจะเสร็จสิ้น การดำเนินการที่ช้าสามารถเริ่มต้นบนเธรดใหม่หรือส่งไปยังเธรดที่รออยู่โดยใช้หนึ่งในเทคนิคข้างต้น (รอ / แจ้งเตือน a BlockingQueue
หรือExecutor
) คุณยังสามารถใช้ a SwingWorker
ซึ่งได้รับการออกแบบมาสำหรับสิ่งนี้และจัดหาเธรดผู้ปฏิบัติงานเบื้องหลังโดยอัตโนมัติ:
JLabel label = new JLabel();
JButton button = new JButton("Calculate answer");
button.addActionListener((ActionEvent e) -> {
class MyWorker extends SwingWorker<String,Void> {
@Override
public String doInBackground() throws Exception {
Thread.sleep(5000);
return "Answer is 42";
}
@Override
protected void done() {
String result;
try {
result = get();
} catch (Exception e) {
throw new RuntimeException(e);
}
label.setText(result);
}
}
new MyWorker().execute();
});
ตัวจับเวลา
ในการดำเนินการเป็นระยะคุณสามารถใช้ไฟล์java.util.Timer
. ใช้งานง่ายกว่าการเขียนไทม์มิ่งลูปของคุณเองและเริ่มและหยุดได้ง่ายกว่า การสาธิตนี้พิมพ์เวลาปัจจุบันหนึ่งครั้งต่อวินาที:
Timer timer = new Timer();
TimerTask task = new TimerTask() {
@Override
public void run() {
System.out.println(System.currentTimeMillis());
}
};
timer.scheduleAtFixedRate(task, 0, 1000);
แต่ละคนjava.util.Timer
มีเธรดพื้นหลังของตัวเองซึ่งใช้ในการรันTimerTask
s ตามกำหนดเวลา โดยปกติเธรดจะนอนระหว่างงานดังนั้นจึงไม่ทำให้ CPU ค้าง
ในรหัส Swing ยังมี a javax.swing.Timer
ซึ่งคล้ายกัน แต่เรียกใช้งาน Listener บน Swing thread ดังนั้นคุณสามารถโต้ตอบกับส่วนประกอบ Swing ได้อย่างปลอดภัยโดยไม่จำเป็นต้องสลับเธรดด้วยตนเอง:
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Timer timer = new Timer(1000, (ActionEvent e) -> {
frame.setTitle(String.valueOf(System.currentTimeMillis()));
});
timer.setRepeats(true);
timer.start();
frame.setVisible(true);
ทางอื่น
หากคุณกำลังเขียนโค้ดหลายเธรดคุณควรสำรวจคลาสในแพ็คเกจเหล่านี้เพื่อดูว่ามีอะไรบ้าง:
และดูส่วนการทำงานพร้อมกันของบทช่วยสอน Java มัลติเธรดมีความซับซ้อน แต่มีความช่วยเหลือมากมาย!