Tại sao i ++ không phải là nguyên tử?


97

Tại sao i++không phải là nguyên tử trong Java?

Để hiểu sâu hơn một chút về Java, tôi đã cố gắng đếm tần suất vòng lặp trong các luồng được thực thi.

Vì vậy, tôi đã sử dụng một

private static int total = 0;

trong lớp học chính.

Tôi có hai chủ đề.

  • Chủ đề 1: Bản in System.out.println("Hello from Thread 1!");
  • Chủ đề 2: Bản in System.out.println("Hello from Thread 2!");

Và tôi đếm số dòng được in ra bởi sợi 1 và sợi 2. Nhưng số dòng của sợi 1 + dòng của sợi 2 không khớp với tổng số dòng được in ra.

Đây là mã của tôi:

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
Tại sao bạn không thử với AtomicInteger?
Braj


3
JVM có một iinchoạt động để tăng số nguyên, nhưng điều đó chỉ hoạt động cho các biến cục bộ, nơi mà sự đồng thời không phải là mối quan tâm. Đối với các trường, trình biên dịch tạo các lệnh đọc-sửa đổi-ghi một cách riêng biệt.
Silly Freak

14
Tại sao bạn thậm chí mong đợi nó là nguyên tử?
Hot Licks

2
@Silly Freak: ngay cả khi có một iinclệnh cho các trường, việc có một lệnh duy nhất không đảm bảo tính nguyên tử, ví dụ như truy cập trường không volatile longdoubletrường không được đảm bảo là nguyên tử bất kể thực tế là nó được thực hiện bởi một lệnh bytecode.
Holger

Câu trả lời:


125

i++có lẽ không phải là nguyên tử trong Java vì tính nguyên tử là một yêu cầu đặc biệt không có trong phần lớn các ứng dụng của i++. Yêu cầu đó có một chi phí đáng kể: có một chi phí lớn trong việc chế tạo một nguyên tử hoạt động gia tăng; nó liên quan đến việc đồng bộ hóa ở cả cấp độ phần mềm và phần cứng mà không cần thiết phải có trong một gia số thông thường.

Bạn có thể đưa ra đối số mà lẽ ra i++phải được thiết kế và lập thành tài liệu là thực hiện cụ thể một phép tăng nguyên tử, để một phép tăng phi nguyên tử được thực hiện bằng cách sử dụng i = i + 1. Tuy nhiên, điều này sẽ phá vỡ "tính tương thích văn hóa" giữa Java, C và C ++. Ngoài ra, nó sẽ làm mất đi một ký hiệu thuận tiện mà các lập trình viên quen thuộc với các ngôn ngữ giống C coi thường, khiến nó có một ý nghĩa đặc biệt chỉ áp dụng trong một số trường hợp hạn chế.

Mã C hoặc C ++ cơ bản for (i = 0; i < LIMIT; i++)muốn dịch sang Java như for (i = 0; i < LIMIT; i = i + 1); bởi vì sẽ không thích hợp nếu sử dụng nguyên tử i++. Điều tồi tệ hơn, các lập trình viên từ C hoặc các ngôn ngữ giống C khác sang Java sẽ sử dụng i++, dẫn đến việc sử dụng các lệnh nguyên tử không cần thiết.

Ngay cả ở mức thiết lập lệnh máy, hoạt động kiểu gia tăng thường không phải là nguyên tử vì lý do hiệu suất. Trong x86, một lệnh đặc biệt "tiền tố khóa" phải được sử dụng để tạo inclệnh nguyên tử: vì các lý do tương tự như trên. Nếu incluôn luôn là nguyên tử, nó sẽ không bao giờ được sử dụng khi yêu cầu không phải nguyên tử; các lập trình viên và trình biên dịch sẽ tạo mã tải, thêm 1 và lưu trữ, bởi vì nó sẽ nhanh hơn.

Trong một số kiến ​​trúc tập lệnh, không có nguyên tử inchoặc có lẽ không có inc; để thực hiện một bước nguyên tử trên MIPS, bạn phải viết một vòng lặp phần mềm sử dụng llsc: tải liên kết và điều kiện lưu trữ. Load-linked đọc từ và lưu trữ có điều kiện lưu trữ giá trị mới nếu từ đó không thay đổi, nếu không từ đó không thành công (được phát hiện và gây ra thử lại).


2
vì java không có con trỏ, việc tăng các biến cục bộ vốn dĩ là lưu luồng, vì vậy với các vòng lặp, vấn đề chủ yếu sẽ không quá tệ. tất nhiên là quan điểm của bạn về điểm ít ngạc nhiên nhất. ngoài ra, như nó vốn có, i = i + 1sẽ là một bản dịch cho ++i, không phảii++
Silly Freak

