11

Tôi thấy mã này khá thường xuyên trong một số bài kiểm tra đơn vị OSS, nhưng nó có an toàn không? Vòng lặp while có được đảm bảo để xem giá trị đúng của invoc không?Đọc không đồng bộ của chuỗi số nguyên trong java?

Nếu không; nerd điểm cho bất cứ ai cũng biết kiến ​​trúc CPU này có thể thất bại trên.

private int invoc = 0; 

    private synchronized void increment() { 
    invoc++; 
    } 

    public void isItThreadSafe() throws InterruptedException { 
     for (int i = 0; i < TOTAL_THREADS; i++) { 
     new Thread(new Runnable() { 
      public void run() { 
      // do some stuff 
      increment(); 
      } 
     }).start(); 
     } 
     while (invoc != TOTAL_THREADS) { 
     Thread.sleep(250); 
     } 
    } 
+0

Không thể nói 100%, nhưng có vẻ như không đọc đồng bộ của trường không bay hơi không được đảm bảo để đọc giá trị gần đây nhất của 'invoc' (mặc dù ghi * được * đỏ lên do từ khóa 'synchronised' trên phương thức' increment() '.) Không biết mô hình bộ nhớ Java khá tốt, đủ để nói chắc chắn. – dlev

+0

@dlev Đó là, loại, câu hỏi;) – krosenvold

+0

Tôi sắp trả lời, "Nó luôn làm việc cho tôi!". Nhưng cũng tìm thấy http://stackoverflow.com/questions/1006655/are-java-primitive-ints-atomic-by-design-or-by-accident có nội dung "Có" cho ints và "Có thể" trong thời gian dài và tăng gấp đôi . –

Trả lời

17

Không, không an toàn. invoc cần phải được khai báo dễ bay hơi hoặc truy cập trong khi đồng bộ hóa trên cùng một khóa hoặc thay đổi để sử dụng AtomicInteger. Chỉ cần sử dụng phương thức đồng bộ để tăng invoc, nhưng không đồng bộ hóa để đọc nó, không đủ tốt.

JVM thực hiện rất nhiều tối ưu hóa, bao gồm bộ nhớ đệm và hướng dẫn sắp xếp lại CPU cụ thể. Nó sử dụng các từ khóa dễ bay hơi và khóa để quyết định khi nào nó có thể tối ưu hóa tự do và khi nó phải có một giá trị cập nhật có sẵn cho các chủ đề khác để đọc. Vì vậy, khi người đọc không sử dụng khóa, JVM không thể không cho nó một giá trị cũ.

quote này từ Java Concurrency in Practice (phần 3.1.3) thảo luận cách cả viết và đọc cần phải được đồng bộ:

khóa Intrinsic có thể được sử dụng để đảm bảo rằng một thread thấy những ảnh hưởng của một cách khác có thể dự đoán được, như minh họa trong hình 3.1. Khi luồng A thực hiện một khối đồng bộ, và sau đó luồng B đi vào một khối đồng bộ được bảo vệ bởi cùng một khóa, các giá trị của các biến được hiển thị cho A trước khi nhả khóa được đảm bảo hiển thị cho B khi lấy khóa. Nói cách khác, mọi thứ A đã làm trong hoặc trước một khối đồng bộ được hiển thị cho B khi nó thực hiện một khối đồng bộ được bảo vệ bởi cùng một khóa. Nếu không có đồng bộ hóa, không có sự bảo đảm như vậy.

Phần tiếp theo (3.1.4) bao gồm sử dụng không ổn định:

Ngôn ngữ Java cũng cung cấp một sự thay thế, hình thức yếu của đồng bộ hóa, biến không ổn định, để đảm bảo rằng thông tin cập nhật cho một biến được tuyên truyền dự đoán cho các chủ đề khác. Khi một trường được khai báo dễ bay hơi, trình biên dịch và thời gian chạy được lưu ý rằng biến này được chia sẻ và các hoạt động trên nó không nên được sắp xếp lại với các hoạt động bộ nhớ khác. Biến dễ bay hơi không được lưu trữ trong sổ đăng ký hoặc trong bộ nhớ cache nơi chúng bị ẩn khỏi các bộ xử lý khác, do đó, đọc một biến dễ bay hơi luôn trả về ghi mới nhất theo bất kỳ chuỗi nào.

Quay lại khi tất cả chúng ta có máy CPU đơn trên máy tính để bàn, chúng tôi sẽ viết mã và không bao giờ gặp sự cố cho đến khi nó chạy trên hộp đa bộ xử lý, thường trong quá trình sản xuất. Một số yếu tố làm phát sinh các vấn đề về khả năng hiển thị, những thứ như bộ nhớ cache của CPU-local và sắp xếp lại lệnh, là những thứ bạn mong đợi từ bất kỳ máy đa xử lý nào. Tuy nhiên, việc loại bỏ các hướng dẫn dường như không cần thiết có thể xảy ra với bất kỳ máy nào. Không có gì buộc JVM phải làm cho người đọc thấy giá trị cập nhật của biến, bạn đang ở lòng thương xót của những người triển khai JVM. Vì vậy, có vẻ như với tôi mã này sẽ không được một đặt cược tốt cho bất kỳ kiến ​​trúc CPU.

2

Vâng!

private volatile int invoc = 0; 

Sẽ thực hiện thủ thuật.

