เหตุใด i ++ จึงไม่ใช่ปรมาณู


97

เหตุใดi++อะตอมจึงไม่อยู่ใน Java

เพื่อให้ลึกลงไปอีกนิดใน Java ฉันพยายามนับความถี่ในการดำเนินการลูปในเธรด

ดังนั้นฉันจึงใช้ไฟล์

private static int total = 0;

ในคลาสหลัก

ฉันมีสองกระทู้

  • หัวข้อ 1: พิมพ์ System.out.println("Hello from Thread 1!");
  • หัวข้อ 2: พิมพ์ System.out.println("Hello from Thread 2!");

และฉันนับบรรทัดที่พิมพ์ด้วยเธรด 1 และเธรด 2 แต่บรรทัดของเธรด 1 + บรรทัดของเธรด 2 ไม่ตรงกับจำนวนบรรทัดทั้งหมดที่พิมพ์ออกมา

นี่คือรหัสของฉัน:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;

public class Test {

    private static int total = 0;
    private static int countT1 = 0;
    private static int countT2 = 0;
    private boolean run = true;

    public Test() {
        ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
        newCachedThreadPool.execute(t1);
        newCachedThreadPool.execute(t2);
        try {
            Thread.sleep(1000);
        }
        catch (InterruptedException ex) {
            Logger.getLogger(Test.class.getName()).log(Level.SEVERE, null, ex);
        }
        run = false;
        try {
            Thread.sleep(1000);
        }
        catch (InterruptedException ex) {
            Logger.getLogger(Test.class.getName()).log(Level.SEVERE, null, ex);
        }
        System.out.println((countT1 + countT2 + " == " + total));
    }

    private Runnable t1 = new Runnable() {
        @Override
        public void run() {
            while (run) {
                total++;
                countT1++;
                System.out.println("Hello #" + countT1 + " from Thread 2! Total hello: " + total);
            }
        }
    };

    private Runnable t2 = new Runnable() {
        @Override
        public void run() {
            while (run) {
                total++;
                countT2++;
                System.out.println("Hello #" + countT2 + " from Thread 2! Total hello: " + total);
            }
        }
    };

    public static void main(String[] args) {
        new Test();
    }
}

14
ทำไมคุณไม่ลองด้วยAtomicIntegerล่ะ?
Braj


3
JVM มีการiincดำเนินการสำหรับการเพิ่มจำนวนเต็ม แต่ใช้ได้กับตัวแปรภายในเท่านั้นโดยที่การทำงานพร้อมกันไม่เป็นปัญหา สำหรับฟิลด์คอมไพลเลอร์จะสร้างคำสั่งอ่านแก้ไขเขียนแยกกัน
Silly Freak

14
ทำไมคุณถึงคาดหวังว่ามันจะเป็นปรมาณู?
Hot Licks

2
@Silly Freak: แม้ว่าจะมีiincคำสั่งสำหรับฟิลด์ แต่การมีคำสั่งเดียวไม่ได้รับประกันความเป็นอะตอมเช่นการเข้าถึงที่ไม่ใช่volatile longและdoubleฟิลด์โดยไม่รับประกันว่าจะเป็นอะตอมโดยไม่คำนึงถึงข้อเท็จจริงที่ว่าจะดำเนินการโดยคำสั่งไบต์โค้ดเดียว
Holger

คำตอบ:


125

i++อาจจะไม่อะตอมในชวาเพราะ atomicity i++เป็นความต้องการพิเศษที่ไม่ได้อยู่ในส่วนของการใช้งานของ ข้อกำหนดนั้นมีค่าใช้จ่ายที่สำคัญ: มีค่าใช้จ่ายจำนวนมากในการสร้างอะตอมของการดำเนินการที่เพิ่มขึ้น มันเกี่ยวข้องกับการซิงโครไนซ์ทั้งในระดับซอฟต์แวร์และฮาร์ดแวร์ที่ไม่จำเป็นต้องเพิ่มขึ้นตามปกติ

คุณสามารถทำให้ข้อโต้แย้งว่าควรจะได้รับการออกแบบและจัดทำเอกสารเป็นเฉพาะการดำเนินการเพิ่มขึ้นของอะตอมเพื่อให้เพิ่มขึ้นไม่ใช่อะตอมจะดำเนินการใช้i++ i = i + 1อย่างไรก็ตามสิ่งนี้จะทำลาย "ความเข้ากันได้ทางวัฒนธรรม" ระหว่าง Java และ C และ C ++ เช่นกันมันจะใช้สัญกรณ์ที่สะดวกสบายซึ่งโปรแกรมเมอร์ที่คุ้นเคยกับภาษาคล้ายซีใช้เพื่อให้มันมีความหมายพิเศษที่ใช้เฉพาะในสถานการณ์ที่ จำกัด