22
Từ đầu tiên của câu hỏi là "tại sao". Hiện tại, đây là câu trả lời duy nhất để giải quyết vấn đề "tại sao". Các câu trả lời khác thực sự chỉ trình bày lại câu hỏi. Vì vậy, +1.
Dawood ibn Kareem

3
Có thể cần lưu ý rằng một bảo đảm về tính nguyên tử sẽ không giải quyết được vấn đề hiển thị đối với các bản cập nhật của các volatiletrường không phải là trường. Vì vậy, trừ khi bạn sẽ coi mọi trường là hoàn toàn volatilesau khi một luồng đã sử dụng ++toán tử trên đó, việc đảm bảo tính nguyên tử như vậy sẽ không giải quyết các vấn đề cập nhật đồng thời. Vì vậy, tại sao có khả năng lãng phí hiệu suất cho một cái gì đó nếu nó không giải quyết được vấn đề.
Holger

1
Ý bạn là @DavidWallace ++? ;)
Dan Hlavenka

36

i++ liên quan đến hai hoạt động:

  1. đọc giá trị hiện tại của i
  2. tăng giá trị và gán nó cho i

Khi hai luồng thực hiện i++trên cùng một biến cùng một lúc, cả hai có thể nhận cùng giá trị hiện tại i, sau đó tăng và đặt nó thành i+1, vì vậy bạn sẽ nhận được một mức tăng duy nhất thay vì hai.

Thí dụ :

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

(Thậm chí nếu i++ nguyên tử, nó sẽ không được rõ ràng / thread-safe hành vi.)
user2864740

15
+1, nhưng "1. A, 2. B và C" nghe giống như ba phép toán, không phải hai. :)
yshavit

3
Lưu ý rằng ngay cả khi hoạt động được thực hiện với một lệnh máy duy nhất làm tăng vị trí lưu trữ tại chỗ, không có gì đảm bảo rằng nó sẽ an toàn theo luồng. Máy vẫn cần tìm nạp giá trị, tăng giá trị và lưu trữ lại, ngoài ra có thể có nhiều bản sao bộ nhớ cache của vị trí lưu trữ đó.
Hot Licks

3
@Aquarelle - Nếu hai bộ xử lý thực hiện cùng một hoạt động trên cùng một vị trí lưu trữ đồng thời và không có phát sóng "dự trữ" trên vị trí đó, thì chúng gần như chắc chắn sẽ can thiệp và tạo ra kết quả không có thật. Có, có thể hoạt động này "an toàn", nhưng nó cần nỗ lực đặc biệt, ngay cả ở cấp độ phần cứng.
Hot Licks

6
Nhưng tôi nghĩ câu hỏi là "Tại sao" chứ không phải "Chuyện gì xảy ra".
Sebastian Mach

11

Điều quan trọng là JLS (Đặc tả ngôn ngữ Java) chứ không phải là cách các triển khai khác nhau của JVM có thể đã hoặc chưa thể triển khai một tính năng nhất định của ngôn ngữ. JLS định nghĩa toán tử hậu tố ++ trong điều khoản 15.14.2 cho biết ia "giá trị 1 được thêm vào giá trị của biến và tổng được lưu trữ trở lại biến". Không nơi nào nó đề cập hoặc gợi ý về đa luồng hoặc tính nguyên tử. Đối với những điều này, JLS cung cấp tính dễ bay hơiđồng bộ . Ngoài ra, có gói java.util.concurrent.atomic (xem http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/atomic/package-summary.html )


5

Tại sao i ++ không phải là nguyên tử trong Java?

Hãy chia hoạt động tăng dần thành nhiều câu lệnh:

Chủ đề 1 & 2:

  1. Tìm nạp tổng giá trị từ bộ nhớ
  2. Thêm 1 vào giá trị
  3. Viết lại vào bộ nhớ

Nếu không có đồng bộ hóa thì giả sử Thread một đã đọc giá trị 3 và tăng nó lên 4, nhưng chưa ghi lại. Tại thời điểm này, chuyển đổi ngữ cảnh xảy ra. Luồng hai đọc giá trị 3, tăng nó và chuyển đổi ngữ cảnh xảy ra. Mặc dù cả hai luồng đều đã tăng tổng giá trị, nó vẫn sẽ là điều kiện 4 - chủng tộc.


