ฉันเข้าใจว่า AtomicInteger และตัวแปร Atomic อื่น ๆ อนุญาตการเข้าถึงพร้อมกัน โดยทั่วไปแล้วคลาสนี้จะใช้ในกรณีใดบ้าง?
ฉันเข้าใจว่า AtomicInteger และตัวแปร Atomic อื่น ๆ อนุญาตการเข้าถึงพร้อมกัน โดยทั่วไปแล้วคลาสนี้จะใช้ในกรณีใดบ้าง?
คำตอบ:
มีการใช้สองหลักAtomicInteger
:
เป็นตัวนับอะตอม ( incrementAndGet()
, ฯลฯ ) ที่สามารถใช้โดยเธรดจำนวนมากพร้อมกัน
เป็นแบบดั้งเดิมที่สนับสนุนคำสั่งเปรียบเทียบและสลับ (compareAndSet()
) เพื่อใช้อัลกอริทึมที่ไม่บล็อก
นี่คือตัวอย่างของการสร้างตัวเลขสุ่มแบบไม่มีการปิดกั้นจากJava Concurrency In Java ของ Brian Göetz :
public class AtomicPseudoRandom extends PseudoRandom {
private AtomicInteger seed;
AtomicPseudoRandom(int seed) {
this.seed = new AtomicInteger(seed);
}
public int nextInt(int n) {
while (true) {
int s = seed.get();
int nextSeed = calculateNext(s);
if (seed.compareAndSet(s, nextSeed)) {
int remainder = s % n;
return remainder > 0 ? remainder : remainder + n;
}
}
}
...
}
อย่างที่คุณเห็นมันใช้งานได้เกือบเหมือนกันincrementAndGet()
แต่ทำการคำนวณโดยพลการ ( calculateNext()
) แทนการเพิ่ม (และประมวลผลผลลัพธ์ก่อนกลับ)
read
และwrite that value + 1
การดำเนินการสิ่งนี้จะถูกตรวจพบแทนการเขียนทับการอัปเดตเก่า (หลีกเลี่ยงปัญหา นี่เป็นกรณีพิเศษของcompareAndSet
- ถ้าค่าเดิม2
คลาสเรียกจริงcompareAndSet(2, 3)
- ดังนั้นถ้าเธรดอื่นได้แก้ไขค่าในระหว่างนั้นวิธีการเพิ่มจะรีสตาร์ทอย่างมีประสิทธิภาพตั้งแต่ต้น
ตัวอย่างที่ง่ายที่สุดที่ฉันนึกได้คือการเพิ่มการทำงานของอะตอม
ด้วย ints มาตรฐาน:
private volatile int counter;
public int getNextUniqueIndex() {
return counter++; // Not atomic, multiple threads could get the same result
}
ด้วย AtomicInteger:
private AtomicInteger counter;
public int getNextUniqueIndex() {
return counter.getAndIncrement();
}
วิธีหลังเป็นวิธีที่ง่ายมากในการแสดงเอฟเฟกต์การกลายพันธุ์อย่างง่าย
ตรรกะที่ซับซ้อนกว่าการซิงโครไนซ์ที่ซับซ้อนมากขึ้นสามารถใช้งานได้โดยใช้compareAndSet()
เป็นประเภทของการล็อกในแง่ดี - รับค่าปัจจุบันคำนวณผลลัพธ์ตามนี้ตั้งค่าผลลัพธ์นี้ถ้าค่าfยังคงเป็นอินพุตที่ใช้ในการคำนวณ ตัวอย่างการนับมีประโยชน์มากและฉันมักจะใช้AtomicIntegers
สำหรับการนับและเครื่องกำเนิดไฟฟ้าที่ไม่เหมือนใครของ VM หากมีคำแนะนำของหลายเธรดที่เกี่ยวข้องเพราะง่ายต่อการทำงานกับฉันเกือบจะคิดว่ามันเป็นการเพิ่มประสิทธิภาพก่อนกำหนดให้ใช้ธรรมดาints
.
ในขณะที่คุณสามารถรับประกันการซิงโครไนซ์ที่เหมือนกันได้ตลอดเวลาints
และsynchronized
การประกาศที่เหมาะสมความสวยงามของAtomicInteger
คือความปลอดภัยของเธรดนั้นถูกสร้างไว้ในวัตถุจริงแทนที่จะเป็นสิ่งที่คุณต้องกังวลเกี่ยวกับการแทรกสอดที่เป็นไปได้ ที่เกิดขึ้นกับการเข้าถึงint
ค่า เป็นการยากที่จะละเมิดความปลอดภัยของเธรดโดยไม่ตั้งใจเมื่อโทรgetAndIncrement()
กลับกว่าi++
และกลับมาจำ (หรือไม่) เพื่อให้ได้จอภาพที่ถูกต้องมาก่อน
หากคุณดูที่วิธีการที่ AtomicInteger มีคุณจะสังเกตเห็นว่าพวกเขามีแนวโน้มที่จะสอดคล้องกับการดำเนินงานทั่วไปใน ints ตัวอย่างเช่น
static AtomicInteger i;
// Later, in a thread
int current = i.incrementAndGet();
เป็นเวอร์ชั่นที่ปลอดภัยสำหรับเธรดของสิ่งนี้:
static int i;
// Later, in a thread
int current = ++i;
วิธีการ map เช่นนี้
++i
จะi.incrementAndGet()
i++
ถูกi.getAndIncrement()
--i
เป็นi.decrementAndGet()
i--
ถูกi.getAndDecrement()
i = x
เป็นi.set(x)
x = i
ถูกx = i.get()
มีวิธีอำนวยความสะดวกอื่น ๆ เช่นกันเช่นcompareAndSet
หรือaddAndGet
การใช้หลักของการเป็นเมื่อคุณอยู่ในบริบทแบบมัลติเธรดและคุณจำเป็นต้องดำเนินการที่ปลอดภัยด้ายในจำนวนเต็มโดยไม่ต้องใช้AtomicInteger
synchronized
นัดและการดึงกับชนิดดั้งเดิมint
มีอะตอมอยู่แล้ว แต่มาพร้อมกับการดำเนินงานจำนวนมากที่ไม่ได้อะตอมAtomicInteger
int
ที่ง่ายที่สุดคือหรือgetAndXXX
xXXAndGet
ตัวอย่างเช่นgetAndIncrement()
เทียบเท่ากับอะตอมi++
ซึ่งไม่ได้เป็นอะตอมเพราะจริงๆแล้วมันเป็นทางลัดสำหรับการดำเนินการสามอย่าง: การดึงการเพิ่มและการกำหนด compareAndSet
มีประโยชน์มากในการใช้เซมาฟอร์ล็อคล๊อคและอื่น ๆ
การใช้AtomicInteger
นั้นเร็วกว่าและอ่านได้มากกว่าการทำการซิงโครไนซ์เดียวกัน
การทดสอบอย่างง่าย:
public synchronized int incrementNotAtomic() {
return notAtomic++;
}
public void performTestNotAtomic() {
final long start = System.currentTimeMillis();
for (int i = 0 ; i < NUM ; i++) {
incrementNotAtomic();
}
System.out.println("Not atomic: "+(System.currentTimeMillis() - start));
}
public void performTestAtomic() {
final long start = System.currentTimeMillis();
for (int i = 0 ; i < NUM ; i++) {
atomic.getAndIncrement();
}
System.out.println("Atomic: "+(System.currentTimeMillis() - start));
}
บนพีซีของฉันที่มี Java 1.6 การทดสอบปรมาณูจะทำงานใน 3 วินาทีในขณะที่ซิงโครไนซ์หนึ่งตัววิ่งในเวลาประมาณ 5.5 วินาที ปัญหาที่นี่คือการดำเนินการเพื่อซิงโครไนซ์ ( notAtomic++
) สั้นมาก ดังนั้นต้นทุนของการซิงโครไนซ์จึงมีความสำคัญมากเมื่อเปรียบเทียบกับการดำเนินการ
ข้างๆ atomicity AtomicInteger สามารถใช้เป็นเวอร์ชันที่ไม่แน่นอนของInteger
อินสแตนซ์ในMap
s เป็นค่า
AtomicInteger
เป็นคีย์แผนที่เนื่องจากใช้การเริ่มต้นequals()
ใช้งานซึ่งแทบจะไม่ใช่สิ่งที่คุณคาดหวังว่าความหมายจะถูกใช้หากใช้ในแผนที่
ตัวอย่างเช่นฉันมีห้องสมุดที่สร้างอินสแตนซ์ของบางคลาส แต่ละอินสแตนซ์เหล่านี้ต้องมี ID จำนวนเต็มเฉพาะเนื่องจากอินสแตนซ์เหล่านี้แสดงคำสั่งที่ส่งไปยังเซิร์ฟเวอร์และแต่ละคำสั่งต้องมี ID ที่ไม่ซ้ำกัน เนื่องจากมีหลายเธรดที่ได้รับอนุญาตให้ส่งคำสั่งพร้อมกันฉันจึงใช้ AtomicInteger เพื่อสร้าง ID เหล่านั้น วิธีการทางเลือกคือใช้การล็อคและจำนวนเต็มปกติ แต่ทั้งสองแบบนั้นช้ากว่าและสง่างามน้อยลง
เช่นเดียวกับ gabuzo ที่กล่าวว่าบางครั้งฉันใช้ AtomicIntegers เมื่อฉันต้องการผ่าน int โดยการอ้างอิง เป็นคลาสในตัวที่มีรหัสเฉพาะสถาปัตยกรรมดังนั้นจึงง่ายขึ้นและมีแนวโน้มที่จะปรับให้เหมาะสมยิ่งขึ้นกว่า MutableInteger ใด ๆ ที่ฉันสามารถเขียนโค้ดได้อย่างรวดเร็ว ที่กล่าวว่ามันให้ความรู้สึกเหมือนเป็นการละเมิดของชั้นเรียน
ในคลาสอะตอมของ Java 8 ได้ถูกขยายด้วยฟังก์ชันที่น่าสนใจสองฟังก์ชัน:
ทั้งสองกำลังใช้ updateFunction เพื่อดำเนินการอัพเดตค่าอะตอมมิก ความแตกต่างคือว่าอันแรกคืนค่าเก่าและอันที่สองคืนค่าใหม่ อาจมีการใช้งาน updateFunction เพื่อดำเนินการ "เปรียบเทียบและตั้งค่า" ที่ซับซ้อนกว่ามาตรฐานหนึ่งรายการ ตัวอย่างเช่นสามารถตรวจสอบว่าตัวนับอะตอมไม่ไปต่ำกว่าศูนย์โดยปกติจะต้องมีการซิงโครไนซ์และที่นี่รหัสล็อคฟรี:
public class Counter {
private final AtomicInteger number;
public Counter(int number) {
this.number = new AtomicInteger(number);
}
/** @return true if still can decrease */
public boolean dec() {
// updateAndGet(fn) executed atomically:
return number.updateAndGet(n -> (n > 0) ? n - 1 : n) > 0;
}
}
รหัสจะนำมาจากJava ตัวอย่างปรมาณู
ฉันมักจะใช้ AtomicInteger เมื่อฉันต้องการให้ Id กับวัตถุที่สามารถ accesed หรือสร้างขึ้นจากหลายกระทู้และฉันมักจะใช้เป็นแอตทริบิวต์คงที่ในชั้นเรียนที่ฉันเข้าถึงในตัวสร้างของวัตถุ
คุณสามารถใช้การล็อคแบบไม่บล็อกโดยใช้ comparAndSwap (CAS) กับจำนวนเต็มหรือยาวของอะตอม "Tl2" ซอฟแวร์การทำธุรกรรมหน่วยความจำกระดาษอธิบายนี้:
เราเชื่อมโยงล็อกการเขียนรุ่นพิเศษกับทุกตำแหน่งหน่วยความจำที่ทำธุรกรรม ในรูปแบบที่ง่ายที่สุดล็อกการเขียนเวอร์ชันเป็นสปินล็อคคำเดียวที่ใช้การดำเนินการ CAS เพื่อรับการล็อกและสโตร์เพื่อปลดล็อค เนื่องจากต้องการเพียงบิตเดียวเพื่อระบุว่ามีการล็อกเราจึงใช้ส่วนที่เหลือของคำล็อคเพื่อเก็บหมายเลขเวอร์ชั่น
สิ่งที่อธิบายได้คืออ่านเลขจำนวนเต็มแบบอะตอมก่อน แยกส่วนนี้ออกเป็น lock-bit ที่ไม่สนใจและหมายเลขรุ่น พยายามเขียน CAS ในขณะที่ล็อคบิตถูกล้างด้วยหมายเลขเวอร์ชันปัจจุบันไปยังชุดล็อคบิตและหมายเลขเวอร์ชันถัดไป วนซ้ำจนกว่าคุณจะประสบความสำเร็จและคุณเป็นเธรดที่เป็นเจ้าของล็อค ปลดล็อคโดยการตั้งค่าหมายเลขรุ่นปัจจุบันด้วยการล้างบิตล็อค กระดาษอธิบายการใช้หมายเลขรุ่นในการล็อกเพื่อประสานงานที่เธรดมีชุดของการอ่านที่สอดคล้องกันเมื่อพวกเขาเขียน
บทความนี้อธิบายว่าโปรเซสเซอร์มีการสนับสนุนฮาร์ดแวร์สำหรับการเปรียบเทียบและสลับการดำเนินการทำให้มีประสิทธิภาพมาก มันยังอ้างว่า:
non-blocking ตัวนับ CAS โดยใช้ตัวแปรอะตอมมิกมีประสิทธิภาพที่ดีกว่าตัวนับแบบล็อกในการช่วงชิงระดับต่ำถึงปานกลาง
กุญแจสำคัญคืออนุญาตให้มีการเข้าถึงและแก้ไขพร้อมกันได้อย่างปลอดภัย พวกเขามักใช้เป็นตัวนับในสภาพแวดล้อมแบบมัลติเธรด - ก่อนที่จะมีการแนะนำตัวนี้จะต้องเป็นคลาสที่ผู้ใช้เขียนซึ่งรวมวิธีการต่าง ๆ ไว้ในบล็อกแบบซิงโครไนซ์
ฉันใช้ AtomicInteger เพื่อแก้ปัญหาของปราชญ์การรับประทานอาหาร
ในโซลูชันของฉันอินสแตนซ์ AtomicInteger ถูกใช้เพื่อแสดงถึงส้อมมีสองสิ่งที่จำเป็นต่อนักปรัชญา นักปรัชญาแต่ละคนถูกระบุว่าเป็นจำนวนเต็ม 1 ถึง 5 เมื่อมีการใช้ส้อมโดยนักปรัชญา AtomicInteger จะเก็บค่าของนักปรัชญา 1 ถึง 5 มิฉะนั้นทางแยกจะไม่ถูกใช้ดังนั้นค่าของ AtomicInteger คือ -1 .
AtomicInteger จากนั้นอนุญาตให้ตรวจสอบว่าทางแยกเป็นอิสระค่า == - 1 และตั้งเป็นเจ้าของทางแยกถ้าว่างในการดำเนินการปรมาณู ดูรหัสด้านล่าง
AtomicInteger fork0 = neededForks[0];//neededForks is an array that holds the forks needed per Philosopher
AtomicInteger fork1 = neededForks[1];
while(true){
if (Hungry) {
//if fork is free (==-1) then grab it by denoting who took it
if (!fork0.compareAndSet(-1, p) || !fork1.compareAndSet(-1, p)) {
//at least one fork was not succesfully grabbed, release both and try again later
fork0.compareAndSet(p, -1);
fork1.compareAndSet(p, -1);
try {
synchronized (lock) {//sleep and get notified later when a philosopher puts down one fork
lock.wait();//try again later, goes back up the loop
}
} catch (InterruptedException e) {}
} else {
//sucessfully grabbed both forks
transition(fork_l_free_and_fork_r_free);
}
}
}
เนื่องจากเมธอด comparAndSet ไม่บล็อกจึงควรเพิ่มปริมาณงานทำให้งานเสร็จ ดังที่คุณอาจทราบว่าปัญหานักปรัชญาการรับประทานอาหารถูกนำมาใช้เมื่อจำเป็นต้องมีการควบคุมการเข้าถึงทรัพยากรเช่นส้อมมีความจำเป็นเช่นกระบวนการต้องการทรัพยากรเพื่อดำเนินงานต่อไป
ตัวอย่างง่าย ๆ สำหรับฟังก์ชั่น comparAndSet ():
import java.util.concurrent.atomic.AtomicInteger;
public class GFG {
public static void main(String args[])
{
// Initially value as 0
AtomicInteger val = new AtomicInteger(0);
// Prints the updated value
System.out.println("Previous value: "
+ val);
// Checks if previous value was 0
// and then updates it
boolean res = val.compareAndSet(0, 6);
// Checks if the value was updated.
if (res)
System.out.println("The value was"
+ " updated and it is "
+ val);
else
System.out.println("The value was "
+ "not updated");
}
}
สิ่งที่พิมพ์คือ: ค่าก่อนหน้า: 0 ค่าถูกอัพเดตและเป็น 6 อีกตัวอย่างง่าย ๆ :
import java.util.concurrent.atomic.AtomicInteger;
public class GFG {
public static void main(String args[])
{
// Initially value as 0
AtomicInteger val
= new AtomicInteger(0);
// Prints the updated value
System.out.println("Previous value: "
+ val);
// Checks if previous value was 0
// and then updates it
boolean res = val.compareAndSet(10, 6);
// Checks if the value was updated.
if (res)
System.out.println("The value was"
+ " updated and it is "
+ val);
else
System.out.println("The value was "
+ "not updated");
}
}
สิ่งที่พิมพ์คือ: ค่าก่อนหน้า: 0 ค่าไม่ได้รับการอัพเดต