Và xem Are java primitive ints atomic by design or by accident? trang web nào có một số định nghĩa java có liên quan. Rõ ràng int là tốt, nhưng đôi dài & dài có thể không được.


chỉnh sửa, bổ trợ. Câu hỏi đặt ra, "xem giá trị đúng của invoc?". "Giá trị chính xác" là gì? Như trong khoảng thời gian liên tục, tính đồng thời không thực sự tồn tại giữa các luồng. Một trong các bài viết trên ghi chú rằng giá trị cuối cùng sẽ bị xóa và chuỗi khác sẽ nhận được nó. Mã "thread safe" có phải là mã không? Tôi sẽ nói "có", bởi vì nó sẽ không "misbehave" dựa trên những bất thường của trình tự, trong trường hợp này.

+1

Tôi không chắc chắn bất kỳ trích dẫn nào trong câu hỏi được đề cập trên thực tế bao gồm khả năng hiển thị cho một chủ đề khác nhau – krosenvold

+1

Không nhận siêu hình Tôi nghĩ có cơ hội lý thuyết vòng ngoài sẽ không bao giờ thấy bất cứ điều gì trừ 0. – krosenvold

0

Theo như tôi hiểu mã, nó phải an toàn. Bytecode có thể được sắp xếp lại, có. Nhưng cuối cùng invoc sẽ được đồng bộ với thread chính một lần nữa. Đồng bộ hóa đảm bảo rằng invoc được tăng lên một cách chính xác để có một đại diện nhất quán của invoc trong một số thanh ghi. Tại một thời điểm nào đó giá trị này sẽ bị xóa và thử nghiệm nhỏ thành công.

Nó chắc chắn không tốt đẹp và tôi sẽ đi với câu trả lời tôi đã bỏ phiếu cho sẽ sửa mã như thế này vì nó có mùi. Nhưng suy nghĩ về nó, tôi sẽ xem xét nó an toàn.

+0

Đó là nhiều hơn về khả năng hiển thị, ít sắp xếp lại. VM ** có thể ** tối ưu hóa bộ nhớ đọc đi hoàn toàn và xử lý _invoc_ như một hằng số trong chuỗi nơi truy cập không được đồng bộ hóa/dễ bay hơi. Vì vậy, điều này đã được sửa chữa, mặc dù nó có thể làm việc đôi khi hoặc thậm chí thường xuyên. – Boris

+0

Tôi biết tôi nên tin tưởng cảm giác ruột của mình nhiều hơn :-) –

1

Về mặt lý thuyết, có thể đọc được lưu vào bộ nhớ cache. Không có gì trong mô hình bộ nhớ Java ngăn chặn điều đó.

Thực tế, điều đó rất khó xảy ra (trong ví dụ cụ thể của bạn). Câu hỏi đặt ra là, liệu JVM có thể tối ưu hóa qua một cuộc gọi phương thức hay không.

read #1 
method(); 
read #2 

Đối với JVM để lý do mà đọC# 2 có thể tái sử dụng các kết quả của đọC# 1 (có thể được lưu trữ trong một thanh ghi CPU), nó phải biết chắc chắn rằng method() không chứa các hành động đồng bộ hóa. Điều này nói chung là không thể - trừ khi, method() được gạch chân và JVM có thể nhìn thấy từ mã flatted mà không có đồng bộ/biến động hoặc các hành động đồng bộ hóa khác giữa đọC# 1 và đọC# 2; sau đó nó có thể loại bỏ một cách an toàn đọC# 2.

Bây giờ trong ví dụ của bạn, phương pháp là Thread.sleep(). Một cách để thực hiện nó là bận vòng lặp cho một số lần, tùy thuộc vào tần số CPU. Sau đó, JVM có thể nội tuyến nó, và sau đó loại bỏ đọC# 2.

Nhưng tất nhiên việc triển khai thực hiện sleep() là không thực tế. Nó thường được thực hiện như một phương thức native gọi là hạt nhân OS. Câu hỏi đặt ra là, JVM có thể tối ưu hóa trên phương thức gốc như vậy không.

Ngay cả khi JVM có kiến ​​thức về hoạt động bên trong của một số phương pháp gốc, do đó có thể tối ưu hóa trên chúng, không thể xảy ra là sleep() được xử lý theo cách đó. sleep(1ms) mất hàng triệu chu kỳ CPU để quay trở lại, thực sự không có điểm tối ưu hóa xung quanh nó để tiết kiệm một vài lần đọc.

-

Thảo luận này cho thấy vấn đề lớn nhất của cuộc đua dữ liệu - phải mất quá nhiều công sức để giải thích. Một chương trình không nhất thiết là sai, nếu nó không được "đồng bộ một cách chính xác", tuy nhiên để chứng minh nó không sai không phải là một nhiệm vụ dễ dàng. Cuộc sống đơn giản hơn nhiều, nếu một chương trình được đồng bộ chính xác và không chứa dữ liệu nào.

+0

Điều kỳ lạ là tôi gần như chắc chắn tôi đã thấy trường hợp chính xác này xảy ra, theo lý thuyết hay không. – krosenvold

0

Nếu bạn không bắt buộc phải sử dụng "int", tôi sẽ đề xuất AtomicInteger làm giải pháp thay thế an toàn cho chủ đề.

Các vấn đề liên quan