รหัส C หรือ C ++ พื้นฐานต้องการfor (i = 0; i < LIMIT; i++)แปลเป็น Java เป็นfor (i = 0; i < LIMIT; i = i + 1); i++เพราะมันจะไม่เหมาะสมที่จะใช้อะตอม ที่แย่ไปกว่านั้นคือโปรแกรมเมอร์ที่มาจากภาษา C หรือภาษาอื่น ๆ ที่คล้าย C ถึง Java จะใช้i++ต่อไปส่งผลให้มีการใช้คำสั่งอะตอมโดยไม่จำเป็น

แม้ในระดับชุดคำสั่งของเครื่องการดำเนินการประเภทการเพิ่มขึ้นมักจะไม่เป็นปรมาณูเนื่องจากเหตุผลด้านประสิทธิภาพ ใน x86 ต้องใช้คำสั่งพิเศษ "lock prefix" เพื่อสร้างincคำสั่ง atomic: ด้วยเหตุผลเดียวกับข้างต้น ถ้าincเป็นปรมาณูเสมอมันจะไม่ถูกใช้เมื่อจำเป็นต้องใช้ non-atomic inc โปรแกรมเมอร์และคอมไพเลอร์จะสร้างโค้ดที่โหลดเพิ่ม 1 และจัดเก็บเพราะมันจะเร็วกว่า

ในสถาปัตยกรรมชุดคำสั่งบางชุดไม่มีอะตอมincหรืออาจไม่มีincเลย ในการทำ atomic inc บน MIPS คุณต้องเขียนซอฟต์แวร์วนซ้ำซึ่งใช้lland sc: load-linked และ store-conditional Load-linked อ่านคำและ store-conditional จะจัดเก็บค่าใหม่หากคำนั้นไม่มีการเปลี่ยนแปลงหรือมิฉะนั้นจะล้มเหลว (ซึ่งตรวจพบและทำให้เกิดการลองใหม่)


2
เนื่องจาก java ไม่มีพอยน์เตอร์การเพิ่มตัวแปรโลคัลจะเป็นการบันทึกเธรดโดยเนื้อแท้ดังนั้นการวนซ้ำปัญหาส่วนใหญ่จะไม่เลวร้ายนัก ประเด็นของคุณเกี่ยวกับจุดยืนที่น่าประหลาดใจน้อยที่สุดแน่นอน ตามที่เป็นอยู่i = i + 1จะเป็นการแปล++iไม่ใช่i++
Silly Freak

22
คำแรกของคำถามคือ "ทำไม" ณ ตอนนี้นี่เป็นคำตอบเดียวที่จะช่วยแก้ปัญหา "ทำไม" คำตอบอื่น ๆ เพียงแค่ระบุคำถามใหม่ ดังนั้น +1
Dawood ibn Kareem

3
อาจเป็นที่น่าสังเกตว่าการรับประกันปรมาณูจะไม่ช่วยแก้ปัญหาการมองเห็นสำหรับการอัปเดตที่ไม่ใช่volatileฟิลด์ ดังนั้นหากคุณไม่ปฏิบัติตามทุกฟิลด์โดยปริยายvolatileเมื่อเธรดหนึ่งใช้ตัว++ดำเนินการกับมันการรับประกันความเป็นอะตอมจะไม่สามารถแก้ปัญหาการอัปเดตพร้อมกันได้ เหตุใดจึงอาจสิ้นเปลืองประสิทธิภาพสำหรับบางสิ่งหากไม่สามารถแก้ปัญหาได้
Holger

1
@DavidWallace คุณไม่ได้หมายความว่า++อย่างไร ;)
Dan Hlavenka

36

i++ เกี่ยวข้องกับการดำเนินการสองอย่าง:

  1. อ่านค่าปัจจุบันของ i
  2. เพิ่มค่าและกำหนดให้ i

เมื่อเธรดสองเธรดทำงานi++กับตัวแปรเดียวกันพร้อมกันทั้งคู่อาจได้รับค่าปัจจุบันเท่ากันiจากนั้นจึงเพิ่มขึ้นและตั้งค่าเป็นi+1ดังนั้นคุณจะได้รับการเพิ่มทีละครั้งแทนที่จะเป็นสอง

ตัวอย่าง:

