การใช้งานจริงสำหรับ AtomicInteger


229

ฉันเข้าใจว่า AtomicInteger และตัวแปร Atomic อื่น ๆ อนุญาตการเข้าถึงพร้อมกัน โดยทั่วไปแล้วคลาสนี้จะใช้ในกรณีใดบ้าง?

คำตอบ:


190

มีการใช้สองหลัก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()) แทนการเพิ่ม (และประมวลผลผลลัพธ์ก่อนกลับ)


1
ฉันคิดว่าฉันเข้าใจการใช้งานครั้งแรก นี่คือเพื่อให้แน่ใจว่าตัวนับเพิ่มขึ้นก่อนที่จะเข้าถึงแอตทริบิวต์อีกครั้ง แก้ไข? คุณสามารถยกตัวอย่างสั้น ๆ สำหรับการใช้งานครั้งที่สองได้หรือไม่?
James P.

8
ความเข้าใจของคุณเกี่ยวกับการใช้งานครั้งแรกนั้นเป็นเรื่องจริง - มันช่วยให้มั่นใจได้ว่าหากเธรดอื่นแก้ไขตัวนับระหว่างreadและwrite that value + 1การดำเนินการสิ่งนี้จะถูกตรวจพบแทนการเขียนทับการอัปเดตเก่า (หลีกเลี่ยงปัญหา นี่เป็นกรณีพิเศษของcompareAndSet- ถ้าค่าเดิม2คลาสเรียกจริงcompareAndSet(2, 3)- ดังนั้นถ้าเธรดอื่นได้แก้ไขค่าในระหว่างนั้นวิธีการเพิ่มจะรีสตาร์ทอย่างมีประสิทธิภาพตั้งแต่ต้น
Andrzej Doyle

3
"ส่วนที่เหลือ> 0? ส่วนที่เหลือ: ส่วนที่เหลือ + n;" ในนิพจน์นี้มีเหตุผลที่จะเพิ่มเศษเหลือเป็น n เมื่อเป็น 0 หรือไม่
sandeepkunkunuru

100

ตัวอย่างที่ง่ายที่สุดที่ฉันนึกได้คือการเพิ่มการทำงานของอะตอม

ด้วย 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++และกลับมาจำ (หรือไม่) เพื่อให้ได้จอภาพที่ถูกต้องมาก่อน


2
ขอบคุณสำหรับคำอธิบายที่ชัดเจนนี้ อะไรคือข้อดีของการใช้ AtomicInteger เหนือคลาสที่มีการซิงโครไนส์วิธีทั้งหมดหรือไม่ หลังนี้จะถือว่าเป็น "หนักกว่า" หรือไม่?
James P.

3
จากมุมมองของฉันส่วนใหญ่คือการห่อหุ้มที่คุณได้รับด้วย AtomicIntegers - การซิงโครไนซ์เกิดขึ้นกับสิ่งที่คุณต้องการและคุณได้รับวิธีการอธิบายที่อยู่ใน API สาธารณะเพื่ออธิบายว่าผลลัพธ์ที่ได้คืออะไร (รวมถึงบางส่วนที่คุณพูดถูกมักจะจบลงด้วยการซิงโครไนซ์วิธีการทั้งหมดในชั้นเรียนซึ่งมีลักษณะหยาบเกินไปแม้ว่า HotSpot จะทำการปรับการล็อคให้เหมาะสมและกฎต่อการปรับให้เหมาะสมก่อนกำหนดฉันพิจารณาว่า ได้ประโยชน์มากกว่าประสิทธิภาพ)
Andrzej Doyle

นี่เป็นคำอธิบายที่ชัดเจนและแม่นยำขอบคุณมาก !!
Akash5288

ในที่สุดก็มีคำอธิบายที่เคลียร์ขึ้นมาสำหรับฉัน
Benny Bottema

58

หากคุณดูที่วิธีการที่ 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


37

การใช้หลักของการเป็นเมื่อคุณอยู่ในบริบทแบบมัลติเธรดและคุณจำเป็นต้องดำเนินการที่ปลอดภัยด้ายในจำนวนเต็มโดยไม่ต้องใช้AtomicInteger synchronizedนัดและการดึงกับชนิดดั้งเดิมintมีอะตอมอยู่แล้ว แต่มาพร้อมกับการดำเนินงานจำนวนมากที่ไม่ได้อะตอมAtomicIntegerint

ที่ง่ายที่สุดคือหรือ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อินสแตนซ์ในMaps เป็นค่า


1
ฉันไม่คิดว่าฉันต้องการใช้AtomicIntegerเป็นคีย์แผนที่เนื่องจากใช้การเริ่มต้นequals()ใช้งานซึ่งแทบจะไม่ใช่สิ่งที่คุณคาดหวังว่าความหมายจะถูกใช้หากใช้ในแผนที่
Andrzej Doyle

1
@Andrzej แน่นอนว่าไม่ใช่กุญแจที่จะต้องไม่มีค่า แต่เป็นค่า
gabuzo

@gabuzo ความคิดใดที่ว่าเลขจำนวนเต็มของอะตอมทำงานได้ดีกว่าการซิงโครไนซ์?
Supun Wijerathne

ตอนนี้การทดสอบค่อนข้างเก่า (มากกว่า 6 ปี) ฉันอาจจะสนใจการทดสอบ JRE ล่าสุดอีกครั้ง ฉันไม่ได้ลึกพอใน AtomicInteger เพื่อตอบ แต่เป็นงานที่เฉพาะเจาะจงมากมันจะใช้เทคนิคการซิงโครไนซ์ที่ทำงานเฉพาะในกรณีนี้เท่านั้น นอกจากนี้โปรดทราบว่าการทดสอบ monothreaded และการทดสอบที่คล้ายกันในสภาพแวดล้อมที่เต็มไปด้วยภาระหนักอาจไม่ให้ชัยชนะที่ชัดเจนสำหรับ AtomicInteger
gabuzo

ฉันเชื่อว่ามันเป็น 3 ms และ 5.5 ms
Sathesh

17

ตัวอย่างเช่นฉันมีห้องสมุดที่สร้างอินสแตนซ์ของบางคลาส แต่ละอินสแตนซ์เหล่านี้ต้องมี ID จำนวนเต็มเฉพาะเนื่องจากอินสแตนซ์เหล่านี้แสดงคำสั่งที่ส่งไปยังเซิร์ฟเวอร์และแต่ละคำสั่งต้องมี ID ที่ไม่ซ้ำกัน เนื่องจากมีหลายเธรดที่ได้รับอนุญาตให้ส่งคำสั่งพร้อมกันฉันจึงใช้ AtomicInteger เพื่อสร้าง ID เหล่านั้น วิธีการทางเลือกคือใช้การล็อคและจำนวนเต็มปกติ แต่ทั้งสองแบบนั้นช้ากว่าและสง่างามน้อยลง


ขอขอบคุณที่แบ่งปันตัวอย่างการปฏิบัตินี้ เสียงนี้เหมือนสิ่งที่ฉันควรใช้เท่าที่ฉันจะต้องมี ID ไม่ซ้ำกันสำหรับแต่ละผมนำเข้าไฟล์ลงในโปรแกรมของฉัน :)
เจมส์พี

