1. เสนอปัญหาการซิงโครไนซ์
สมมติว่าเราใช้โปรเซสเซอร์แบบดูอัลคอร์เพื่อดำเนินการสองเธรด A และ B, Core 1 ดำเนินการเธรด A และ Core 2 ดำเนินการเธรด B ตอนนี้เธรดทั้งสองตอนนี้ต้องเพิ่ม 1 ลงในตัวแปรสมาชิก I ของวัตถุชื่อ OBJ สมมติว่าค่าเริ่มต้นของฉันคือ 0 ในทางทฤษฎีค่าของฉันควรจะกลายเป็น 2 หลังจากสองเธรดทำงาน แต่ในความเป็นจริงมันเป็นไปได้มากที่ผลลัพธ์จะเป็น 1
มาวิเคราะห์เหตุผลตอนนี้ เพื่อความเรียบง่ายของการวิเคราะห์เราไม่ได้พิจารณาสถานการณ์แคช ในความเป็นจริงมีแคชที่จะเพิ่มความเป็นไปได้ที่ผลลัพธ์คือ 1 เธรด A อ่านตัวแปร I ในหน่วยความจำลงในหน่วยการดำเนินการเลขคณิต 1 จากนั้นทำการดำเนินการเพิ่มเติมแล้วเขียนผลการคำนวณกลับไปที่หน่วยความจำ เนื่องจากการดำเนินการข้างต้นไม่ใช่การทำงานของอะตอมตราบใดที่เธรด B อ่านค่าของฉันในหน่วยความจำก่อนที่จะด้าย A เขียนค่าของฉันโดยการเพิ่ม 1 กลับไปยังหน่วยความจำ (ค่าของฉันคือ 0 ในเวลานี้) จากนั้นผลลัพธ์ของฉันจะปรากฏเป็น 1 เนื่องจากฉันอ่านค่า 1
ทางออกที่พบบ่อยที่สุดคือการใช้คำหลักที่ซิงโครไนซ์เพื่อล็อควัตถุ OBJ ด้วยรหัสที่เพิ่ม 1 ลงในรหัส I-Visible ในสองเธรด วันนี้เราแนะนำโซลูชันใหม่ซึ่งจะใช้คลาสที่เกี่ยวข้องในแพ็คเกจอะตอมเพื่อแก้ปัญหา
2. การรองรับฮาร์ดแวร์อิง
ในระบบโปรเซสเซอร์เดียว (ตัวประมวลผล uniprocessor) การดำเนินการที่สามารถทำได้ในคำสั่งเดียวสามารถพิจารณาได้ว่า "การดำเนินการอะตอม" เนื่องจากการขัดจังหวะสามารถเกิดขึ้นได้ระหว่างคำแนะนำเท่านั้น นี่คือเหตุผลที่ระบบคำสั่ง CPU บางระบบแนะนำ test_and_set, test_and_clear และคำแนะนำอื่น ๆ สำหรับการยกเว้นทรัพยากรที่สำคัญ มันแตกต่างกันในโครงสร้างหลายโปรเซสเซอร์แบบสมมาตรเนื่องจากโปรเซสเซอร์หลายตัวทำงานอย่างอิสระในระบบแม้กระทั่งการดำเนินการที่สามารถทำได้ในคำสั่งเดียวอาจถูกรบกวน
บนแพลตฟอร์ม x86 CPU ให้วิธีการล็อคบัสในระหว่างการดำเนินการตามคำสั่ง มี lead #hlockpin บนชิป CPU หากคำนำหน้า "ล็อค" ถูกเพิ่มลงในคำสั่งในโปรแกรมภาษาแอสเซมบลีรหัสเครื่องแอสเซมบลีจะทำให้ CPU ลดศักยภาพของ #Hlockpin เมื่อดำเนินการคำสั่งนี้และปล่อยมันจนกว่าจะสิ้นสุดคำสั่งนี้จึงล็อคบัส ด้วยวิธีนี้ซีพียูอื่น ๆ บนรถบัสเดียวกันไม่สามารถเข้าถึงหน่วยความจำผ่านบัสได้ในขณะนี้เพื่อให้มั่นใจว่าอะตอมของคำสั่งนี้ในสภาพแวดล้อมแบบมัลติโปรเซสเซอร์ แน่นอนว่าคำแนะนำทั้งหมดไม่สามารถนำหน้าด้วยการล็อค เพิ่มเฉพาะ, ADC, และ, BTC, BTR, BTS, CMPXCHG, DEC, Inc, Neg, Not, หรือ, SBB, Sub, Xor, XADD และ XCHG สามารถนำหน้าด้วยคำแนะนำ "ล็อค" เพื่อตระหนักถึงการดำเนินงานของอะตอม
การทำงานหลักของอะตอมคือ CAS (เปรียบเทียบกับการใช้งานโดยใช้คำสั่ง CMPXCHG ซึ่งเป็นคำสั่งอะตอม) คำสั่งนี้มีสามตัวถูกดำเนินการค่าหน่วยความจำ V ของตัวแปร (ตัวย่อของค่า) ค่าที่คาดหวังปัจจุบัน e ของตัวแปร (ตัวย่อของข้อยกเว้น) ค่า U ของตัวแปรต้องการอัปเดต (ตัวย่อของการอัปเดต) เมื่อค่าหน่วยความจำเหมือนกับค่าปัจจุบันที่คาดหวังค่าที่อัปเดตของตัวแปรจะถูกเขียนทับโดยตัวแปรและรหัสหลอกจะถูกดำเนินการดังนี้
if (v == e) {v = u return true} else else {return false}ตอนนี้เราจะใช้การดำเนินงาน CAS เพื่อแก้ปัญหาข้างต้น เธรด B อ่านตัวแปร I ในหน่วยความจำลงในตัวแปรชั่วคราว (สมมติว่าค่าที่อ่านในเวลานี้คือ 0) จากนั้นอ่านค่าของ I ลงในหน่วยปฏิบัติการเลขคณิตของ Core1 ถัดไปเพิ่ม 1 เพื่อเปรียบเทียบว่าค่าในตัวแปรชั่วคราวนั้นเหมือนกับค่าปัจจุบันของ i หากค่าของ I ในหน่วยความจำเหมือนกันกับค่าของผลลัพธ์ในหน่วยการดำเนินงาน (เช่น i+1) (โปรดทราบว่าส่วนนี้เป็นการดำเนินการ CAS มันเป็นการดำเนินการอะตอมซึ่งไม่สามารถขัดจังหวะและการดำเนินการ CAS ในเธรดอื่นไม่สามารถดำเนินการในเวลาเดียวกัน) มิฉะนั้นการดำเนินการคำสั่งล้มเหลว หากคำสั่งล้มเหลวนั่นหมายความว่าเธรด A ได้เพิ่มค่าของ i โดย 1 จากนี้เราจะเห็นได้ว่าหากค่าของฉันอ่านโดยเธรดทั้งสองเป็น 0 ที่จุดเริ่มต้นการดำเนินการ CAS ของเธรดเพียงหนึ่งเดียวเท่านั้นที่สามารถประสบความสำเร็จได้เนื่องจากการดำเนินการ CAS ไม่สามารถดำเนินการพร้อมกันได้ สำหรับเธรดที่ล้มเหลวในการดำเนินการ CAS ตราบใดที่การดำเนินการ CAS ดำเนินการอย่างต่อเนื่องมันจะประสบความสำเร็จอย่างแน่นอน คุณจะเห็นว่าไม่มีการบล็อกด้ายซึ่งแตกต่างจากหลักการของการซิงโครไนซ์
3. บทนำเกี่ยวกับแพ็คเกจอะตอมและการวิเคราะห์รหัสที่มา
คุณลักษณะพื้นฐานของคลาสในแพ็คเกจอะตอมคือในสภาพแวดล้อมแบบมัลติเธรดเมื่อหลายเธรดทำงานในตัวแปรประเภทเดียว (รวมถึงประเภทพื้นฐานและประเภทอ้างอิง) ในเวลาเดียวกันมันเป็นพิเศษนั่นคือเมื่อหลายเธรดอัพเดตค่าของตัวแปรในเวลาเดียวกัน
วิธีการหลักในคลาส Atomic Series จะเรียกวิธีการในท้องถิ่นหลายวิธีในคลาสที่ไม่ปลอดภัย เราจำเป็นต้องรู้ก่อนว่าสิ่งหนึ่งคือคลาสที่ไม่ปลอดภัยด้วยชื่อเต็ม: sun.misc.unsafe คลาสนี้มีการดำเนินการจำนวนมากในรหัส C รวมถึงการจัดสรรหน่วยความจำโดยตรงจำนวนมากและการเรียกใช้อะตอม เหตุผลที่มีการทำเครื่องหมายว่าไม่ปลอดภัยคือการบอกคุณว่าการโทรจำนวนมากในพื้นที่นี้จะมีความเสี่ยงด้านความปลอดภัยและจำเป็นต้องใช้อย่างรอบคอบมิฉะนั้นจะนำไปสู่ผลกระทบร้ายแรง ตัวอย่างเช่นเมื่อจัดสรรหน่วยความจำผ่านทางไม่ปลอดภัยหากคุณระบุบางพื้นที่ด้วยตัวคุณเองอาจทำให้พอยน์เตอร์บางตัวเช่น C ++ ข้ามขอบเขตไปยังกระบวนการอื่น ๆ
คลาสในแพ็คเกจอะตอมสามารถแบ่งออกเป็น 4 กลุ่มตามประเภทข้อมูลการดำเนินงาน
AtomicBoolean,AtomicInteger,AtomicLong
ประเภทพื้นฐานของการดำเนินการอะตอมสำหรับเธรดที่ปลอดภัย
AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
การทำงานของอะตอมที่ปลอดภัยของเธรดซึ่งไม่ทำงานในอาร์เรย์ทั้งหมด แต่ในองค์ประกอบเดียวในอาร์เรย์
AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater
การดำเนินการแบบเธรดที่ปลอดภัยตามประเภทพื้นฐาน (จำนวนเต็มยาว, จำนวนเต็มและประเภทอ้างอิง) ในวัตถุหลักการสะท้อน
AtomicReference,AtomicMarkableReference,AtomicStampedReference
ประเภทการอ้างอิงแบบเธรดที่ปลอดภัยและการดำเนินการอะตอมของประเภทอ้างอิงที่ป้องกันปัญหา ABA
โดยทั่วไปเราใช้ Atomicinteger, Atomicreference และ AtomicstampedReference ตอนนี้มาวิเคราะห์ซอร์สโค้ดของจำนวนเต็มอะตอมในแพ็คเกจอะตอม รหัสแหล่งที่มาของคลาสอื่นมีความคล้ายคลึงกันในหลักการ
1. ตัวสร้างพารามิเตอร์
Public Atomicinteger (int initialValue) {value = initialValue;}ดังที่เห็นได้จากฟังก์ชันตัวสร้างค่าจะถูกเก็บไว้ในค่าตัวแปรสมาชิก
ค่า int ผันผวนส่วนตัว;
ค่าตัวแปรสมาชิกถูกประกาศว่าเป็นประเภทที่ผันผวนซึ่งแสดงการมองเห็นภายใต้หลายเธรดนั่นคือการปรับเปลี่ยนของเธรดใด ๆ จะเห็นได้ทันทีในเธรดอื่น ๆ
2. วิธีการ compareanderset (ค่าของค่าจะถูกส่งผ่านภายในนี้และค่า Offsetset)
Public Final Boolean PomperEndset (Int คาดหวัง, Int Update) {return unsafe.compareandswapint (นี่, valueOffset, คาดหวัง, อัปเดต);}วิธีนี้เป็นการดำเนินการ Core CAS ที่มากที่สุด
3. วิธีการ GetAndSet ซึ่งมีการเรียกใช้วิธีการเปรียบเทียบ
สาธารณะสุดท้าย int getandset (int newValue) {สำหรับ (;;) {int current = get (); ถ้า (เปรียบเทียบ (ปัจจุบัน, newValue)) ส่งคืนกระแสไฟฟ้า; -หากเธรดอื่นเปลี่ยนค่าของค่าก่อนที่จะดำเนินการหาก (เปรียบเทียบ (ปัจจุบัน, newValue) ค่าของค่าจะต้องแตกต่างจากค่าปัจจุบันหากการเปรียบเทียบไม่สามารถดำเนินการได้คุณสามารถรับค่าของค่าใหม่ได้เท่านั้น
4. การใช้งาน I ++
public int final int getandincrement () {สำหรับ (;;) {int current = get (); int next = current + 1; ถ้า (เปรียบเทียบ (ปัจจุบัน, ถัดไป)) ส่งคืนกระแส; -5. การใช้ ++ i
INT INTINT ขั้นสุดท้ายสาธารณะ () {สำหรับ (;;) {int current = get (); int next = current + 1; ถ้า (เปรียบเทียบ (ปัจจุบัน, ถัดไป)) กลับมาถัดไป; -4. ใช้ตัวอย่าง Atomicinteger
โปรแกรมต่อไปนี้ใช้ Atomicinteger เพื่อจำลองโปรแกรมการขายตั๋ว ทั้งสองโปรแกรมจะไม่ขายตั๋วเดียวกันในผลการดำเนินการและพวกเขาจะไม่ขายตั๋วเป็นลบ
แพ็คเกจ javaleanning; นำเข้า java.util.concurrent.atomic.atomicinteger; selltickets คลาสสาธารณะ {atomicinteger tickets = Atomicinteger ใหม่ (100); ผู้ขายในชั้นเรียนใช้งานได้ Tickets.get (); if (tickets.compareandset (tmp, tmp-1)) {system.out.println (thread.currentthread (). getName ()+""+tmp);}}}}}}}}}}}}}}}}}}}}}}}}} "sellera"). start (); เธรดใหม่ (St.New Seller (), "SellerB"). start ();}}5. ปัญหา ABA
ตัวอย่างข้างต้นเรียกใช้ผลลัพธ์ที่ถูกต้องอย่างสมบูรณ์ สิ่งนี้ขึ้นอยู่กับความจริงที่ว่าเธรดสอง (หรือมากกว่า) ทำงานบนข้อมูลในทิศทางเดียวกัน ในตัวอย่างข้างต้นเธรดทั้งสองทำงานบนตั๋วลดลง ตัวอย่างเช่นหากหลายเธรดดำเนินการการลงทะเบียนวัตถุในคิวที่ใช้ร่วมกันผลลัพธ์ที่ถูกต้องสามารถรับได้ผ่านคลาส Atomicreference (นี่เป็นกรณีของคิวที่เก็บรักษาไว้ใน AQS) อย่างไรก็ตามหลายเธรดสามารถลงทะเบียนหรือเพิกถอนได้นั่นคือทิศทางการทำงานของข้อมูลไม่สอดคล้องกันดังนั้น ABA อาจเกิดขึ้น
ตอนนี้เรามาเป็นตัวอย่างที่ค่อนข้างเข้าใจง่ายเพื่ออธิบายปัญหา ABA สมมติว่ามีสองเธรด T1 และ T2 และเธรดทั้งสองนี้ดำเนินการสแต็คและการสแต็กในสแต็กเดียวกัน
เราใช้หางที่กำหนดโดย Atomicreference เพื่อบันทึกตำแหน่งสูงสุดของสแต็ก
Atomicreference <T> หาง;
สมมติว่าเธรด T1 พร้อมที่จะเปิดตัวสำหรับการดำเนินการสแต็กเราจำเป็นต้องอัปเดตตำแหน่งสูงสุดของสแต็กจาก SP ไปยัง Newsp ผ่านการดำเนินการ CAS ดังแสดงในรูปที่ 1 อย่างไรก็ตามก่อนที่เธรด T1 จะดำเนินการหาง T2 ดำเนินการสามการดำเนินการ: A ออกจากสแต็ก B ออกจากสแต็คแล้ว A ก็อยู่บนสแต็ก ในเวลานี้ระบบจะเริ่มตั้งเวลาอีกครั้งและเธรด T1 ยังคงดำเนินการสแต็ค แต่ในมุมมองของเธรด T1 องค์ประกอบที่อยู่ด้านบนของสแต็กยังคงเป็น (นั่นคือ T1 ยังเชื่อว่า B ยังคงเป็นองค์ประกอบต่อไปของสแต็ก A) ตัวชี้ด้านบนของสแต็คชี้ไปที่โหนด B ในความเป็นจริง B ไม่มีอยู่ในสแต็กอีกต่อไป ผลลัพธ์หลังจาก T1 วางออกจากสแต็กแสดงในรูปที่ 3 ซึ่งเห็นได้ชัดว่าไม่ใช่ผลลัพธ์ที่ถูกต้อง
6. การแก้ปัญหา ABA
ใช้ AtommarkableReference, AtomicstampedReference ใช้คลาสอะตอมสองชั้นที่กล่าวถึงข้างต้นเพื่อดำเนินการ เมื่อดำเนินการตามคำสั่งเปรียบเทียบกับพวกเขาไม่เพียง แต่จำเป็นต้องเปรียบเทียบค่าก่อนหน้าและค่าที่คาดหวังของวัตถุ แต่ยังต้องเปรียบเทียบค่าแสตมป์ปัจจุบัน (การดำเนินการ) และค่าแสตมป์ (การดำเนินการ) ที่คาดหวัง เฉพาะเมื่อทุกอย่างเหมือนกันสามารถใช้วิธีการเปรียบเทียบได้สำเร็จ ทุกครั้งที่การอัปเดตสำเร็จค่าแสตมป์จะเปลี่ยนไปและการตั้งค่าของค่าแสตมป์จะถูกควบคุมโดยโปรแกรมเมอร์เอง
การเปรียบเทียบบูลีนสาธารณะ (V ที่คาดว่าจะได้รับ, v newReference, int ที่คาดหวัง, int newstamp) {pair <v> current = pair; return returne == current.reference && คาดหวัง == current.stamp && (newreference == current.reference && Newstamp == current.stamp) || caspairในเวลานี้วิธีการเปรียบเทียบต้องใช้พารามิเตอร์สี่ตัว: คาดหวังการอ้างอิง, NewReference, คาดว่า Stamp, Newstamp เมื่อเราใช้วิธีนี้เราต้องตรวจสอบให้แน่ใจว่าค่าแสตมป์ที่คาดหวังไม่เหมือนกับค่าการอัปเดตการอัปเดต โดยปกติ Newstamp = คาดว่าจะได้+1
ใช้ตัวอย่างข้างต้น
สมมติว่าเธรด T1 อยู่ก่อนสแต็ก: SP ชี้ไปที่ A และค่าแสตมป์คือ 100
เธรด T2 ดำเนินการ: หลังจาก A ถูกปล่อยออกมาแล้ว SP ชี้ไปที่ B และค่าแสตมป์จะกลายเป็น 101
หลังจาก B ถูกปล่อยออกมาแล้ว SP ชี้ไปที่ C และค่าแสตมป์จะกลายเป็น 102
หลังจากใส่ลงในสแต็กแล้ว SP จะชี้ไปที่ A และค่าแสตมป์จะกลายเป็น 103
เธรด T1 ยังคงดำเนินการคำสั่งเปรียบเทียบและพบว่าแม้ว่า SP ยังคงชี้ไปที่ A แต่ค่าที่คาดหวังของค่าแสตมป์ 100 นั้นแตกต่างจากค่าปัจจุบัน 103 ดังนั้นการเปรียบเทียบจะล้มเหลว คุณต้องได้รับมูลค่าของ Newsp (ในเวลานี้ Newsp จะชี้ไปที่ C) และมูลค่าที่คาดหวังของค่าแสตมป์ 103 จากนั้นทำการดำเนินการเปรียบเทียบอีกครั้ง ด้วยวิธีนี้ความสำเร็จในการเปิดสแต็ก SP จะชี้ไปที่ C.
โปรดทราบว่าเนื่องจากการเปรียบเทียบสามารถเปลี่ยนค่าละครั้งได้เพียงครั้งเดียวและไม่สามารถเปลี่ยน NewReference และ Newstamp ได้ในเวลาเดียวกันในระหว่างการใช้งานคลาสคู่จะถูกกำหนดภายในเพื่อเปลี่ยน Newreference และ Newstamp เป็นวัตถุเดียว เมื่อดำเนินการ CAS จริง ๆ แล้วมันเป็นการดำเนินการบนวัตถุคู่
คู่คลาสคงที่ส่วนตัว <T> {การอ้างอิงสุดท้าย t; แสตมป์ int สุดท้าย; คู่ส่วนตัว (การอ้างอิง t, แสตมป์ int) {this.reference = การอ้างอิง; this.stamp = แสตมป์; } คงที่ <T> pair <t> ของ (t การอ้างอิง, แสตมป์ int) {ส่งคืนคู่ใหม่ <t> (อ้างอิง, แสตมป์); -สำหรับ AtomicMarkableReference ค่าแสตมป์เป็นตัวแปรบูลีนในขณะที่ค่าแสตมป์ใน AtomicstampedReference เป็นตัวแปรจำนวนเต็ม
สรุป
ข้างต้นเป็นเรื่องเกี่ยวกับการอภิปรายสั้น ๆ ของบทความนี้เกี่ยวกับหลักการดำเนินการและการใช้งานของแพ็คเกจอะตอมใน Java ฉันหวังว่ามันจะเป็นประโยชน์กับทุกคน เพื่อนที่สนใจสามารถอ้างถึงหัวข้ออื่น ๆ ที่เกี่ยวข้องในเว็บไซต์นี้ต่อไป หากมีข้อบกพร่องใด ๆ โปรดฝากข้อความไว้เพื่อชี้ให้เห็น