Là! = Kiểm tra chủ đề an toàn?


140

Tôi biết rằng các hoạt động ghép như i++không phải là luồng an toàn vì chúng liên quan đến nhiều hoạt động.

Nhưng là kiểm tra tham chiếu với chính nó một hoạt động an toàn chủ đề?

a != a //is this thread-safe

Tôi đã cố gắng lập trình điều này và sử dụng nhiều chủ đề nhưng không thành công. Tôi đoán tôi không thể mô phỏng cuộc đua trên máy của mình.

BIÊN TẬP:

public class TestThreadSafety {
    private Object a = new Object();

    public static void main(String[] args) {

        final TestThreadSafety instance = new TestThreadSafety();

        Thread testingReferenceThread = new Thread(new Runnable() {

            @Override
            public void run() {
                long countOfIterations = 0L;
                while(true){
                    boolean flag = instance.a != instance.a;
                    if(flag)
                        System.out.println(countOfIterations + ":" + flag);

                    countOfIterations++;
                }
            }
        });

        Thread updatingReferenceThread = new Thread(new Runnable() {

            @Override
            public void run() {
                while(true){
                    instance.a = new Object();
                }
            }
        });

        testingReferenceThread.start();
        updatingReferenceThread.start();
    }

}

Đây là chương trình mà tôi đang sử dụng để kiểm tra độ an toàn của luồng.

Hành vi kỳ lạ

Khi chương trình của tôi bắt đầu giữa một số lần lặp, tôi nhận được giá trị cờ đầu ra, điều đó có nghĩa là !=kiểm tra tham chiếu không thành công trên cùng một tham chiếu. NHƯNG sau một số lần lặp, đầu ra trở thành giá trị không đổi falsevà sau đó thực thi chương trình trong một thời gian dài không tạo ra một trueđầu ra duy nhất .

Như đầu ra gợi ý sau một số lần lặp n (không cố định), đầu ra dường như là giá trị không đổi và không thay đổi.

Đầu ra:

Đối với một số lần lặp:

1494:true
1495:true
1496:true
19970:true
19972:true
19974:true
//after this there is not a single instance when the condition becomes true

2
Bạn có ý nghĩa gì bởi "chủ đề an toàn" trong bối cảnh này? Bạn đang hỏi nếu nó được đảm bảo để luôn luôn trả lại sai?
JB Nizet

@JBNizet có. Đó là những gì tôi đã nghĩ đến.
Narendra Pathai

5
Nó thậm chí không luôn trả về false trong ngữ cảnh đơn luồng. Nó có thể là một NaN ..
harold

4
Giải thích có thể có: mã được biên dịch đúng lúc và mã được biên dịch chỉ tải biến một lần. Điều này được mong đợi.
Marko Topolnik

3
In kết quả cá nhân là một cách kém để kiểm tra các cuộc đua. In (cả định dạng và ghi kết quả) tương đối tốn kém so với thử nghiệm của bạn (và đôi khi chương trình của bạn sẽ bị chặn khi ghi khi băng thông của kết nối đến thiết bị đầu cuối hoặc thiết bị đầu cuối chậm). Ngoài ra, IO thường chứa các mutexes của chính nó sẽ hoán vị thứ tự thực hiện của các luồng của bạn (lưu ý các dòng riêng lẻ của bạn 1234:truekhông bao giờ đập vỡ nhau ). Một bài kiểm tra cuộc đua cần một vòng lặp bên trong chặt chẽ hơn. In một bản tóm tắt ở cuối (như ai đó đã làm dưới đây với khung kiểm tra đơn vị).
Ben Jackson

Câu trả lời:


124

Trong trường hợp không đồng bộ mã này

Object a;

public boolean test() {
    return a != a;
}

có thể sản xuất true. Đây là mã byte chotest()

    ALOAD 0
    GETFIELD test/Test1.a : Ljava/lang/Object;
    ALOAD 0
    GETFIELD test/Test1.a : Ljava/lang/Object;
    IF_ACMPEQ L1
...

như chúng ta có thể thấy nó tải trường avào các vars cục bộ hai lần, đó là một hoạt động phi nguyên tử, nếu ađược thay đổi ở giữa bởi một so sánh luồng khác có thể tạo ra false.

Ngoài ra, vấn đề về khả năng hiển thị bộ nhớ có liên quan ở đây, không có gì đảm bảo rằng những thay đổi ađược thực hiện bởi một luồng khác sẽ hiển thị với luồng hiện tại.


22
Mặc dù bằng chứng mạnh mẽ, mã byte thực sự không phải là một bằng chứng. Nó cũng phải ở đâu đó trong JLS ...
Marko Topolnik