int i = 5;
Thread 1 : i++;
           // reads value 5
Thread 2 : i++;
           // reads value 5
Thread 1 : // increments i to 6
Thread 2 : // increments i to 6
           // i == 6 instead of 7

(แม้ว่าจะi++ เป็นปรมาณู แต่ก็ไม่ได้กำหนดไว้อย่างดี / พฤติกรรมที่ปลอดภัยของเธรด)
user2864740

15
+1 แต่ "1. A, 2. B และ C" ดูเหมือนการดำเนินการสามครั้งไม่ใช่สอง :)
yshavit

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

3
@Aquarelle - หากโปรเซสเซอร์สองตัวดำเนินการเดียวกันกับที่เก็บข้อมูลเดียวกันพร้อมกันและไม่มีการออกอากาศ "สำรอง" ในสถานที่นั้นพวกเขาแทบจะรบกวนและสร้างผลลัพธ์ที่ผิดพลาด ใช่มันเป็นไปได้ที่การดำเนินการนี้จะ "ปลอดภัย" แต่ต้องใช้ความพยายามเป็นพิเศษแม้ในระดับฮาร์ดแวร์
Hot Licks

6
แต่ฉันคิดว่าคำถามคือ "ทำไม" ไม่ใช่ "เกิดอะไรขึ้น"
Sebastian Mach

11