7

เช่นเดียวกับ gabuzo ที่กล่าวว่าบางครั้งฉันใช้ AtomicIntegers เมื่อฉันต้องการผ่าน int โดยการอ้างอิง เป็นคลาสในตัวที่มีรหัสเฉพาะสถาปัตยกรรมดังนั้นจึงง่ายขึ้นและมีแนวโน้มที่จะปรับให้เหมาะสมยิ่งขึ้นกว่า MutableInteger ใด ๆ ที่ฉันสามารถเขียนโค้ดได้อย่างรวดเร็ว ที่กล่าวว่ามันให้ความรู้สึกเหมือนเป็นการละเมิดของชั้นเรียน


7

ในคลาสอะตอมของ Java 8 ได้ถูกขยายด้วยฟังก์ชันที่น่าสนใจสองฟังก์ชัน:

  • int getAndUpdate (ฟังก์ชันอัพเดต IntUnaryOperator)
  • int updateAndGet (ฟังก์ชันอัพเดต IntUnaryOperator)

ทั้งสองกำลังใช้ 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 ตัวอย่างปรมาณู


5

ฉันมักจะใช้ AtomicInteger เมื่อฉันต้องการให้ Id กับวัตถุที่สามารถ accesed หรือสร้างขึ้นจากหลายกระทู้และฉันมักจะใช้เป็นแอตทริบิวต์คงที่ในชั้นเรียนที่ฉันเข้าถึงในตัวสร้างของวัตถุ