10
@Marko Tôi đồng ý với suy nghĩ của bạn, nhưng không nhất thiết là kết luận của bạn. Đối với tôi, mã byte ở trên là cách triển khai rõ ràng / chính tắc !=, liên quan đến việc tải LHS và RHS một cách riêng biệt. Và vì vậy, nếu JLS không đề cập bất cứ điều gì cụ thể về tối ưu hóa khi LHS và RHS giống hệt nhau về mặt cú pháp, thì quy tắc chung sẽ được áp dụng, có nghĩa là tải ahai lần.
Andrzej Doyle

20
Trên thực tế, giả sử rằng mã byte được tạo ra phù hợp với JLS, đó là một bằng chứng!
proskor

6
@Adrian: Thứ nhất: ngay cả khi giả định đó không hợp lệ, sự tồn tại của một trình biên dịch duy nhất mà nó có thể đánh giá là "true" là đủ để chứng minh rằng đôi khi nó có thể đánh giá là "true" (ngay cả khi thông số đó cấm nó - mà nó không). Thứ hai: Java được chỉ định rõ và hầu hết các trình biên dịch tuân thủ chặt chẽ với nó. Nó có ý nghĩa để sử dụng chúng như một tài liệu tham khảo trong khía cạnh này. Thứ ba: bạn sử dụng thuật ngữ "JRE", nhưng tôi không nghĩ nó có nghĩa như bạn nghĩ. . .
ruakh

2
@AlexanderTorstling - "Tôi không chắc liệu điều đó có đủ để ngăn chặn việc tối ưu hóa một lần đọc hay không." Nó không đủ Trên thực tế, trong trường hợp không có sự đồng bộ hóa (và các mối quan hệ "xảy ra trước" bổ sung), việc tối ưu hóa là hợp lệ,
Stephen C

47

Kiểm tra a != achủ đề có an toàn không?

Nếu acó khả năng có thể được cập nhật bởi một luồng khác (không có đồng bộ hóa phù hợp!), Thì Không.

Tôi đã cố gắng lập trình điều này và sử dụng nhiều chủ đề nhưng không thành công. Tôi đoán không thể mô phỏng cuộc đua trên máy của tôi.

Điều đó không có nghĩa gì cả! Vấn đề là nếu một thực thi ađược cập nhật bởi một luồng khác được JLS cho phép , thì mã không an toàn cho luồng. Việc bạn không thể khiến điều kiện cuộc đua xảy ra với một trường hợp thử nghiệm cụ thể trên một máy cụ thể và một triển khai Java cụ thể, không ngăn cản điều đó xảy ra trong các trường hợp khác.

Điều này có nghĩa là a! = A có thể trở lại true.

Vâng, trên lý thuyết, trong những trường hợp nhất định.

Ngoài ra, a != acó thể trở lại falsemặc dù ađã thay đổi đồng thời.


Liên quan đến "hành vi kỳ lạ":

Khi chương trình của tôi bắt đầu giữa một số lần lặp, tôi nhận được giá trị cờ đầu ra, điều đó có nghĩa là tham chiếu! = Kiểm tra không thành công trên cùng một tham chiếu. NHƯNG sau một số lần lặp, đầu ra trở thành giá trị không đổi và sau đó thực thi chương trình trong một thời gian dài không tạo ra một đầu ra đúng duy nhất.

Hành vi "kỳ lạ" này phù hợp với kịch bản thực hiện sau:

  1. Chương trình được tải và JVM bắt đầu diễn giải các mã byte. Vì (như chúng ta đã thấy từ đầu ra javap), mã byte thực hiện hai lần tải, đôi khi bạn (dường như) thấy kết quả của điều kiện cuộc đua.

  2. Sau một thời gian, mã được biên dịch bởi trình biên dịch JIT. Trình tối ưu hóa JIT thông báo rằng có hai tải của cùng một khe nhớ ( a) gần nhau và tối ưu hóa cái thứ hai đi. (Trên thực tế, có một cơ hội để nó tối ưu hóa hoàn toàn bài kiểm tra ...)

  3. Bây giờ điều kiện cuộc đua không còn biểu hiện, bởi vì không còn hai tải.

Lưu ý rằng tất cả đều phù hợp với những gì JLS cho phép triển khai Java để làm.


@kriss bình luận như vậy:

Điều này có vẻ như đây có thể là điều mà các lập trình viên C hoặc C ++ gọi là "Hành vi không xác định" (phụ thuộc vào việc thực hiện). Có vẻ như có thể có một vài UB trong java trong các trường hợp góc như thế này.