2
Tôi không hiểu đây phải là câu trả lời cho câu hỏi như thế nào. Một ngôn ngữ có thể xác định bất kỳ tính năng nào là nguyên tử, có thể là gia số hoặc kỳ lân. Bạn chỉ là ví dụ về một hệ quả của việc không phải là nguyên tử.
Sebastian Mach

Có một ngôn ngữ có thể xác định bất kỳ tính năng nào là nguyên tử nhưng theo như java được coi là toán tử tăng dần (là câu hỏi được đăng bởi OP) không phải là nguyên tử và câu trả lời của tôi nêu rõ lý do.
Aniket Thakur

1
(xin lỗi vì giọng điệu gay gắt của tôi trong bình luận đầu tiên) Nhưng sau đó, lý do dường như là "bởi vì nếu nó là nguyên tử, thì sẽ không có điều kiện chủng tộc". Tức là, có vẻ như một điều kiện chủng tộc là mong muốn.
Sebastian Mach

@phresnel chi phí được giới thiệu để giữ một nguyên tử gia tăng là rất lớn và hiếm khi mong muốn, giữ cho hoạt động rẻ và kết quả là không phải nguyên tử là mong muốn hầu hết thời gian.
josefx

4
@josefx: Lưu ý rằng tôi không đặt câu hỏi về sự kiện, mà là lý do trong câu trả lời này. Về cơ bản, nó nói rằng "i ++ không phải là nguyên tử trong Java vì điều kiện cuộc đua mà nó có" , điều này giống như nói "một chiếc xe không có túi khí vì những va chạm có thể xảy ra" hoặc "bạn không có dao với lệnh currywurst của bạn vì wurst có thể cần phải được cắt giảm " . Vì vậy, tôi không nghĩ đây là một câu trả lời. Câu hỏi không phải là "Tôi ++ làm gì?" hoặc "Hệ quả của việc i ++ không được đồng bộ hóa là gì?" .
Sebastian Mach

5

i++ là một câu lệnh chỉ bao gồm 3 hoạt động:

  1. Đọc giá trị hiện tại
  2. Viết giá trị mới
  3. Lưu trữ giá trị mới

Ba hoạt động này không có nghĩa là được thực hiện trong một bước duy nhất hay nói cách khác i++không phải là một phép toán ghép . Kết quả là tất cả mọi thứ đều có thể xảy ra sai sót khi nhiều hơn một luồng tham gia vào một hoạt động đơn lẻ nhưng không phải là kết hợp.

Hãy xem xét tình huống sau:

Lần 1 :

Thread A fetches i
Thread B fetches i

Thời gian 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)

Lần 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.

Và bạn có nó rồi đấy! Một điều kiện chủng tộc.


Đó là lý do tại sao i++không phải là nguyên tử. Nếu đúng như vậy thì sẽ không có chuyện này xảy ra và mỗi cái fetch-update-storesẽ xảy ra nguyên tử. Đó chính xác AtomicIntegerlà những gì dành cho và trong trường hợp của bạn, nó có thể sẽ phù hợp.

PS

Một cuốn sách xuất sắc bao gồm tất cả những vấn đề đó và sau đó là một số vấn đề này: Java Concurrency in Practice


1
Hừ! Một ngôn ngữ có thể xác định bất kỳ tính năng nào là nguyên tử, có thể là gia số hoặc kỳ lân. Bạn chỉ là ví dụ về một hệ quả của việc không phải là nguyên tử.
Sebastian Mach

@phresnel Chính xác. Nhưng tôi cũng chỉ ra rằng đó không phải là một phép toán đơn lẻ mà theo phần mở rộng ngụ ý rằng chi phí tính toán để biến nhiều phép toán như vậy thành các phép toán nguyên tử đắt hơn nhiều, điều đó - về phương diện - biện minh tại sao i++không phải là nguyên tử.
kstratis