สิ่งที่สำคัญคือ JLS (ข้อกำหนดภาษา Java) แทนที่จะเป็นวิธีการใช้งาน JVM ที่หลากหลายอาจมีหรือไม่มีคุณสมบัติบางอย่างของภาษา JLS กำหนดตัวดำเนินการ ++ postfix ในข้อ 15.14.2 ซึ่งระบุว่า ia "ค่า 1 ถูกเพิ่มให้กับค่าของตัวแปรและผลรวมจะถูกเก็บไว้ในตัวแปร" ไม่มีที่ไหนกล่าวถึงหรือบอกใบ้ถึงมัลติเธรดหรือปรมาณู สำหรับเหล่านี้ JLS ให้ระเหยและตรงกัน นอกจากนี้ยังมีแพ็คเกจjava.util.concurrent.atomic (ดูhttp://docs.oracle.com/javase/7/docs/api/java/util/concurrent/atomic/package-summary.html )


5

เหตุใด i ++ จึงไม่ใช่ atomic ใน Java

มาแบ่งการดำเนินการเพิ่มเป็นหลายคำสั่ง:

หัวข้อที่ 1 และ 2:

  1. ดึงมูลค่ารวมจากหน่วยความจำ
  2. เพิ่ม 1 ในค่า
  3. เขียนกลับไปที่ความทรงจำ

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


2
ฉันไม่เข้าใจว่านี่ควรเป็นคำตอบสำหรับคำถามนี้อย่างไร ภาษาสามารถกำหนดคุณลักษณะใด ๆ เป็นอะตอมไม่ว่าจะเป็นแบบเพิ่มขึ้นหรือยูนิคอร์น คุณเพียงแค่ยกตัวอย่างผลของการไม่เป็นปรมาณู
Sebastian Mach

ใช่ภาษาสามารถกำหนดคุณลักษณะใด ๆ เป็นอะตอมได้ แต่เท่าที่จาวาถือว่าเป็นตัวดำเนินการที่เพิ่มขึ้น (ซึ่งเป็นคำถามที่โพสต์โดย OP) ไม่ใช่อะตอมและคำตอบของฉันระบุเหตุผล
Aniket Thakur

1
(ขออภัยสำหรับน้ำเสียงที่รุนแรงของฉันในความคิดเห็นแรก) แต่แล้วเหตุผลน่าจะเป็น "เพราะถ้าเป็นปรมาณูก็จะไม่มีเงื่อนไขการแข่งขัน" กล่าวคือดูเหมือนว่าสภาพการแข่งขันเป็นที่ต้องการ
Sebastian Mach

@phresnel ค่าโสหุ้ยที่นำมาใช้เพื่อให้อะตอมที่เพิ่มขึ้นนั้นมีขนาดใหญ่มากและไม่ค่อยต้องการทำให้การดำเนินการมีราคาถูกและเป็นที่ต้องการเกือบตลอดเวลา
josefx

4
@josefx: โปรดทราบว่าฉันไม่ได้ตั้งคำถามกับข้อเท็จจริง แต่เป็นเหตุผลในคำตอบนี้ โดยพื้นฐานแล้วมันบอกว่า"i ++ ไม่ใช่ปรมาณูใน Java เนื่องจากสภาพการแข่งขันมันมี"ซึ่งเหมือนกับการพูดว่า"รถไม่มีถุงลมนิรภัยเนื่องจากการชนที่อาจเกิดขึ้นได้"หรือ"คุณไม่ได้มีดตามคำสั่ง Currywurst เพราะ อาจต้องตัด wurst " . ดังนั้นฉันไม่คิดว่านี่คือคำตอบ คำถามไม่ใช่"i ++ ทำอะไร" หรือ"ผลที่ตามมาของการไม่ซิงค์ i ++ คืออะไร" .
Sebastian Mach

5

i++ เป็นคำสั่งที่เกี่ยวข้องกับการดำเนินการ 3 อย่าง:

  1. อ่านค่าปัจจุบัน
  2. เขียนค่าใหม่
  3. จัดเก็บค่าใหม่

การดำเนินการทั้งสามนี้ไม่ได้หมายถึงการดำเนินการในขั้นตอนเดียวหรือกล่าวอีกนัยหนึ่งi++ไม่ใช่การ ดำเนินการแบบผสม เป็นผลให้ทุกสิ่งผิดพลาดได้เมื่อมีเธรดมากกว่าหนึ่งเธรดที่เกี่ยวข้องในการดำเนินการเดียว แต่ไม่รวมกัน

พิจารณาสถานการณ์ต่อไปนี้:

ครั้งที่ 1 :

Thread A fetches i
Thread B fetches i

เวลา 2 :

Thread A overwrites i with a new value say -foo-
Thread B overwrites i with a new value say -bar-
Thread B stores -bar- in i

// At this time thread B seems to be more 'active'. Not only does it overwrite 
// its local copy of i but also makes it in time to store -bar- back to 
// 'main' memory (i)

ครั้งที่ 3 :

Thread A attempts to store -foo- in memory effectively overwriting the -bar- 
value (in i) which was just stored by thread B in Time 2.

Thread B has nothing to do here. Its work was done by Time 2. However it was 
all for nothing as -bar- was eventually overwritten by another thread.

และคุณก็มี สภาพการแข่งขัน


นั่นเป็นเหตุผลที่i++ไม่ใช่ปรมาณู ถ้าเป็นเช่นนั้นสิ่งนี้จะไม่เกิดขึ้นและแต่ละอย่างfetch-update-storeจะเกิดขึ้นในอะตอม นั่นคือสิ่งที่AtomicIntegerมีไว้สำหรับและในกรณีของคุณมันอาจจะพอดี

ปล

หนังสือที่ยอดเยี่ยมครอบคลุมประเด็นเหล่านั้นทั้งหมดและบางเล่มก็คือ: Java Concurrency in Practice


1
อืม. ภาษาสามารถกำหนดคุณลักษณะใด ๆ เป็นอะตอมไม่ว่าจะเป็นแบบเพิ่มขึ้นหรือยูนิคอร์น คุณเพียงแค่ยกตัวอย่างผลของการไม่เป็นปรมาณู
Sebastian Mach

@phresnel เป๊ะ. แต่ฉันยังชี้ให้เห็นว่ามันไม่ใช่การดำเนินการเดียวซึ่งโดยส่วนขยายแสดงให้เห็นว่าค่าใช้จ่ายในการคำนวณสำหรับการเปลี่ยนการดำเนินการหลายอย่างให้เป็นอะตอมนั้นแพงกว่ามากซึ่งในทางกลับกัน - บางส่วน - แสดงให้เห็นว่าทำไมถึงi++ไม่เป็นอะตอม
kstratis

1
ในขณะที่ฉันเข้าใจคำตอบของคุณก็ค่อนข้างสับสนกับการเรียนรู้ ฉันเห็นตัวอย่างและข้อสรุปที่ระบุว่า "เนื่องจากสถานการณ์ในตัวอย่าง"; นี่เป็นเหตุผลที่ไม่สมบูรณ์ :(
Sebastian Mach

1
@phresnel อาจไม่ใช่คำตอบที่สอนได้มากที่สุด แต่เป็นคำตอบที่ดีที่สุดที่ฉันสามารถเสนอได้ หวังว่าจะช่วยให้ผู้คนและไม่สับสน อย่างไรก็ตามขอขอบคุณสำหรับการวิจารณ์ ฉันจะพยายามเขียนโพสต์ในอนาคตให้แม่นยำยิ่งขึ้น
kstratis

2

ใน JVM การเพิ่มขึ้นเกี่ยวข้องกับการอ่านและการเขียนดังนั้นจึงไม่ใช่ปรมาณู


2

หากการดำเนินการi++เป็นแบบปรมาณูคุณจะไม่มีโอกาสอ่านค่าจากมัน นี่คือสิ่งที่คุณต้องการทำโดยใช้i++(แทนที่จะใช้++i)

ตัวอย่างเช่นดูรหัสต่อไปนี้:

public static void main(final String[] args) {
    int i = 0;
    System.out.println(i++);
}

ในกรณีนี้เราคาดว่าผลลัพธ์จะเป็น: 0 (เนื่องจากเราโพสต์ส่วนเพิ่มเช่นอ่านก่อนแล้วอัปเดต)

นี่เป็นสาเหตุหนึ่งที่การดำเนินการไม่สามารถเป็นอะตอมได้เนื่องจากคุณต้องอ่านค่า (และทำอะไรบางอย่างกับมัน) จากนั้นอัปเดตค่า

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


2
นี่เป็นความเข้าใจผิด คุณต้องแยกการดำเนินการและการรับผลลัพธ์มิฉะนั้นคุณจะไม่สามารถรับค่าจากการดำเนินการอะตอมใด ๆได้
Sebastian Mach

ไม่ใช่นั่นคือสาเหตุที่ AtomicInteger ของ Java มี get (), getAndIncrement (), getAndDecrement (), IncrementAndGet (), degmentAndGet () เป็นต้น
Roy van Rijn

1
และภาษา Java สามารถกำหนดi++ให้ขยายเป็นi.getAndIncrement(). การขยายตัวดังกล่าวไม่ใช่เรื่องใหม่ เช่น lambdas ใน C ++ ถูกขยายเป็นนิยามคลาสที่ไม่ระบุชื่อใน C ++
Sebastian Mach

การระบุอะตอมi++สามารถสร้างอะตอม++iหรือในทางกลับกันได้เล็กน้อย หนึ่งเทียบเท่ากับอีกบวกหนึ่ง
David Schwartz

2

มีสองขั้นตอน:

  1. ดึงฉันจากหน่วยความจำ
  2. ตั้งค่า i + 1 เป็น i

ดังนั้นจึงไม่ใช่การทำงานของอะตอม เมื่อ thread1 รัน i ++ และ thread2 รัน i ++ ค่าสุดท้ายของ i อาจเป็น i + 1


-1

เห็นพ้องด้วย (คนThreadชั้นเรียนและดังกล่าว) เป็นคุณลักษณะที่เพิ่มเข้ามาใน v1.0 ของJava i++ถูกเพิ่มในเบต้าก่อนหน้านั้นและด้วยเหตุนี้จึงยังมีโอกาสมากกว่าในการใช้งานดั้งเดิม (มากหรือน้อย)

ขึ้นอยู่กับโปรแกรมเมอร์ในการซิงโครไนซ์ตัวแปร ตรวจสอบการกวดวิชาของออราเคิลเกี่ยวกับเรื่องนี้

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

ตัวดำเนินการ ++ ถูกกำหนดไว้ใน B (1969) ซึ่งมาก่อนจาวาและเธรดโดยใช้เพียงเล็กน้อย


-1 "public class Thread ... ตั้งแต่: JDK1.0" ที่มา: docs.oracle.com/javase/7/docs/api/index.html?java/lang/…
Silly Freak

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

5
สิ่งสำคัญคือการอ้างสิทธิ์ของคุณ "ยังคงดำเนินการก่อนคลาสเธรด" ไม่ได้รับการสนับสนุนจากแหล่งที่มา i++การไม่ใช้ปรมาณูเป็นการตัดสินใจในการออกแบบไม่ใช่การกำกับดูแลระบบที่กำลังเติบโต
Silly Freak

น่ารักจัง i ++ ถูกกำหนดไว้ก่อนเธรดเพียงเพราะมีภาษาที่มีอยู่ก่อน Java ผู้สร้าง Java ใช้ภาษาอื่น ๆ เหล่านั้นเป็นฐานแทนที่จะกำหนดขั้นตอนที่ยอมรับกันใหม่ ไหนไม่เคยบอกว่าเป็นการกำกับดูแล
TheBat

@SillyFreak นี่คือแหล่งข้อมูลบางส่วนที่แสดงให้เห็นว่า ++ อายุเท่าไหร่: en.wikipedia.org/wiki/Increment_and_decrement_operators en.wikipedia.org/wiki/B_(programming_language)
TheBat
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.