Mô hình bộ nhớ Java (được chỉ định trong JLS 17.4 ) chỉ định một tập các điều kiện tiên quyết, theo đó một luồng được đảm bảo để xem các giá trị bộ nhớ được ghi bởi một luồng khác. Nếu một luồng cố gắng đọc một biến được viết bởi một biến khác và các điều kiện tiên quyết đó không được thỏa mãn, thì có thể có một số thực thi có thể xảy ra ... một số trong đó có thể không chính xác (theo quan điểm của các yêu cầu của ứng dụng). Nói cách khác, tập hợp các hành vi có thể (nghĩa là tập hợp "các hành vi được định dạng tốt") được xác định, nhưng chúng ta không thể nói hành vi nào sẽ xảy ra.

Trình biên dịch được phép kết hợp và sắp xếp lại các tải và lưu (và thực hiện các thao tác khác) với điều kiện hiệu ứng cuối của mã là như nhau:

  • khi được thực hiện bởi một luồng đơn và
  • khi được thực thi bởi các luồng khác nhau đồng bộ hóa chính xác (theo Mô hình bộ nhớ).

Nhưng nếu mã không đồng bộ hóa đúng cách (và do đó, các mối quan hệ "xảy ra trước" không đủ ràng buộc tập hợp các thực thi được định dạng tốt), trình biên dịch được phép sắp xếp lại tải và lưu trữ theo cách cho kết quả "không chính xác". (Nhưng điều đó thực sự chỉ nói rằng chương trình không chính xác.)


Điều này có nghĩa là a != acó thể trở lại đúng?
proskor

Tôi có nghĩa là có thể trên máy của tôi, tôi không thể mô phỏng rằng đoạn mã trên không phải là luồng an toàn. Vì vậy, có thể có một lý do lý thuyết đằng sau nó.
Narendra Pathai

@NarendraPathai - Không có lý do lý thuyết tại sao bạn không thể chứng minh điều đó. Có thể có một lý do thực tế ... hoặc có thể bạn không gặp may mắn.
Stephen C

Vui lòng kiểm tra câu trả lời cập nhật của tôi với chương trình mà tôi đang sử dụng. Kiểm tra đôi khi trả về đúng nhưng dường như có một hành vi kỳ lạ trong đầu ra.
Narendra Pathai

1
@NarendraPathai - Xem giải thích của tôi.
Stephen C

27

Chứng minh bằng test-ng:

public class MyTest {

  private static Integer count=1;

  @Test(threadPoolSize = 1000, invocationCount=10000)
  public void test(){
    count = new Integer(new Random().nextInt());
    Assert.assertFalse(count != count);
  }

}

Tôi có 2 lần thất bại trong 10 000 lời mời. Vì vậy, KHÔNG , nó không phải là chủ đề an toàn


6
Bạn thậm chí không kiểm tra sự bình đẳng ... Random.nextInt()phần này là thừa. Bạn có thể đã thử nghiệm với new Object()chỉ là tốt.
Marko Topolnik

@MarkoTopolnik Vui lòng kiểm tra câu trả lời cập nhật của tôi với chương trình mà tôi đang sử dụng. Kiểm tra đôi khi trả về đúng nhưng dường như có một hành vi kỳ lạ trong đầu ra.
Narendra Pathai

1
Một lưu ý phụ, các đối tượng ngẫu nhiên thường có nghĩa là được sử dụng lại, không được tạo ra mỗi khi bạn cần một int mới.
Simon Forsberg

15

Không có nó không phải là. Để so sánh, Java VM phải đặt hai giá trị để so sánh trên ngăn xếp và chạy lệnh so sánh (cái nào phụ thuộc vào loại "a").

Máy ảo Java có thể:

  1. Đọc "a" hai lần, đặt từng cái lên ngăn xếp và sau đó so sánh kết quả
  2. Chỉ đọc "a" một lần, đặt nó lên ngăn xếp, sao chép nó (hướng dẫn "dup") và chạy so sánh
  3. Loại bỏ hoàn toàn biểu thức và thay thế nó bằng false

Trong trường hợp đầu tiên, một luồng khác có thể sửa đổi giá trị cho "a" giữa hai lần đọc.

Chiến lược nào được chọn phụ thuộc vào trình biên dịch Java và Java Runtime (đặc biệt là trình biên dịch JIT). Nó thậm chí có thể thay đổi trong thời gian chạy chương trình của bạn.

Nếu bạn muốn chắc chắn làm thế nào biến được truy cập, bạn phải tạo nó volatile(cái gọi là "hàng rào bộ nhớ một nửa") hoặc thêm một hàng rào bộ nhớ đầy đủ ( synchronized). Bạn cũng có thể sử dụng một số API cấp độ cao hơn (ví dụ AtomicIntegernhư được đề cập bởi Juned Ahasan).

