หากมูลนิธิ Java ของคุณอ่อนแอหรือคุณไม่มีความเข้าใจที่ดีเกี่ยวกับ Java multithreading โปรดอ่านบทความนี้ "เรียนรู้คำจำกัดความของเธรดสถานะและคุณสมบัติของ Java multithreading"
การซิงโครไนซ์เป็นจุดที่ยากสำหรับการหลายเธรด Java และไม่ค่อยมีการใช้เมื่อเราทำการพัฒนา Android แต่นี่ไม่ใช่เหตุผลว่าทำไมเราไม่คุ้นเคยกับการซิงโครไนซ์ ฉันหวังว่าบทความนี้จะช่วยให้ผู้คนเข้าใจและใช้การซิงโครไนซ์ Java ได้มากขึ้น
ในแอปพลิเคชันหลายเธรดเธรดสองตัวขึ้นไปจำเป็นต้องแชร์การเข้าถึงข้อมูลเดียวกัน สิ่งนี้มักจะกลายเป็นเงื่อนไขการแข่งขันหากสองเธรดเข้าถึงวัตถุเดียวกันและแต่ละเธรดเรียกวิธีการที่ปรับเปลี่ยนวัตถุ
ตัวอย่างที่ง่ายที่สุดของเงื่อนไขการแข่งขันคือ: ตัวอย่างเช่นตั๋วรถไฟมีความแน่นอน แต่มีหน้าต่างสำหรับการขายตั๋วรถไฟทุกที่หน้าต่างแต่ละหน้าต่างเทียบเท่ากับหนึ่งเธรดและเธรดจำนวนมากแบ่งปันทรัพยากรตั๋วรถไฟทั้งหมด และไม่สามารถรับประกันความเป็นอะตอมของมันได้ หากสองเธรดใช้ทรัพยากรนี้ในเวลาหนึ่งตั๋วรถไฟที่พวกเขานำออกมาเหมือนกัน (หมายเลขที่นั่งเหมือนกัน) ซึ่งจะทำให้เกิดปัญหาสำหรับผู้โดยสาร วิธีแก้ปัญหาคือเมื่อเธรดต้องการใช้ทรัพยากรตั๋วรถไฟเราให้ล็อคและหลังจากเสร็จสิ้นการทำงานเราจะให้ล็อคไปยังเธรดอื่นที่ต้องการใช้ทรัพยากรนี้ ด้วยวิธีนี้สถานการณ์ข้างต้นจะไม่เกิดขึ้น
1. ล็อควัตถุ
คำหลักที่ซิงโครไนซ์จะให้ล็อคและเงื่อนไขที่เกี่ยวข้องโดยอัตโนมัติ มันสะดวกมากที่จะใช้การซิงโครไนซ์ในกรณีส่วนใหญ่ที่จำเป็นต้องล็อคอย่างชัดเจน อย่างไรก็ตามเมื่อเราเข้าใจคลาส reentrantlock และวัตถุที่มีเงื่อนไขเราสามารถเข้าใจคำหลักที่ซิงโครไนซ์ได้ดีขึ้น Reentrantlock เปิดตัวใน Java SE 5.0 โครงสร้างของบล็อกรหัสได้รับการปกป้องโดยใช้ reentrantlock ดังนี้:
mlock.lock (); ลอง {... } ในที่สุด {mlock.unlock ();}โครงสร้างนี้ช่วยให้มั่นใจได้ว่ามีเธรดเดียวเท่านั้นที่เข้าสู่พื้นที่วิกฤตได้ตลอดเวลา เมื่อเธรดบล็อกวัตถุล็อคจะไม่มีเธรดอื่นใดสามารถผ่านคำสั่งล็อคได้ เมื่อเธรดอื่นโทรล็อคพวกเขาจะถูกบล็อกจนกว่าเธรดแรกจะปล่อยวัตถุล็อค จำเป็นอย่างยิ่งที่จะต้องทำการปลดล็อคในที่สุด หากข้อยกเว้นเกิดขึ้นในพื้นที่วิกฤตจะต้องปล่อยล็อคมิฉะนั้นเธรดอื่น ๆ จะถูกบล็อกตลอดไป
2. วัตถุที่มีเงื่อนไข <br /> เมื่อเข้าสู่พื้นที่วิกฤตพบว่าสามารถดำเนินการได้หลังจากเงื่อนไขที่แน่นอน ใช้วัตถุที่มีเงื่อนไขเพื่อจัดการเธรดที่ได้รับการล็อค แต่ไม่สามารถทำงานที่มีประโยชน์ได้ วัตถุที่มีเงื่อนไขเรียกว่าตัวแปรเงื่อนไข
มาดูตัวอย่างต่อไปนี้เพื่อดูว่าทำไมวัตถุมีเงื่อนไขจึงจำเป็น
สมมติว่าในสถานการณ์ที่เราต้องใช้การโอนเงินธนาคารก่อนอื่นเราจะเขียนชั้นเรียนธนาคารและตัวสร้างจะต้องถูกโอนไปยังจำนวนบัญชีและจำนวนบัญชี
ธนาคารชั้นสาธารณะ {บัญชีเอกชนคู่ []; ล็อคส่วนตัวแบงก์ล็อค; ธนาคารสาธารณะ (int n, การเริ่มต้นสองครั้ง) {บัญชี = ใหม่สองเท่า [n]; banklock = ใหม่ reentrantlock (); สำหรับ (int i = 0; i <accounts.length; i ++) {บัญชี [i] = itied -balance; -ต่อไปเราต้องการถอนเงินและเขียนวิธีการถอน จากคือผู้โอนไปยังเป็นตัวรับและจำนวนการโอนจำนวนเงิน เป็นผลให้เราพบว่ายอดคงเหลือของผู้โอนไม่เพียงพอ หากเธรดอื่นประหยัดเงินให้กับผู้โอนเพียงพอการโอนสามารถประสบความสำเร็จ อย่างไรก็ตามเธรดนี้ได้รับการล็อคซึ่งเป็นเอกสิทธิ์และเธรดอื่น ๆ ไม่สามารถรับล็อคเพื่อดำเนินการฝากเงินได้ นี่คือเหตุผลที่เราต้องแนะนำวัตถุที่มีเงื่อนไข
การถ่ายโอนโมฆะสาธารณะ (int จาก, int ถึงจำนวน int) {banklock.lock (); ลอง {ในขณะที่ (บัญชี [จาก] <จำนวนเงิน) {// wait}} ในที่สุด {banklock.unlock (); -วัตถุล็อคมีวัตถุเงื่อนไขที่เกี่ยวข้องหลายอย่าง คุณสามารถใช้วิธีการใหม่เพื่อให้ได้วัตถุเงื่อนไข หลังจากที่เราได้รับวัตถุเงื่อนไขเราเรียกวิธีการรอคอยและเธรดปัจจุบันจะถูกบล็อกและล็อคถูกทอดทิ้ง
ธนาคารชั้นสาธารณะ {บัญชีเอกชนคู่ []; ล็อคส่วนตัวแบงก์ล็อค; เงื่อนไขส่วนตัว; ธนาคารสาธารณะ (int n, การเริ่มต้นสองครั้ง) {บัญชี = ใหม่สองเท่า [n]; banklock = ใหม่ reentrantlock (); // รับเงื่อนไขเงื่อนไขวัตถุ = banklock.newCondition (); สำหรับ (int i = 0; i <accounts.length; i ++) {บัญชี [i] = itied -balance; }} การถ่ายโอนโมฆะสาธารณะ (int จาก, int to, จำนวน int) พ่น interruptedException {banklock.lock (); ลอง {ในขณะที่ (บัญชี [จาก] <จำนวนเงิน) {// บล็อกเธรดปัจจุบันและให้เงื่อนไขการล็อค Await (); }} ในที่สุด {banklock.unlock (); - เธรดที่รอการล็อคนั้นแตกต่างจากเธรดที่เรียกวิธีการรอคอย เมื่อเธรดเรียกวิธีการรอคอยมันจะเข้าสู่ชุดรอของเงื่อนไขนั้น เมื่อล็อคพร้อมใช้งานเธรดไม่สามารถปลดล็อคได้ทันที แต่แทนที่จะอยู่ในสถานะการปิดกั้นจนกว่าเธรดอื่นจะเรียกวิธี SignalAll ในสภาพเดียวกัน เมื่อเธรดอื่นพร้อมที่จะโอนเงินไปยังผู้โอนก่อนหน้าของเราเพียงแค่โทรเงื่อนไข signalall (); การโทรนี้จะเปิดใช้งานเธรดทั้งหมดที่รอเงื่อนไขนี้อีกครั้ง
เมื่อเธรดเรียกวิธีการรอคอยมันจะไม่สามารถเปิดใช้งานตัวเองอีกครั้งและหวังว่าเธรดอื่น ๆ จะเรียกวิธีการ SignalAll เพื่อเปิดใช้งานตัวเอง หากไม่มีเธรดอื่นเปิดใช้งานเธรดรอการหยุดชะงักจะเกิดขึ้น หากเธรดอื่น ๆ ทั้งหมดถูกบล็อกและการเรียกเธรดที่ใช้งานล่าสุดจะรอก่อนที่จะยกเลิกการปิดกั้นเธรดอื่น ๆ ก็จะถูกบล็อกด้วย ไม่มีเธรดที่สามารถปลดบล็อกเธรดอื่น ๆ และโปรแกรมจะถูกระงับ
แล้ว SignalAll จะถูกเรียกเมื่อไหร่? โดยปกติแล้วควรเป็นประโยชน์ในการโทรหา SignalAll เมื่อรอทิศทางของเธรดเพื่อเปลี่ยน ในตัวอย่างนี้เมื่อยอดคงเหลือในบัญชีเปลี่ยนเธรดการรอควรมีโอกาสตรวจสอบยอดคงเหลือ
การถ่ายโอนโมฆะสาธารณะ (int จาก, จำนวน int, จำนวน int) พ่น InterruptedException {banklock.lock (); ลอง {ในขณะที่ (บัญชี [จาก] <จำนวนเงิน) {// บล็อกเธรดปัจจุบันและให้เงื่อนไขการล็อค Await (); } // การถ่ายโอนการดำเนินการ ... เงื่อนไข. signalall (); } ในที่สุด {banklock.unlock (); -เมื่อมีการเรียกวิธีการ SignalAll เธรดที่รอจะไม่เปิดใช้งานทันที มันเพียงแค่ปลดบล็อกเธรดที่รอคอยเพื่อให้เธรดเหล่านี้สามารถเข้าถึงวัตถุได้โดยการแข่งขันหลังจากเธรดปัจจุบันออกจากวิธีการซิงโครนัส อีกวิธีหนึ่งคือสัญญาณซึ่งจะปลดบล็อกแบบสุ่ม หากเธรดยังคงไม่ทำงานมันจะถูกบล็อกอีกครั้ง หากไม่มีสัญญาณการเรียกเธรดอื่นอีกครั้งระบบจะถูกปิดกั้น
3. คำหลักที่ซิงโครไนซ์
อินเทอร์เฟซล็อคและเงื่อนไขให้โปรแกรมเมอร์ที่มีการควบคุมการล็อคระดับสูงอย่างไรก็ตามในกรณีส่วนใหญ่ไม่จำเป็นต้องมีการควบคุมดังกล่าวและกลไกที่ฝังอยู่ภายในภาษา Java สามารถใช้งานได้ เริ่มต้นจาก Java เวอร์ชัน 1.0 วัตถุทุกชิ้นใน Java มีล็อคภายใน หากมีการประกาศวิธีการด้วยคำหลักที่ซิงโครไนซ์การล็อคของวัตถุจะปกป้องวิธีทั้งหมด นั่นคือการเรียกใช้วิธีนี้เธรดจะต้องได้รับการล็อควัตถุภายใน
กล่าวอีกนัยหนึ่ง
โมฆะแบบซิงโครไนซ์สาธารณะ () {}เทียบเท่ากับ
โมฆะสาธารณะวิธี () {this.lock.lock (); ลอง {} ในที่สุด {this.lock.unlock ();} ในตัวอย่างของธนาคารด้านบนเราสามารถประกาศวิธีการโอนของชั้นเรียนธนาคารเป็นซิงโครไนซ์แทนที่จะใช้ล็อคที่แสดง
มีเพียงหนึ่งเงื่อนไขที่เกี่ยวข้องสำหรับการล็อควัตถุภายใน รอการขยายตัวจะถูกเพิ่มเข้าไปในเธรดในชุดรอ แจ้งเตือนหรือแจ้งวิธีการปลดบล็อกเธรดที่รอ กล่าวอีกนัยหนึ่งการรอนั้นเทียบเท่ากับเงื่อนไขการโทร Aawait () แจ้งเตือนเทียบเท่ากับเงื่อนไข signalall ();
วิธีการถ่ายโอนตัวอย่างของเราสามารถเขียนได้เช่นนี้:
การถ่ายโอนโมฆะแบบซิงโครไนซ์สาธารณะ (int จาก, int to, จำนวน int) พ่น InterruptedException {ในขณะที่ (บัญชี [จาก] <จำนวน) {wait (); } // การถ่ายโอนการดำเนินการ ... NotifyAll (); -คุณจะเห็นว่าการใช้คำหลักที่ซิงโครไนซ์เพื่อเขียนโค้ดนั้นง่ายกว่ามาก แน่นอนเพื่อทำความเข้าใจรหัสนี้คุณต้องเข้าใจว่าแต่ละวัตถุมีการล็อคภายในและล็อคมีเงื่อนไขภายใน ล็อคจัดการเธรดเหล่านั้นที่พยายามป้อนวิธีการซิงโครไนซ์และเงื่อนไขจะจัดการเธรดเหล่านั้นที่โทรหา
4. การปิดกั้นแบบซิงโครนัส <br /> ข้างต้นเราบอกว่าวัตถุ Java ทุกชิ้นมีล็อคและเธรดสามารถเรียกวิธีการซิงโครไนซ์เพื่อรับล็อคและมีกลไกอื่นที่จะได้รับการล็อค โดยการป้อนบล็อกการซิงโครไนซ์เมื่อเธรดเข้าสู่รูปแบบการบล็อกต่อไปนี้:
ซิงโครไนซ์ (obj) {}ดังนั้นเขาจึงได้ล็อคของ OBJ มาดูชั้นเรียนธนาคารกันเถอะ
ธนาคารชั้นเรียนสาธารณะ {บัญชีสองส่วนส่วนตัว [] บัญชีส่วนตัวล็อควัตถุส่วนตัว = วัตถุใหม่ (); ธนาคารสาธารณะ (int n, การเริ่มต้นสองครั้ง) {บัญชี = ใหม่สองเท่า [n]; สำหรับ (int i = 0; i <accounts.length; i ++) {บัญชี [i] = itied -balance; }} การถ่ายโอนโมฆะสาธารณะ (int จาก, int to, จำนวน int) {ซิงโครไนซ์ (ล็อค) {// การถ่ายโอนการดำเนินการ ... }}}}ที่นี่การสร้างวัตถุล็อคใช้เพื่อใช้ล็อคที่จัดขึ้นโดยแต่ละวัตถุ Java บางครั้งนักพัฒนาซอฟต์แวร์ใช้ล็อคของวัตถุเพื่อใช้การดำเนินการอะตอมเพิ่มเติมที่เรียกว่าการล็อคไคลเอนต์ ตัวอย่างเช่นคลาสเวกเตอร์วิธีการของมันเป็นแบบซิงโครนัส ตอนนี้สมมติว่ายอดคงเหลือของธนาคารจะถูกเก็บไว้ในเวกเตอร์
การถ่ายโอนโมฆะสาธารณะ (Vector <bortion> บัญชี, int จาก, int to, จำนวน int) {accounts.set (จาก, accounts.get (จาก) -amount); accounts.set (ถึง, accounts.get (ถึง)+จำนวน;}วิธีการรับและการตั้งค่าของคลาส Vecror นั้นเป็นแบบซิงโครนัส แต่สิ่งนี้ไม่ได้ช่วยเรา หลังจากการโทรครั้งแรกเพื่อให้เสร็จสมบูรณ์เป็นไปได้ทั้งหมดที่เธรดหนึ่งถูกปฏิเสธสิทธิ์ในการทำงานในวิธีการถ่ายโอนดังนั้นเธรดอื่นอาจเก็บค่าที่แตกต่างกันในตำแหน่งที่เก็บข้อมูลเดียวกัน แต่เราสามารถสกัดกั้นล็อคนี้ได้
การถ่ายโอนโมฆะสาธารณะ (Vector <bortu> บัญชี, int จาก, int ถึง, จำนวน int) {ซิงโครไนซ์ (บัญชี) {accounts.set (จาก, บัญชี get.get (จาก) -amount); accounts.set (ถึง, accounts.get (ถึง)+จำนวน;}}การล็อคไคลเอนต์ (บล็อกรหัสแบบซิงโครนัส) มีความเปราะบางมากและไม่แนะนำให้ใช้ โดยทั่วไปจะเป็นการดีที่สุดที่จะใช้คลาสที่ให้ไว้ภายใต้แพ็คเกจ java.util.concurrent เช่นการปิดกั้นคิว หากวิธีการซิงโครไนซ์เหมาะสำหรับโปรแกรมของคุณโปรดลองใช้วิธีการซิงโครไนซ์ สามารถลดจำนวนรหัสที่เขียนและลดโอกาสของข้อผิดพลาด หากคุณต้องการใช้คุณสมบัติเฉพาะที่จัดทำโดยโครงสร้างล็อค/เงื่อนไขให้ใช้ล็อค/เงื่อนไขเท่านั้น
ข้างต้นเป็นเรื่องเกี่ยวกับบทความนี้ฉันหวังว่ามันจะเป็นประโยชน์กับการเรียนรู้ของทุกคน