1
Mặc dù tôi hiểu ý bạn, nhưng câu trả lời của bạn hơi khó hiểu với việc học. Tôi thấy một ví dụ, và một kết luận nói rằng "vì tình huống trong ví dụ"; imho đây là một suy luận không đầy đủ :(
Sebastian Mach

1
@phresnel Có thể không phải là câu trả lời sư phạm nhất nhưng đó là câu trả lời tốt nhất hiện tại tôi có thể đưa ra. Hi vọng nó sẽ giúp ích cho mọi người và không nhầm lẫn. Tuy nhiên, cảm ơn vì chủ nghĩa phê bình. Tôi sẽ cố gắng nói chính xác hơn trong các bài viết trong tương lai của mình.
kstratis

2

Trong JVM, gia số liên quan đến việc đọc và ghi, vì vậy nó không phải là nguyên tử.


2

Nếu hoạt động i++là nguyên tử, bạn sẽ không có cơ hội đọc giá trị từ nó. Đây chính xác là những gì bạn muốn làm bằng cách sử dụng i++(thay vì sử dụng ++i).

Ví dụ, hãy xem đoạn mã sau:

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

Trong trường hợp này, chúng tôi mong đợi kết quả đầu ra là: 0 (vì chúng tôi đăng tăng dần, ví dụ: đọc lần đầu, sau đó cập nhật)

Đây là một trong những lý do khiến thao tác không thể là nguyên tử, vì bạn cần đọc giá trị (và làm gì đó với nó) rồi cập nhật giá trị.

Lý do quan trọng khác là làm một cái gì đó nguyên tử thường mất nhiều thời gian hơn vì khóa. Sẽ là ngớ ngẩn nếu tất cả các phép toán trên nguyên thủy mất nhiều thời gian hơn một chút đối với những trường hợp hiếm hoi khi người ta muốn có các phép toán nguyên tử. Đó là lý do tại sao họ đã thêm AtomicIntegercác lớp nguyên tử khác vào ngôn ngữ.


2
Điều này gây hiểu lầm. Bạn phải tách riêng việc thực thi và nhận kết quả, nếu không, bạn không thể nhận giá trị từ bất kỳ hoạt động nguyên tử nào .
Sebastian Mach

Không nó không phải là, đó là lý do tại sao AtomicInteger Java có get (), getAndIncrement (), getAndDecrement (), incrementAndGet (), decrementAndGet () vv
Roy van Rijn

1
Và ngôn ngữ Java có thể đã được định nghĩa i++để được mở rộng i.getAndIncrement(). Việc mở rộng như vậy không phải là mới. Ví dụ: lambdas trong C ++ được mở rộng thành các định nghĩa lớp ẩn danh trong C ++.
Sebastian Mach

Với một nguyên tử, i++người ta có thể tạo ra một nguyên tử ++ihoặc ngược lại. Một là tương đương với khác cộng một.
David Schwartz

2

Có hai bước:

  1. lấy tôi từ bộ nhớ
  2. đặt i + 1 thành i

vì vậy nó không phải là hoạt động nguyên tử. Khi thread1 thực thi i ++ và thread2 thực thi i ++, giá trị cuối cùng của i có thể là i + 1.


-1

Concurrency ( Threadlớp và tương tự) là một tính năng được bổ sung trong v1.0 của Java . i++đã được thêm vào bản beta trước đó, và như vậy, nó vẫn có nhiều khả năng trong triển khai ban đầu (ít nhiều).

Việc đồng bộ hóa các biến tùy thuộc vào lập trình viên. Hãy xem hướng dẫn của Oracle về điều này .

Chỉnh sửa: Để làm rõ, i ++ là một thủ tục được định nghĩa rõ ràng có trước Java, và do đó, các nhà thiết kế của Java đã quyết định giữ nguyên chức năng ban đầu của thủ tục đó.

Toán tử ++ được định nghĩa trong B (1969) có trước java và phân luồng chỉ bằng một tad.


-1 "public class Thread ... Since: JDK1.0" Nguồn: docs.oracle.com/javase/7/docs/api/index.html?java/lang/…
Silly Freak

Phiên bản không quan trọng lắm vì thực tế là nó vẫn được triển khai trước lớp Thread và không bị thay đổi vì nó, nhưng tôi đã chỉnh sửa câu trả lời của mình để làm hài lòng bạn.
TheBat

5
Điều quan trọng là tuyên bố của bạn "nó vẫn được triển khai trước lớp Thread" không được hỗ trợ bởi các nguồn. i++không phải là nguyên tử là một quyết định thiết kế, không phải là một sự giám sát trong một hệ thống đang phát triển.
Silly Freak

Thật dễ thương. i ++ đã được định nghĩa rõ ràng trước Threads, đơn giản vì có những ngôn ngữ tồn tại trước Java. Những người tạo ra Java đã sử dụng các ngôn ngữ khác đó làm cơ sở thay vì xác định lại một quy trình được chấp nhận tốt. Tôi đã từng nói đó là một cuộc giám sát ở đâu?
TheBat

@SillyFreak Đây là một số nguồn cho biết ++ bao nhiêu tuổi: en.wikipedia.org/wiki/Increment_and_decrement_operators en.wikipedia.org/wiki/B_(programming_language)
TheBat
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.