คำนำ
บางครั้งหากคุณใช้การซิงโครไนซ์เพียงเพื่ออ่านและเขียนฟิลด์อินสแตนซ์หนึ่งหรือสองฟิลด์ดูเหมือนว่าแพงเกินไป คำหลักที่ผันผวนมีกลไกที่ปราศจากล็อคสำหรับการเข้าถึงฟิลด์อินสแตนซ์แบบซิงโครนัส หากโดเมนถูกประกาศว่าเป็นความผันผวนคอมไพเลอร์และเครื่องเสมือนรู้ว่าโดเมนอาจได้รับการปรับปรุงพร้อมกับเธรดอื่น ก่อนที่จะพูดคุยเกี่ยวกับคำหลักที่ผันผวนเราจำเป็นต้องเข้าใจแนวคิดที่เกี่ยวข้องของโมเดลหน่วยความจำและคุณสมบัติทั้งสามในการเขียนโปรแกรมพร้อมกัน: ความผิดปกติการมองเห็นและความเป็นระเบียบ
1. โมเดลหน่วยความจำ Java ที่มีความเป็นอะตอมการมองเห็นและความเป็นระเบียบ
โมเดลหน่วยความจำ Java กำหนดว่าตัวแปรทั้งหมดมีอยู่ในหน่วยความจำหลักและแต่ละเธรดมีหน่วยความจำในการทำงานของตัวเอง การดำเนินการทั้งหมดของเธรดบนตัวแปรจะต้องดำเนินการในหน่วยความจำการทำงานและไม่สามารถทำงานได้โดยตรงกับหน่วยความจำหลัก และแต่ละเธรดไม่สามารถเข้าถึงหน่วยความจำที่ใช้งานได้ของเธรดอื่น ๆ
ใน Java ดำเนินการข้อความต่อไปนี้:
int i = 3;
เธรดการดำเนินการจะต้องกำหนดบรรทัดแคชซึ่งตัวแปรที่ฉันอยู่ในเธรดการทำงานของตัวเองจากนั้นเขียนลงในหน่วยความจำหลัก แทนที่จะเขียนค่า 3 ลงในหน่วยความจำหลักโดยตรง
ดังนั้นภาษาชวาที่รับประกันได้ว่าจะมีความเป็นปรมาณูทัศนวิสัยและความเป็นระเบียบอย่างไร?
อะตอม
การดำเนินการอ่านและการกำหนดตัวแปรของชนิดข้อมูลพื้นฐานคือการดำเนินการอะตอมนั่นคือการดำเนินการเหล่านี้ไม่สามารถขัดจังหวะและดำเนินการหรือไม่
มาดูรหัสต่อไปนี้:
x = 10; // คำสั่ง 1y = x; // คำสั่ง 2x ++; // คำสั่ง 3x = x + 1; // คำสั่ง 4
คำแถลง 1 เท่านั้นคือการดำเนินการอะตอมและไม่มีคำแถลงอีกสามข้อความที่เป็นการดำเนินการอะตอม
คำแถลงที่ 2 มีการดำเนินการ 2 ครั้ง ก่อนอื่นต้องอ่านค่าของ X จากนั้นเขียนค่าของ X ไปยังหน่วยความจำที่ใช้งานได้ แม้ว่าการดำเนินการทั้งสองของการอ่านค่าของ X และการเขียนค่าของ X ไปยังหน่วยความจำที่ทำงานคือการดำเนินการอะตอม
ในทำนองเดียวกัน x ++ และ x = x+1 รวมถึง 3 การดำเนินการ: อ่านค่าของ x ดำเนินการของการเพิ่ม 1 และเขียนค่าใหม่
กล่าวอีกนัยหนึ่งการอ่านและการมอบหมายง่าย ๆ เท่านั้น (และจำนวนจะต้องได้รับการกำหนดให้กับตัวแปรและการกำหนดร่วมกันระหว่างตัวแปรไม่ใช่การดำเนินการอะตอม) เป็นการดำเนินการอะตอม
มีหลายคลาสใน java.util.concurrent.atomic แพ็คเกจที่ใช้คำแนะนำระดับเครื่องจักรที่มีประสิทธิภาพมาก (แทนที่จะล็อค) เพื่อให้แน่ใจว่าอะตอมของการดำเนินการอื่น ๆ ตัวอย่างเช่นคลาส Atomicinteger ให้วิธีการเพิ่มขึ้นและ decrementandget ซึ่งเพิ่มขึ้นตามลำดับและลดจำนวนเต็มในลักษณะอะตอม คลาส Atomicinteger สามารถใช้อย่างปลอดภัยเป็นตัวนับที่ใช้ร่วมกันโดยไม่ต้องซิงโครไนซ์
นอกจากนี้แพ็คเกจนี้ยังมีคลาสอะตอมเช่น Atomicboolean, Atomiclong และ Atomicreference สำหรับโปรแกรมเมอร์ระบบที่พัฒนาเครื่องมือพร้อมกันเท่านั้นและโปรแกรมเมอร์แอปพลิเคชันไม่ควรใช้คลาสเหล่านี้
การมองเห็น
การมองเห็นหมายถึงการมองเห็นระหว่างเธรดและสถานะที่แก้ไขของเธรดหนึ่งสามารถมองเห็นได้ในเธรดอื่น นั่นคือผลลัพธ์ของการปรับเปลี่ยนเธรด จะเห็นเธรดอื่นทันที
เมื่อตัวแปรที่ใช้ร่วมกันได้รับการแก้ไขโดยความผันผวนจะช่วยให้มั่นใจได้ว่าค่าที่แก้ไขจะได้รับการปรับปรุงเป็นหน่วยความจำหลักทันทีดังนั้นจึงสามารถมองเห็นเธรดอื่นได้ เมื่อเธรดอื่นต้องอ่านมันจะอ่านค่าใหม่ในหน่วยความจำ
อย่างไรก็ตามตัวแปรที่ใช้ร่วมกันทั่วไปไม่สามารถรับประกันการมองเห็นได้เนื่องจากไม่แน่นอนเมื่อตัวแปรที่ใช้ร่วมกันปกติถูกเขียนไปยังหน่วยความจำหลักหลังจากแก้ไขแล้ว เมื่อเธรดอื่นอ่านค่าเดิมอาจยังอยู่ในหน่วยความจำดังนั้นจึงไม่สามารถรับประกันการมองเห็นได้
เป็นระเบียบ
ในโมเดลหน่วยความจำ Java คอมไพเลอร์และโปรเซสเซอร์ได้รับอนุญาตให้จัดลำดับคำแนะนำใหม่ แต่กระบวนการจัดเรียงใหม่จะไม่ส่งผลกระทบต่อการดำเนินการของโปรแกรมเธรดเดี่ยว แต่จะส่งผลกระทบต่อความถูกต้องของการดำเนินการพร้อมกันหลายเธรด
คำหลักที่ผันผวนสามารถใช้เพื่อให้แน่ใจว่า "orderline" ที่แน่นอน นอกจากนี้ยังสามารถใช้การซิงโครไนซ์และล็อคเพื่อให้แน่ใจว่ามีการสั่งซื้อ เห็นได้ชัดว่าซิงโครไนซ์และล็อคตรวจสอบให้แน่ใจว่ามีเธรดที่ดำเนินการรหัสการซิงโครไนซ์ในแต่ละช่วงเวลาซึ่งเทียบเท่ากับการปล่อยให้เธรดดำเนินการรหัสการซิงโครไนซ์ตามลำดับ
2. คำหลักที่ผันผวน
เมื่อตัวแปรที่ใช้ร่วมกัน (ตัวแปรสมาชิกชั้นเรียนตัวแปรสมาชิกระดับสแตติก) จะถูกแก้ไขโดยผันผวนจะมีความหมายสองชั้น:
มาดูรหัสชิ้นหนึ่งก่อน หากเธรด 1 ถูกเรียกใช้ก่อนและเธรด 2 จะถูกดำเนินการในภายหลัง:
// เธรด 1boolean stop = false; ในขณะที่ (! หยุด) {dosomething ();} // เธรด 2stop = true; หลายคนอาจใช้วิธีการมาร์กอัปนี้เมื่อขัดจังหวะเธรด แต่อันที่จริงแล้วรหัสนี้จะทำงานได้อย่างถูกต้องหรือไม่? เธรดจะถูกขัดจังหวะหรือไม่? ไม่จำเป็น บางทีเวลาส่วนใหญ่รหัสนี้สามารถขัดจังหวะเธรดได้ แต่อาจทำให้เธรดไม่ถูกขัดจังหวะ (แม้ว่าความเป็นไปได้นี้จะเล็กมากเมื่อสิ่งนี้เกิดขึ้นมันจะทำให้เกิดการวนซ้ำ)
เหตุใดจึงเป็นไปได้ที่จะทำให้เธรดล้มเหลวในการขัดจังหวะ? แต่ละเธรดมีหน่วยความจำการทำงานของตัวเองในระหว่างกระบวนการทำงาน เมื่อเธรด 1 กำลังทำงานอยู่มันจะคัดลอกค่าของตัวแปรหยุดและใส่ไว้ในหน่วยความจำการทำงานของตัวเอง จากนั้นเมื่อเธรด 2 เปลี่ยนค่าของตัวแปรหยุด แต่ไม่มีเวลาเขียนลงในหน่วยความจำหลักเธรด 2 จะทำสิ่งอื่น ๆ จากนั้นเธรด 1 ไม่ทราบเกี่ยวกับการเปลี่ยนแปลงของเธรด 2 ในตัวแปรหยุดดังนั้นมันจะยังคงวนเวียนอยู่ต่อไป
แต่หลังจากแก้ไขด้วยความผันผวนแล้วมันจะแตกต่างกัน:
การรับประกันความผันผวนของอะตอมหรือไม่?
เรารู้ว่าคำหลักที่ผันผวนทำให้มั่นใจได้ว่าการมองเห็นการดำเนินงาน แต่สามารถระเหยได้มั่นใจได้ว่าการดำเนินการของตัวแปรนั้นเป็นอะตอมหรือไม่?
การทดสอบระดับสาธารณะ {สาธารณะผันผวน int inc = 0; โมฆะสาธารณะเพิ่ม () {inc ++; } โมฆะคงที่สาธารณะหลัก (สตริง [] args) {ทดสอบการทดสอบขั้นสุดท้าย = การทดสอบใหม่ (); สำหรับ (int i = 0; i <10; i ++) {เธรดใหม่ () {โมฆะสาธารณะเรียกใช้ () {สำหรับ (int j = 0; j <1000; j ++) test.increase (); - }.เริ่ม(); } // ตรวจสอบให้แน่ใจว่าเธรดก่อนหน้านี้เสร็จสิ้นการดำเนินการในขณะที่ (thread.activeCount ()> 1) thread.yield (); System.out.println (test.inc); - ผลลัพธ์ของรหัสนี้ไม่สอดคล้องกันทุกครั้งที่มันทำงาน มีจำนวนน้อยกว่า 10,000 ดังที่ได้กล่าวไว้ก่อนหน้านี้การดำเนินการอัตโนมัติไม่ได้เป็นอะตอม มันรวมถึงการอ่านค่าดั้งเดิมของตัวแปรดำเนินการเพิ่มเติม 1 การดำเนินการและการเขียนไปยังหน่วยความจำที่ใช้งานได้ กล่าวคืออาจมีการดำเนินการย่อยทั้งสามของการดำเนินการในตนเองอาจถูกดำเนินการแยกต่างหาก
หากค่าของ Variable Inc เป็น 10 ในเวลาหนึ่งเธรด 1 จะทำการดำเนินการในตัวเองบนตัวแปรเธรด 1 แรกจะอ่านค่าดั้งเดิมของตัวแปร Inc จากนั้นเธรด 1 จะถูกบล็อก จากนั้นเธรด 2 จะดำเนินการในการปรับตัวด้วยตนเองบนตัวแปรและเธรด 2 ยังอ่านค่าดั้งเดิมของตัวแปร Inc เนื่องจากเธรด 1 ดำเนินการอ่านเฉพาะในตัวแปร Inc และไม่ได้แก้ไขตัวแปรจึงไม่ทำให้แคชสายแคชตัวแปร Inc ในเธรด 2 ไม่ถูกต้องในหน่วยความจำที่ทำงานดังนั้นเธรด 2 จะไปที่หน่วยความจำหลักโดยตรงเพื่ออ่านค่าของ Inc เมื่อพบว่าค่าของ Inc คือ 10 จากนั้นจะเพิ่มขึ้น 1 และเขียน 11 ไปยังหน่วยความจำที่ทำงานและในที่สุดก็เขียนลงในหน่วยความจำหลัก จากนั้นเธรด 1 จากนั้นทำการดำเนินการเพิ่มเติม เนื่องจากค่าของ Inc ได้รับการอ่านโปรดทราบว่าค่าของ Inc ในเธรด 1 ยังคงเป็น 10 ในเวลานี้ดังนั้นหลังจากเธรด 1 เพิ่ม Inc ค่าของ Inc คือ 11 จากนั้นเขียน 11 เพื่อทำงานหน่วยความจำและเขียนลงในหน่วยความจำหลัก จากนั้นหลังจากสองเธรดทำการดำเนินการในตัวเอง, Inc เพิ่มขึ้นเพียง 1
การดำเนินการอัตโนมัติไม่ใช่การดำเนินการอะตอมและความผันผวนไม่สามารถรับประกันได้ว่าการดำเนินการใด ๆ ในตัวแปรนั้นเป็นอะตอม
ความผันผวนสามารถรับรองความเป็นระเบียบได้หรือไม่?
ดังที่ได้กล่าวไว้ก่อนหน้านี้คำหลักที่ผันผวนสามารถห้ามการสั่งซื้อการสั่งซื้อใหม่ดังนั้นความผันผวนสามารถรับรองได้ในระดับหนึ่ง
มีความหมายสองประการที่ต้องห้ามสั่งซื้อคำหลักที่ผันผวนใหม่:
3. ใช้คำหลักที่ผันผวนอย่างถูกต้อง
คำหลักที่ซิงโครไนซ์ป้องกันหลายเธรดจากการดำเนินการรหัสชิ้นส่วนในเวลาเดียวกันซึ่งจะส่งผลกระทบอย่างมากต่อประสิทธิภาพการดำเนินการของโปรแกรม ประสิทธิภาพของคำหลักที่ผันผวนดีกว่าการซิงโครไนซ์ในบางกรณี อย่างไรก็ตามควรสังเกตว่าคำหลักที่ผันผวนไม่สามารถแทนที่คำหลักที่ซิงโครไนซ์ได้เนื่องจากคำหลักที่ผันผวนไม่สามารถรับประกันความเป็นอะตอมของการดำเนินการได้ โดยทั่วไปการพูดสองเงื่อนไขต่อไปนี้จะต้องปฏิบัติตามเมื่อใช้ความผันผวน:
เงื่อนไขแรกคือมันไม่สามารถดำเนินการเช่นการเพิ่มขึ้นของตนเองและลดการลดลงของตนเอง ดังที่ได้กล่าวมาแล้วความผันผวนไม่รับประกันความเป็นอะตอม
มายกตัวอย่างสิ่งนี้กันเถอะ มันมีค่าคงที่: ขอบเขตล่างมักจะน้อยกว่าหรือเท่ากับขอบเขตบน
NumberRange ชั้นเรียนสาธารณะ {ส่วนตัวผันผวน int ล่าง, บน; public int getlower () {return ล่าง; } public int getupper () {return updpon; } โมฆะสาธารณะ setLower (ค่า int) {ถ้า (ค่า> บน) โยน unlegalargumentException ใหม่ (... ); ต่ำกว่า = ค่า; } โมฆะสาธารณะ setupper (ค่า int) {ถ้า (ค่า <ต่ำกว่า) โยน unlegalargumentException ใหม่ (... ); บน = ค่า; - วิธีนี้ จำกัด ตัวแปรสถานะที่กำหนดขอบเขตดังนั้นการกำหนดเขตข้อมูลล่างและบนเนื่องจากประเภทที่ผันผวนไม่ได้ใช้ความปลอดภัยของเธรดของชั้นเรียนอย่างเต็มที่และยังต้องมีการซิงโครไนซ์ มิฉะนั้นหากสองเธรดเกิดขึ้นเพื่อดำเนินการ setlower และ setupper ที่มีค่าที่ไม่สอดคล้องกันในเวลาเดียวกันช่วงจะไม่สอดคล้องกัน ตัวอย่างเช่นหากสถานะเริ่มต้นคือ (0, 5) ในเวลาเดียวกันให้ใช้เธรดการโทร setlower (4) และเธรด b call setupper (3) จะเห็นได้ชัดว่าค่าที่เก็บไว้ใน cross-Stored โดยการดำเนินการทั้งสองนี้ไม่เป็นไปตามเงื่อนไขทั้งสองเธรดจะผ่านการตรวจสอบเพื่อป้องกันค่าคงที่
ในความเป็นจริงมันคือเพื่อให้แน่ใจว่าอะตอมของการดำเนินการเพื่อใช้ความผันผวน มีสองสถานการณ์หลักสำหรับการใช้ความผันผวน:
สถานะสถานะ
Boolean ผันผวน shutdownRequested; ... โมฆะสาธารณะปิดการปิด () {shutdownRequested = true; } โมฆะสาธารณะ Dowork () {ในขณะที่ (! shutdownRequested) {// do stuff}}เป็นไปได้ว่าวิธีการปิด () จะถูกเรียกจากนอกลูป - เช่นในเธรดอื่น - ดังนั้นจึงต้องดำเนินการซิงโครไนซ์บางชนิดเพื่อให้แน่ใจว่าการมองเห็นของตัวแปร shutdownrequested ถูกนำไปใช้อย่างถูกต้อง อย่างไรก็ตามการเขียนลูปด้วยบล็อกที่ซิงโครไนซ์นั้นลำบากกว่าการเขียนด้วยธงสถานะผันผวน เนื่องจากการระเหยง่ายทำให้การเข้ารหัสง่ายขึ้นและสถานะสถานะไม่ขึ้นอยู่กับสถานะอื่น ๆ ในโปรแกรมจึงเหมาะสำหรับผันผวนที่นี่
โหมดตรวจสอบสองครั้ง (DCL)
Singleton คลาสสาธารณะ {อินสแตนซ์แบบสแตติกแบบคงที่ส่วนตัว = NULL; สาธารณะคงที่ singleton getInstance () {ถ้า (อินสแตนซ์ == null) {ซิงโครไนซ์ (นี่) {ถ้า (อินสแตนซ์ == null) {อินสแตนซ์ = ใหม่ singleton (); }} ส่งคืนอินสแตนซ์; - การใช้ความผันผวนที่นี่จะส่งผลกระทบต่อประสิทธิภาพมากขึ้นหรือน้อยลง แต่เมื่อพิจารณาจากความถูกต้องของโปรแกรมมันยังคงคุ้มค่าที่จะเสียสละประสิทธิภาพนี้
ข้อได้เปรียบของ DCL คือมันมีการใช้ทรัพยากรสูง วัตถุซิงเกิลตันจะถูกสร้างอินสแตนซ์เฉพาะเมื่อ GetInstance ถูกดำเนินการเป็นครั้งแรกซึ่งมีประสิทธิภาพสูง ข้อเสียคือปฏิกิริยาจะช้าลงเล็กน้อยเมื่อโหลดเป็นครั้งแรกและมีข้อบกพร่องบางอย่างในสภาพแวดล้อมที่เกิดขึ้นพร้อมกันสูงถึงแม้ว่าความน่าจะเป็นของการเกิดขึ้นจะเล็กมาก
แม้ว่า DCL จะแก้ปัญหาการใช้ทรัพยากรการซิงโครไนซ์ที่ไม่จำเป็นความปลอดภัยของเธรด ฯลฯ ในระดับหนึ่ง แต่ก็ยังมีปัญหาความล้มเหลวในบางกรณีนั่นคือความล้มเหลวของ DCL ในหนังสือ "การเขียนโปรแกรมการเขียนโปรแกรมพร้อมกันของ Java" แนะนำให้ใช้รหัสต่อไปนี้
Public Class Singleton {Private Singleton () {} Public Static Singleton GetInstance () {return singletonholder.sinstance; } ชั้นเรียนแบบคงที่ส่วนตัว {ส่วนตัวคงที่ singleton singleton sinstance = New Singleton (); - เกี่ยวกับการตรวจสอบสองครั้งคุณสามารถดูได้
4. สรุป
เมื่อเปรียบเทียบกับล็อคตัวแปรผันผวนนั้นง่ายมาก แต่ในเวลาเดียวกันกลไกการซิงโครไนซ์ที่เปราะบางมากซึ่งในบางกรณีจะให้ประสิทธิภาพและความสามารถในการปรับขนาดได้ดีกว่าล็อค หากคุณปฏิบัติตามเงื่อนไขการใช้งานของความผันผวนที่เป็นอิสระอย่างแท้จริงจากตัวแปรอื่น ๆ และค่าก่อนหน้าของตัวเองคุณสามารถใช้ความผันผวนแทนการซิงโครไนซ์เพื่อลดความซับซ้อนของรหัสในบางกรณี อย่างไรก็ตามรหัสที่ใช้ความผันผวนมักจะเกิดข้อผิดพลาดมากกว่ารหัสโดยใช้ล็อค บทความนี้แนะนำกรณีการใช้งานที่พบบ่อยที่สุดสองกรณีที่สามารถใช้ความผันผวนแทนการซิงโครไนซ์ ในกรณีอื่น ๆ เราควรใช้ซิงโครไนซ์ดีกว่า
ข้างต้นเป็นเรื่องเกี่ยวกับบทความนี้ฉันหวังว่ามันจะเป็นประโยชน์กับการเรียนรู้ของทุกคน