ข้อกำหนดเบื้องต้นสำหรับการเกิดขึ้นของการดำเนินการปรมาณู
มาดูตัวอย่างนี้เพื่อช่วยให้คุณเข้าใจการทำงานของอะตอม:
public class Counter {
int count;
public void increment() {
count++;
}
}
เมื่อเรามีหนึ่งเธรด ทุกอย่างทำงานได้ดี แต่ถ้าเราเพิ่มมัลติเธรด เราจะได้ผลลัพธ์ที่ผิด และทั้งหมดเป็นเพราะการดำเนินการเพิ่มไม่ใช่การดำเนินการเดียว แต่เป็นสาม: คำขอเพื่อรับค่าปัจจุบันนับจากนั้นเพิ่มทีละ 1 แล้วเขียนอีกครั้งเป็นนับ.
และเมื่อสองเธรดต้องการเพิ่มตัวแปร คุณมักจะสูญเสียข้อมูล นั่นคือทั้งสองเธรดได้รับ 100 ดังนั้นทั้งคู่จะเขียน 101 แทนค่าที่คาดไว้ 102
แล้วจะแก้ยังไง? คุณต้องใช้ล็อค คีย์เวิร์ดที่ซิงโครไนซ์ช่วยแก้ปัญหานี้ โดยใช้คีย์เวิร์ดนี้รับประกันว่าเธรดหนึ่งรายการจะเข้าถึงเมธอดในแต่ละครั้ง
public class SynchronizedCounterWithLock {
private volatile int count;
public synchronized void increment() {
count++;
}
}
นอกจากนี้ คุณต้องเพิ่ม คำหลักที่ผันผวนซึ่งช่วยให้มองเห็นการอ้างอิงระหว่างเธรดต่างๆ ได้อย่างถูกต้อง เราได้ตรวจสอบงานของเขาด้านบน
แต่ก็ยังมีข้อเสีย สิ่งที่ใหญ่ที่สุดคือประสิทธิภาพ ณ เวลาที่หลายเธรดพยายามรับการล็อกและมีโอกาสเขียน เธรดที่เหลือจะถูกบล็อกหรือระงับจนกว่าเธรดจะถูกปล่อย
กระบวนการทั้งหมดเหล่านี้ การบล็อก การเปลี่ยนไปใช้สถานะอื่นมีราคาแพงมากสำหรับประสิทธิภาพของระบบ
ปฏิบัติการปรมาณู
อัลกอริทึมใช้คำสั่งเครื่องจักรระดับต่ำ เช่น การเปรียบเทียบและการแลกเปลี่ยน (CAS, การเปรียบเทียบและการแลกเปลี่ยน ซึ่งรับประกันความสมบูรณ์ของข้อมูลและมีการวิจัยจำนวนมากอยู่แล้ว)
การดำเนินการ CAS ทั่วไปดำเนินการกับตัวถูกดำเนินการสามตัว:
- พื้นที่หน่วยความจำสำหรับการทำงาน (M)
- ค่าคาดหวังที่มีอยู่ (A) ของตัวแปร
- ค่าใหม่ (B) ที่จะตั้งค่า
CAS จะอัปเดตอะตอมของ M เป็น B แต่ถ้าค่าของ M เหมือนกับ A เท่านั้น มิฉะนั้นจะไม่ดำเนินการใดๆ
ในกรณีแรกและกรณีที่สอง ค่าของ M จะถูกส่งกลับ วิธีนี้ทำให้คุณสามารถรวมสามขั้นตอน ได้แก่ การรับค่า การเปรียบเทียบค่า และการอัปเดต และทั้งหมดจะกลายเป็นการดำเนินการเดียวที่ระดับเครื่องจักร
ทันทีที่แอปพลิเคชันแบบมัลติเธรดเข้าถึงตัวแปรและพยายามอัปเดตและใช้ CAS เธรดตัวใดตัวหนึ่งจะได้รับและสามารถอัปเดตได้ แต่แตกต่างจากการล็อก เธรดอื่นๆ จะได้รับข้อผิดพลาดเกี่ยวกับการไม่สามารถอัปเดตค่าได้ จากนั้นพวกเขาจะไปทำงานต่อไปและการสลับจะไม่รวมอยู่ในงานประเภทนี้โดยสิ้นเชิง
ในกรณีนี้ ตรรกะจะยากขึ้นเนื่องจากเราต้องจัดการกับสถานการณ์เมื่อการดำเนินการ CAS ทำงานไม่สำเร็จ เราจะจำลองโค้ดเพื่อไม่ให้ดำเนินการต่อจนกว่าการดำเนินการจะสำเร็จ
ความรู้เบื้องต้นเกี่ยวกับประเภทของอะตอม
คุณเคยเจอสถานการณ์ที่ต้องตั้งค่าการซิงโครไนซ์สำหรับตัวแปรประเภทที่ง่ายที่สุดintหรือไม่
วิธีแรกที่เรากล่าวถึงคือการใช้volatile + synchronized แต่ยังมีคลาส Atomic* พิเศษอีกด้วย
หากเราใช้ CAS การดำเนินการจะทำงานได้เร็วกว่าเมื่อเทียบกับวิธีแรก นอกจากนี้ เรายังมีวิธีการพิเศษและสะดวกมากสำหรับการเพิ่มมูลค่าและการดำเนินการเพิ่มและลด
AtomicBoolean , AtomicInteger , AtomicLong , AtomicIntegerArray , AtomicLongArrayเป็นคลาสที่มีการดำเนินการเป็นอะตอมมิก ด้านล่างเราจะวิเคราะห์งานกับพวกเขา
จำนวนเต็มอะตอม
คลาสAtomicIntegerให้การดำเนินการกับ ค่า intที่สามารถอ่านและเขียนได้ทางอะตอม นอกเหนือจากการดำเนินการทางอะตอมแบบขยาย
มีเมธอดรับและเซ็ ต ที่ทำงานเหมือนกับการอ่านและเขียนตัวแปร
นั่นคือ "เกิดขึ้นก่อน" ด้วยการรับตัวแปรเดียวกันที่เราพูดถึงก่อนหน้านี้ในภายหลัง วิธีการเปรียบเทียบ อะตอมและการตั้งค่า ยังมีคุณสมบัติความสอดคล้องของหน่วยความจำเหล่านี้ด้วย
การดำเนินการทั้งหมดที่ส่งคืนค่าใหม่จะดำเนินการแบบอะตอม:
int addAndGet (int เดลต้า) | เพิ่มค่าเฉพาะให้กับค่าปัจจุบัน |
บูลีนเปรียบเทียบ AndSet (คาดว่า int, อัปเดต int) | ตั้งค่าเป็นค่าที่อัปเดตแล้วหากค่าปัจจุบันตรงกับค่าที่คาดไว้ |
int ลดลงและรับ () | ลดค่าปัจจุบันทีละหนึ่ง |
int getAndAdd (int เดลต้า) | เพิ่มค่าที่กำหนดให้กับค่าปัจจุบัน |
int getAndDecrement() | ลดค่าปัจจุบันทีละหนึ่ง |
int getAndIncrement() | เพิ่มค่าปัจจุบันทีละหนึ่ง |
int getAndSet(int ค่าใหม่) | ตั้งค่าที่กำหนดและส่งกลับค่าเก่า |
int เพิ่มขึ้นและรับ () | เพิ่มค่าปัจจุบันทีละหนึ่ง |
lazySet (int ค่าใหม่) | สุดท้ายกำหนดเป็นค่าที่กำหนด |
บูลีนจุดอ่อนCompareAndSet (คาดว่า, อัปเดต int) | ตั้งค่าเป็นค่าที่อัปเดตแล้วหากค่าปัจจุบันตรงกับค่าที่คาดไว้ |
ตัวอย่าง:
ExecutorService executor = Executors.newFixedThreadPool(5);
IntStream.range(0, 50).forEach(i -> executor.submit(atomicInteger::incrementAndGet));
executor.shutdown();
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.HOURS);
System.out.println(atomicInteger.get()); // prints 50
GO TO FULL VERSION