Để biết chi tiết về an toàn của luồng, hãy đọc JSR 133 ( Mô hình bộ nhớ Java ).


Tuyên bố anhư volatilevẫn sẽ ngụ ý hai lần đọc khác biệt, với khả năng thay đổi ở giữa.
Holger

6

Tất cả đã được Stephen C. giải thích rõ, để giải trí, bạn có thể thử chạy cùng một mã với các tham số JVM sau:

-XX:InlineSmallCode=0

Điều này sẽ ngăn việc tối ưu hóa được thực hiện bởi JIT (nó thực hiện trên máy chủ hotspot 7) và bạn sẽ thấy truemãi mãi (tôi đã dừng ở mức 2.000.000 nhưng tôi cho rằng nó sẽ tiếp tục sau đó).

Để biết thông tin, bên dưới là mã JIT'ed. Thành thật mà nói, tôi không đọc lắp ráp đủ trôi chảy để biết thử nghiệm đã thực sự được thực hiện hay hai tải đến từ đâu. (dòng 26 là thử nghiệm flag = a != avà dòng 31 là dấu ngoặc kép của while(true)).

  # {method} 'run' '()V' in 'javaapplication27/TestThreadSafety$1'
  0x00000000027dcc80: int3   
  0x00000000027dcc81: data32 data32 nop WORD PTR [rax+rax*1+0x0]
  0x00000000027dcc8c: data32 data32 xchg ax,ax
  0x00000000027dcc90: mov    DWORD PTR [rsp-0x6000],eax
  0x00000000027dcc97: push   rbp
  0x00000000027dcc98: sub    rsp,0x40
  0x00000000027dcc9c: mov    rbx,QWORD PTR [rdx+0x8]
  0x00000000027dcca0: mov    rbp,QWORD PTR [rdx+0x18]
  0x00000000027dcca4: mov    rcx,rdx
  0x00000000027dcca7: movabs r10,0x6e1a7680
  0x00000000027dccb1: call   r10
  0x00000000027dccb4: test   rbp,rbp
  0x00000000027dccb7: je     0x00000000027dccdd
  0x00000000027dccb9: mov    r10d,DWORD PTR [rbp+0x8]
  0x00000000027dccbd: cmp    r10d,0xefc158f4    ;   {oop('javaapplication27/TestThreadSafety$1')}
  0x00000000027dccc4: jne    0x00000000027dccf1
  0x00000000027dccc6: test   rbp,rbp
  0x00000000027dccc9: je     0x00000000027dcce1
  0x00000000027dcccb: cmp    r12d,DWORD PTR [rbp+0xc]
  0x00000000027dcccf: je     0x00000000027dcce1  ;*goto
                                                ; - javaapplication27.TestThreadSafety$1::run@62 (line 31)
  0x00000000027dccd1: add    rbx,0x1            ; OopMap{rbp=Oop off=85}
                                                ;*goto
                                                ; - javaapplication27.TestThreadSafety$1::run@62 (line 31)
  0x00000000027dccd5: test   DWORD PTR [rip+0xfffffffffdb53325],eax        # 0x0000000000330000
                                                ;*goto
                                                ; - javaapplication27.TestThreadSafety$1::run@62 (line 31)
                                                ;   {poll}
  0x00000000027dccdb: jmp    0x00000000027dccd1
  0x00000000027dccdd: xor    ebp,ebp
  0x00000000027dccdf: jmp    0x00000000027dccc6
  0x00000000027dcce1: mov    edx,0xffffff86
  0x00000000027dcce6: mov    QWORD PTR [rsp+0x20],rbx
  0x00000000027dcceb: call   0x00000000027a90a0  ; OopMap{rbp=Oop off=112}
                                                ;*aload_0
                                                ; - javaapplication27.TestThreadSafety$1::run@2 (line 26)
                                                ;   {runtime_call}
  0x00000000027dccf0: int3   
  0x00000000027dccf1: mov    edx,0xffffffad
  0x00000000027dccf6: mov    QWORD PTR [rsp+0x20],rbx
  0x00000000027dccfb: call   0x00000000027a90a0  ; OopMap{rbp=Oop off=128}
                                                ;*aload_0
                                                ; - javaapplication27.TestThreadSafety$1::run@2 (line 26)
                                                ;   {runtime_call}
  0x00000000027dcd00: int3                      ;*aload_0
                                                ; - javaapplication27.TestThreadSafety$1::run@2 (line 26)
  0x00000000027dcd01: int3   

