บทความนี้ส่วนใหญ่ติดตามบทความก่อนหน้าสองเรื่องของมัลติเธรดเพื่อสรุปปัญหาความปลอดภัยของเธรดในมัลติเธรด Java
1. ตัวอย่างความปลอดภัยด้าย Java ทั่วไป
Public Class Threadtest {โมฆะสาธารณะคงที่หลัก (สตริง [] args) {บัญชีบัญชี = บัญชีใหม่ ("123456", 1,000); DrawMoneyRunnable DrawMoneyRunnable = ใหม่ DrawMoneyRunnable (บัญชี, 700); เธรด MyThread1 = เธรดใหม่ (drawMoneyRunnable); เธรด MyThread2 = เธรดใหม่ (drawMoneyRunnable); MYTHREAD1.START (); Mythread2.start (); }} คลาส drawMoneyRunnable ใช้งาน runnable {บัญชีส่วนตัว; คู่หูคู่ส่วนตัว; Public DrawMoneyRunnable (บัญชีบัญชี, Double DrawAmount) {super (); this.account = บัญชี; this.drawamount = drawamount; } public void run () {ถ้า (account.getBalance ()> = drawAmount) {// 1 system.out.println ("การถอนสำเร็จการถอนเงินคือ:" + drawamount); Double Balance = account.getBalance () - drawamount; Account.setBalance (ยอดคงเหลือ); System.out.println ("ยอดคงเหลือคือ:" + สมดุล); }}} บัญชีคลาส {Private String AccountNo; สมดุลสองเท่าส่วนตัว; บัญชีสาธารณะ () {} บัญชีสาธารณะ (String AccountNo, Double Balance) {this.accountNo = accountNo; this.balance = Balance; } สตริงสาธารณะ getAccountNo () {return accountno; } โมฆะสาธารณะ setAccountNo (String AccountNo) {this.AccountNo = accountNo; } public double getBalance () {Balance return; } โมฆะสาธารณะ setBalance (สมดุลสองเท่า) {this.balance = สมดุล; -ตัวอย่างข้างต้นเข้าใจง่าย มีบัตรธนาคารที่มียอดคงเหลือ 1,000 โปรแกรมจำลองฉากที่คุณและภรรยาของคุณถอนเงินใน ATM ในเวลาเดียวกัน เรียกใช้โปรแกรมนี้หลายครั้งและอาจมีผลลัพธ์ผลลัพธ์ในชุดค่าผสมที่แตกต่างกันหลายอย่าง หนึ่งในผลลัพธ์ที่เป็นไปได้คือ:
1 การถอนเงินสำเร็จการถอนเงินคือ: 700.0
2 สมดุลคือ: 300.0
3 การถอนเงินสำเร็จการถอนเงินคือ: 700.0
4 ยอดคงเหลือคือ: -400.0
กล่าวอีกนัยหนึ่งสำหรับบัตรธนาคารที่มียอดคงเหลือเพียง 1,000 คุณสามารถถอนได้ทั้งหมด 1,400 ซึ่งเห็นได้ชัดว่าเป็นปัญหา
หลังจากการวิเคราะห์ปัญหาอยู่ที่ความไม่แน่นอนของการดำเนินการในสภาพแวดล้อมแบบมัลติเธรด Java CPU อาจสุ่มสลับระหว่างหลายเธรดในสถานะพร้อมดังนั้นจึงเป็นไปได้มากที่สถานการณ์ต่อไปนี้เกิดขึ้น: เมื่อเธรด 1 ดำเนินการรหัสที่ // 1 เงื่อนไขการตัดสินเป็นจริง ในเวลานี้ CPU จะเปลี่ยนเป็น Thread2, เรียกใช้รหัสที่ // 1 และพบว่ามันยังคงเป็นจริง จากนั้นจะดำเนินการ Thread2 จากนั้นสลับไปที่ Thread1 จากนั้นการดำเนินการเสร็จสิ้น ในเวลานี้ผลลัพธ์ข้างต้นจะปรากฏขึ้น
ดังนั้นเมื่อพูดถึงปัญหาด้านความปลอดภัยของเธรดมันหมายความว่าการเข้าถึงทรัพยากรที่ใช้ร่วมกันในสภาพแวดล้อมแบบมัลติเธรดอาจทำให้เกิดความไม่สอดคล้องกันในทรัพยากรที่ใช้ร่วมกันนี้ ดังนั้นเพื่อหลีกเลี่ยงปัญหาด้านความปลอดภัยของเธรดควรหลีกเลี่ยงการเข้าถึงทรัพยากรที่ใช้ร่วมกันนี้ในสภาพแวดล้อมแบบมัลติเธรด
2. วิธีการซิงโครไนซ์
การปรับเปลี่ยนคำหลักที่ซิงโครไนซ์จะถูกเพิ่มลงในนิยามวิธีการสำหรับการเข้าถึงทรัพยากรที่ใช้ร่วมกันทำให้วิธีนี้เรียกว่าวิธีการซิงโครไนซ์ สามารถเข้าใจได้อย่างง่ายดายว่าวิธีนี้ถูกล็อคและวัตถุที่ถูกล็อคเป็นวัตถุที่มีวิธีการปัจจุบันอยู่ ในสภาพแวดล้อมแบบมัลติเธรดเมื่อดำเนินการวิธีนี้คุณต้องได้รับการล็อคการซิงโครไนซ์นี้ก่อน (และอย่างมากที่สุดเพียงหนึ่งเธรดสามารถรับได้) เฉพาะเมื่อเธรดดำเนินการวิธีการซิงโครไนซ์นี้จะปล่อยวัตถุล็อคและเธรดอื่น ๆ สามารถรับการล็อคการซิงโครไนซ์นี้และอื่น ๆ ...
ในตัวอย่างข้างต้นทรัพยากรที่ใช้ร่วมกันเป็นวัตถุบัญชีและเมื่อใช้วิธีการซิงโครไนซ์จะสามารถแก้ปัญหาความปลอดภัยของเธรดได้ เพียงเพิ่มคำหลักที่ถูกซิงโครไนซ์ก่อนวิธีการเรียกใช้ ()
เป็นโมฆะที่ซิงโครไนซ์สาธารณะ () {// .... }3. ซิงโครไนซ์บล็อกรหัส
ดังที่วิเคราะห์ไว้ข้างต้นการแก้ปัญหาความปลอดภัยของเธรดจะต้อง จำกัด เพียงความไม่แน่นอนของการเข้าถึงทรัพยากรที่ใช้ร่วมกัน เมื่อใช้วิธีการซิงโครไนซ์ร่างกายวิธีทั้งหมดจะกลายเป็นสถานะการดำเนินการแบบซิงโครนัสซึ่งอาจทำให้ช่วงการซิงโครไนซ์เกิดขึ้น ดังนั้นวิธีการซิงโครไนซ์อื่น - บล็อกรหัสการซิงโครไนซ์ - สามารถแก้ไขได้โดยตรงสำหรับรหัสที่ต้องการการซิงโครไนซ์
รูปแบบของบล็อกรหัสแบบซิงโครนัสคือ:
ซิงโครไนซ์ (obj) {// ... }ในหมู่พวกเขา OBJ เป็นวัตถุล็อคดังนั้นจึงเป็นสิ่งสำคัญในการเลือกวัตถุที่จะล็อค โดยทั่วไปการพูดวัตถุทรัพยากรที่ใช้ร่วมกันนี้ถูกเลือกเป็นวัตถุล็อค
ดังในตัวอย่างข้างต้นควรใช้วัตถุบัญชีเป็นวัตถุล็อค (แน่นอนว่ามันเป็นไปได้ที่จะเลือกสิ่งนี้เนื่องจากเธรดการสร้างใช้วิธีการเรียกใช้งานได้หากเป็นเธรดที่สร้างขึ้นโดยตรงกับวิธีการใช้เธรดการใช้วัตถุนี้เป็นการล็อคการซิงโครไนซ์จะไม่เล่นบทบาทใด ๆ เพราะเป็นวัตถุที่แตกต่างกันดังนั้นคุณต้องระมัดระวังเป็นพิเศษ
4. ล็อคการซิงโครไนซ์วัตถุล็อค
อย่างที่เราเห็นข้างต้นอย่างแม่นยำเพราะเราต้องระมัดระวังเกี่ยวกับการเลือกวัตถุล็อคแบบซิงโครนัสมีวิธีแก้ปัญหาง่าย ๆ หรือไม่? มันสามารถอำนวยความสะดวกในการแยกวัตถุล็อคแบบซิงโครนัสจากทรัพยากรที่ใช้ร่วมกันในขณะเดียวกันก็แก้ปัญหาความปลอดภัยของเธรดได้ดี
การใช้ล็อคการซิงโครไนซ์วัตถุล็อคสามารถแก้ปัญหานี้ได้อย่างง่ายดาย สิ่งเดียวที่ควรทราบคือวัตถุล็อคจำเป็นต้องมีความสัมพันธ์แบบหนึ่งต่อหนึ่งกับวัตถุทรัพยากร รูปแบบทั่วไปของล็อคการซิงโครไนซ์วัตถุล็อคคือ:
คลาส X {// แสดงวัตถุที่กำหนดล็อคการซิงโครไนซ์ล็อคซึ่งมีความสัมพันธ์แบบหนึ่งต่อหนึ่งกับทรัพยากรส่วนตัวที่ใช้ร่วมกันล็อคสุดท้ายล็อค = ใหม่ reentrantlock (); โมฆะสาธารณะ m () {// lock lock.lock (); // ... รหัสที่ต้องใช้การซิงโครไนซ์แบบเธรดที่ปลอดภัย // ปล่อยล็อคล็อคล็อคล็อค (); -5. WAIT ()/NOTIFY ()/NOTIFYALL () การสื่อสารเธรด
ทั้งสามวิธีนี้ถูกกล่าวถึงในโพสต์บล็อก "Java Summary Series: Java.lang.Object" แม้ว่าวิธีการทั้งสามนี้ส่วนใหญ่จะใช้ในการมัลติเธรด แต่เป็นวิธีการท้องถิ่นในคลาสวัตถุ ดังนั้นในทางทฤษฎีวัตถุวัตถุใด ๆ สามารถใช้เป็นโทนหลักของวิธีการทั้งสามนี้ ในการเขียนโปรแกรมแบบมัลติเธรดจริงโดยการซิงโครไนซ์วัตถุล็อคเพื่อปรับแต่งวิธีการทั้งสามนี้เท่านั้นที่สามารถเธรดการสื่อสารระหว่างหลายเธรดจะเสร็จสมบูรณ์
รอ (): ทำให้เธรดปัจจุบันรอและทำให้เข้าสู่สถานะการปิดกั้นการรอ จนกว่าเธรดอื่นจะเรียกวิธีการแจ้งเตือน () หรือ notifyall () ของวัตถุล็อคซิงโครนัสเพื่อปลุกเธรด
แจ้งเตือน (): ปลุกเธรดเดียวที่รออยู่บนวัตถุล็อคแบบซิงโครนัสนี้ หากหลายเธรดกำลังรอวัตถุล็อคแบบซิงโครนัสนี้หนึ่งในเธรดจะถูกเลือกสำหรับการทำงานปลุก เฉพาะเมื่อเธรดปัจจุบัน abandons ล็อคบนวัตถุการล็อคแบบซิงโครนัสสามารถดำเนินการเธรดที่ตื่นขึ้นมาได้
NotifyAll (): ปลุกเธรดทั้งหมดที่รออยู่บนวัตถุล็อคแบบซิงโครนัสนี้ เฉพาะเมื่อเธรดปัจจุบัน abandons ล็อคบนวัตถุการล็อคแบบซิงโครนัสสามารถดำเนินการเธรดที่ตื่นขึ้นมาได้
แพ็คเกจ com.qqyumidi; Threadtest คลาสสาธารณะ {โมฆะคงที่สาธารณะหลัก (สตริง [] args) {บัญชีบัญชี = บัญชีใหม่ ("123456", 0); เธรด drawMoneyThread = ใหม่ drawMoneyThread ("รับเธรดเงิน" บัญชี 700); เธรดเงินฝาก ฯลฯ = เงินฝากใหม่ steposithread ("ประหยัดเงินเธรด" บัญชี 700); drawMoneyThread.start (); เงินฝาก start.start (); }} คลาส drawMoneyThread ขยายเธรด {บัญชีส่วนตัว; จำนวนสองเท่าส่วนตัว; Public DrawMoneyThread (String ThreadName, บัญชีบัญชี, จำนวนเงินสองเท่า) {super (threadName); this.account = บัญชี; this.amount = จำนวน; } โมฆะสาธารณะเรียกใช้ () {สำหรับ (int i = 0; i <100; i ++) {account.draw (จำนวน, i); }}} คลาส depositemoneyThread ขยายเธรด {บัญชีส่วนตัว; จำนวนสองเท่าส่วนตัว; Public DepositMoneyThread (String ThreadName, บัญชีบัญชี, จำนวนเงินสองเท่า) {super (threadname); this.account = บัญชี; this.amount = จำนวน; } โมฆะสาธารณะเรียกใช้ () {สำหรับ (int i = 0; i <100; i ++) {account.deposit (จำนวน, i); }}} บัญชีคลาส {Private String AccountNo; สมดุลสองเท่าส่วนตัว; // ระบุว่ามีการฝากเงินในธงบูลีนส่วนตัวหรือไม่ = เท็จ; บัญชีสาธารณะ () {} บัญชีสาธารณะ (String AccountNo, Double Balance) {this.accountNo = accountNo; this.balance = Balance; } สตริงสาธารณะ getAccountNo () {return accountno; } โมฆะสาธารณะ setAccountNo (String AccountNo) {this.AccountNo = accountNo; } public double getBalance () {Balance return; } โมฆะสาธารณะ setBalance (สมดุลสองเท่า) {this.balance = สมดุล; } / ** * ประหยัดเงิน * * @param เงินฝาก * / การฝากโมฆะแบบซิงโครไนซ์สาธารณะ (เงินฝากสองครั้ง, int i) {ถ้า (ธง) {// ใครบางคนในบัญชีได้บันทึกเงินไปแล้ว รอ(); // 1 system.out.println (thread.currentthread (). getName () + "ดำเนินการรอการดำเนินการ" + " - i =" + i); } catch (interruptedException e) {e.printStackTrace (); }} else {// เริ่มบันทึก system.out.println (thread.currentthread (). getName () + "เงินฝาก:" + เงินฝาก + " - i =" + i); SetBalance (ยอดคงเหลือ + เงินฝาก); ธง = จริง; // ปลุกเธรดอื่น ๆ แจ้งเตือน (); // 2 ลอง {thread.sleep (3000); } catch (interruptedException e) {e.printStackTrace (); } system.out.println (thread.currentthread (). getName () + "- บันทึกเงิน- การดำเนินการเสร็จสมบูรณ์" + "- i =" + i); }} / ** * ถอนเงิน * * @param drawAmount * / void synchronized public (การดึงสองครั้ง, int i) {ถ้า (! ธง) {// ไม่มีใครในบัญชีที่บันทึกเงินและเธรดปัจจุบันต้องรอการดำเนินการ " ฉัน); รอ(); System.out.println (thread.currentthread (). getName () + "ดำเนินการรอการดำเนินการ" + "ดำเนินการรอการดำเนินการ" + " - i =" + i); } catch (interruptedException e) {e.printStackTrace (); }} else {// เริ่มถอนเงิน money.out.println (thread.currentthread (). getName () + "ถอนเงิน:" + drawAmount + " - i =" + i); setBalance (getBalance () - drawamount); ธง = เท็จ; // ปลุกเธรดอื่น ๆ แจ้งเตือน (); System.out.println (thread.currentthread (). getName () + "-ถอนเงิน-execution เสร็จสมบูรณ์" + "-i =" + i); // 3}}} ตัวอย่างข้างต้นแสดงให้เห็นถึงการใช้งานของ WAIT ()/NOTIFY ()/NOTIFYALL () ผลลัพธ์ผลลัพธ์บางอย่างคือ:
เธรดการถอนเงินเริ่มดำเนินการรอการรอและดำเนินการรอการรอ- i = 0
การบันทึกการฝากเธรด: 700.0 - i = 0
ประหยัดเงิน-การตรวจสอบเงิน-ฉัน = 0
เธรดประหยัดเงินต้องดำเนินการรอ- i = 1
เธรดการถอนเงินดำเนินการรอการดำเนินการและรอการดำเนินการ- i = 0
ถอนเงินเงินถอนเงิน: 700.0 - i = 1
การถอนเงินเธรด-การดราวัล-execution-ฉัน = 1
เธรดที่จะถอนเงินจะต้องเริ่มดำเนินการรอและดำเนินการรอการรอ- i = 2
เธรดประหยัดเงินดำเนินการรอการดำเนินการ- i = 1
การบันทึกด้ายฝาก: 700.0 - i = 2
ประหยัดเงิน-การตรวจสอบเงิน-ฉัน = 2
เธรดการถอนเรียกใช้งานการรอคอยและดำเนินการดำเนินการรอ- i = 2
ถอนเงินเงินถอนเงินถอนเงิน: 700.0 - i = 3
การถอนเงินเธรด-การดราวัล-execution-ฉัน = 3
เธรดเพื่อถอนเงินจะต้องดำเนินการรอการรอและดำเนินการรอการรอ- i = 4
บันทึกการฝากเธรด: 700.0 - i = 3
ประหยัดเงินการใช้เงินแบบด้าย-ฉัน = 3
เธรดประหยัดเงินต้องดำเนินการรอ- i = 4
เธรดการถอนเงินดำเนินการรอการดำเนินการและรอการดำเนินการ- i = 4
ถอนเงินเงินถอนเงินออก: 700.0 - i = 5
การถอนเงินเธรด-การดราวัล-execution-ฉัน = 5
เธรดที่จะถอนเงินต้องเริ่มดำเนินการรอและดำเนินการรอการดำเนินการ- i = 6
เธรดประหยัดเงินดำเนินการรอการดำเนินการ- i = 4
การบันทึกด้ายฝาก: 700.0 - i = 5
ประหยัดเงิน-การตรวจสอบเงิน-ฉัน = 5
เธรดประหยัดเงินต้องดำเนินการรอ- i = 6
เธรดการถอนเงินดำเนินการรอการดำเนินการและรอการดำเนินการ- i = 6
ถอนเงินเงินถอนเงินออก: 700.0 - i = 7
การถอนเงินเธรด-การดราวัล-execution-ฉัน = 7
เธรดการถอนเงินเริ่มดำเนินการรอการรอและดำเนินการรอการรอ-ฉัน = 8
เธรดประหยัดเงินดำเนินการรอการดำเนินการ- i = 6
การบันทึกการฝากเธรด: 700.0 - i = 7
ดังนั้นเราต้องให้ความสนใจกับประเด็นต่อไปนี้:
1. หลังจากดำเนินการวิธีการรอ () เธรดปัจจุบันจะเข้าสู่สถานะการปิดกั้นการรอคอยทันทีและรหัสที่ตามมาจะไม่ถูกดำเนินการ;
2. หลังจากวิธีการแจ้งเตือน ()/notifyall () ถูกดำเนินการแล้ววัตถุเธรด (any-notify ()/all-notifyall ()) บนวัตถุล็อคการซิงโครไนซ์นี้จะถูกปลุก อย่างไรก็ตามวัตถุล็อคการซิงโครไนซ์ไม่ได้ถูกปล่อยออกมาในเวลานี้ กล่าวคือหากมีรหัสอยู่เบื้องหลังการแจ้งเตือน ()/notifyall () มันจะดำเนินการต่อไป เฉพาะเมื่อมีการดำเนินการเธรดปัจจุบันวัตถุล็อคการซิงโครไนซ์จะถูกปล่อยออกมา
3. หลังจากแจ้ง ()/notifyall () ถูกดำเนินการหากมีวิธีการนอนหลับ () ทางด้านขวาเธรดปัจจุบันจะเข้าสู่สถานะการปิดกั้น แต่การล็อควัตถุซิงโครไนซ์ไม่ได้ถูกปล่อยออกมาและยังคงอยู่ด้วยตัวเอง จากนั้นเธรดจะยังคงดำเนินการต่อไปหลังจากระยะเวลาหนึ่ง 2 ถัดไป;
4. รอ ()/แจ้งเตือน ()/nitifyall () เสร็จสิ้นการสื่อสารหรือการทำงานร่วมกันระหว่างเธรดตามการล็อควัตถุที่แตกต่างกัน ดังนั้นหากเป็นล็อควัตถุซิงโครไนซ์ที่แตกต่างกันมันจะสูญเสียความหมาย ในเวลาเดียวกันการล็อควัตถุซิงโครไนซ์ดีที่สุดในการรักษาความสัมพันธ์แบบหนึ่งต่อหนึ่งกับวัตถุทรัพยากรที่ใช้ร่วมกัน
5. เมื่อเธรดรอตื่นขึ้นมาและดำเนินการรหัสวิธีการรอ () ที่ดำเนินการครั้งสุดท้ายยังคงดำเนินการต่อไป
แน่นอนตัวอย่างข้างต้นนั้นค่อนข้างง่ายเพียงแค่ใช้วิธีการรอ ()/แจ้ง ()/noitifyall () แต่ในสาระสำคัญมันเป็นโมเดลผู้ผลิตผู้ผลิตอย่างง่ายอยู่แล้ว
ชุดของบทความ:
คำอธิบายของอินสแตนซ์หลายเธรด Java (i)
คำอธิบายโดยละเอียดเกี่ยวกับอินสแตนซ์แบบมัลติเธรด Java (II)
คำอธิบายโดยละเอียดเกี่ยวกับอินสแตนซ์แบบมัลติเธรด Java (iii)