4

คุณสามารถใช้การล็อคแบบไม่บล็อกโดยใช้ comparAndSwap (CAS) กับจำนวนเต็มหรือยาวของอะตอม "Tl2" ซอฟแวร์การทำธุรกรรมหน่วยความจำกระดาษอธิบายนี้:

เราเชื่อมโยงล็อกการเขียนรุ่นพิเศษกับทุกตำแหน่งหน่วยความจำที่ทำธุรกรรม ในรูปแบบที่ง่ายที่สุดล็อกการเขียนเวอร์ชันเป็นสปินล็อคคำเดียวที่ใช้การดำเนินการ CAS เพื่อรับการล็อกและสโตร์เพื่อปลดล็อค เนื่องจากต้องการเพียงบิตเดียวเพื่อระบุว่ามีการล็อกเราจึงใช้ส่วนที่เหลือของคำล็อคเพื่อเก็บหมายเลขเวอร์ชั่น

สิ่งที่อธิบายได้คืออ่านเลขจำนวนเต็มแบบอะตอมก่อน แยกส่วนนี้ออกเป็น lock-bit ที่ไม่สนใจและหมายเลขรุ่น พยายามเขียน CAS ในขณะที่ล็อคบิตถูกล้างด้วยหมายเลขเวอร์ชันปัจจุบันไปยังชุดล็อคบิตและหมายเลขเวอร์ชันถัดไป วนซ้ำจนกว่าคุณจะประสบความสำเร็จและคุณเป็นเธรดที่เป็นเจ้าของล็อค ปลดล็อคโดยการตั้งค่าหมายเลขรุ่นปัจจุบันด้วยการล้างบิตล็อค กระดาษอธิบายการใช้หมายเลขรุ่นในการล็อกเพื่อประสานงานที่เธรดมีชุดของการอ่านที่สอดคล้องกันเมื่อพวกเขาเขียน

บทความนี้อธิบายว่าโปรเซสเซอร์มีการสนับสนุนฮาร์ดแวร์สำหรับการเปรียบเทียบและสลับการดำเนินการทำให้มีประสิทธิภาพมาก มันยังอ้างว่า:

non-blocking ตัวนับ CAS โดยใช้ตัวแปรอะตอมมิกมีประสิทธิภาพที่ดีกว่าตัวนับแบบล็อกในการช่วงชิงระดับต่ำถึงปานกลาง


3

กุญแจสำคัญคืออนุญาตให้มีการเข้าถึงและแก้ไขพร้อมกันได้อย่างปลอดภัย พวกเขามักใช้เป็นตัวนับในสภาพแวดล้อมแบบมัลติเธรด - ก่อนที่จะมีการแนะนำตัวนี้จะต้องเป็นคลาสที่ผู้ใช้เขียนซึ่งรวมวิธีการต่าง ๆ ไว้ในบล็อกแบบซิงโครไนซ์


ฉันเห็น. นี่เป็นกรณีที่แอ็ตทริบิวต์หรืออินสแตนซ์ทำหน้าที่เป็นตัวแปรโกลบอลภายในแอ็พพลิเคชัน หรือมีกรณีอื่นที่คุณนึกออก
James P.

1

ฉันใช้ 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 ไม่บล็อกจึงควรเพิ่มปริมาณงานทำให้งานเสร็จ ดังที่คุณอาจทราบว่าปัญหานักปรัชญาการรับประทานอาหารถูกนำมาใช้เมื่อจำเป็นต้องมีการควบคุมการเข้าถึงทรัพยากรเช่นส้อมมีความจำเป็นเช่นกระบวนการต้องการทรัพยากรเพื่อดำเนินงานต่อไป


0

ตัวอย่างง่าย ๆ สำหรับฟังก์ชั่น 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 ค่าไม่ได้รับการอัพเดต

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.