1
Đây là một ví dụ tốt về loại mã mà JVM sẽ thực sự tạo ra khi bạn có một vòng lặp vô hạn và mọi thứ ít nhiều có thể được đưa ra. "Vòng lặp" thực tế ở đây là ba hướng dẫn từ 0x27dccd1đến 0x27dccdf. Các jmptrong vòng lặp là vô điều kiện (kể từ khi vòng lặp là vô hạn). Hai hướng dẫn duy nhất khác trong vòng lặp là add rbc, 0x1- đang tăng dần countOfIterations(mặc dù thực tế là vòng lặp sẽ không bao giờ được thoát nên giá trị này sẽ không được đọc: có lẽ nó cần thiết trong trường hợp bạn xâm nhập vào trình gỡ lỗi), .. .
BeeOnRope

... và testhướng dẫn tìm kiếm kỳ lạ , thực sự chỉ dành cho truy cập bộ nhớ (lưu ý eaxthậm chí không bao giờ được đặt trong phương thức!): đó là một trang đặc biệt được đặt thành không thể đọc được khi JVM muốn kích hoạt tất cả các luồng để đạt được điểm an toàn, do đó, nó có thể thực hiện gc hoặc một số thao tác khác yêu cầu tất cả các luồng phải ở trạng thái đã biết.
BeeOnRope

Hơn nữa, JVM hoàn toàn đưa ra sự instance. a != instance.aso sánh ra khỏi vòng lặp và chỉ thực hiện nó một lần, trước khi vòng lặp được nhập! Nó biết rằng không cần phải tải lại instancehoặc avì chúng không được khai báo là không ổn định và không có mã nào khác có thể thay đổi chúng trên cùng một luồng, vì vậy nó chỉ giả sử chúng giống nhau trong toàn bộ vòng lặp, được bộ nhớ cho phép mô hình.
BeeOnRope

5

Không, a != akhông phải là chủ đề an toàn. Biểu thức này bao gồm ba phần: tải a, tải alại và thực hiện !=. Có thể cho một luồng khác để có được khóa nội tại trên acha mẹ và thay đổi giá trị aở giữa 2 hoạt động tải.

Một yếu tố khác là liệu ađịa phương. Nếu alà cục bộ thì không có luồng nào khác có quyền truy cập vào nó và do đó nên là luồng an toàn.

void method () {
    int a = 0;
    System.out.println(a != a);
}

cũng nên luôn luôn in false.

Tuyên bố anhư volatilesẽ không giải quyết vấn đề nếu astatichoặc ví dụ. Vấn đề không phải là các luồng có các giá trị khác nhau a, mà là một luồng tải ahai lần với các giá trị khác nhau. Nó thực sự có thể làm cho trường hợp ít an toàn hơn cho luồng .. Nếu akhông volatilethì acó thể được lưu vào bộ đệm và thay đổi trong một luồng khác sẽ không ảnh hưởng đến giá trị được lưu trong bộ nhớ cache.


Ví dụ của bạn synchronizedlà sai: để mã đó được đảm bảo để in false, tất cả các phương thức được đặt a cũng sẽ phải như synchronizedvậy.
ruakh

Tại sao vậy? Nếu phương thức được đồng bộ hóa, làm thế nào bất kỳ luồng nào khác có được khóa nội tại trên acha mẹ trong khi phương thức đang thực thi, cần thiết để đặt giá trị a.
DoubleMx2

1
Cơ sở của bạn là sai. Bạn có thể đặt trường của đối tượng mà không cần lấy khóa nội tại của nó. Java không yêu cầu một luồng để có được khóa nội tại của đối tượng trước khi thiết lập các trường của nó.
ruakh

3

Về hành vi kỳ lạ:

Do biến akhông được đánh dấu là volatile, nên tại một thời điểm nào đó, giá trị của nó có athể được lưu trong bộ đệm. Cả hai đều aa != aphiên bản lưu trữ và do đó luôn giống nhau (nghĩa flaglà luôn luôn như vậy false).


0

Ngay cả đọc đơn giản không phải là nguyên tử. Nếu ađược longvà không được đánh dấu như volatilesau đó trên các JVM 32-bit long b = akhông được thread-safe.


dễ bay hơi và nguyên tử không liên quan. ngay cả khi tôi đánh dấu một chất dễ bay hơi thì nó sẽ không phải là nguyên tử
Narendra Pathai 4/12/13

Việc gán một trường dài dễ bay hơi luôn luôn là nguyên tử. Các hoạt động khác như ++ thì không.
ZhekaKozlov
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.