Các biến tĩnh có được chia sẻ giữa các luồng không?


95

Giáo viên của tôi trong một lớp Java cấp cao hơn về phân luồng đã nói điều gì đó mà tôi không chắc chắn.

Ông nói rằng đoạn mã sau đây không nhất thiết phải cập nhật readybiến. Theo ông, hai luồng không nhất thiết phải chia sẻ biến tĩnh, cụ thể là trong trường hợp mỗi luồng (luồng chính so với ReaderThread) đang chạy trên bộ xử lý của chính nó và do đó không chia sẻ cùng một thanh ghi / bộ nhớ cache / v.v. và một CPU. sẽ không cập nhật cái khác.

Về cơ bản, anh ấy nói rằng có thể readyđược cập nhật trong chuỗi chính, nhưng KHÔNG phải trong ReaderThread, vì vậy điều đó ReaderThreadsẽ lặp lại vô hạn.

Ông cũng tuyên bố rằng chương trình có thể in 0hoặc 42. Tôi hiểu làm thế nào 42có thể được in, nhưng không 0. Ông đã đề cập đây sẽ là trường hợp khi numberbiến được đặt thành giá trị mặc định.

Tôi nghĩ rằng có lẽ nó không được đảm bảo rằng biến tĩnh được cập nhật giữa các luồng, nhưng điều này khiến tôi cảm thấy rất kỳ lạ đối với Java. Làm readybiến động có khắc phục được vấn đề này không?

Anh ấy đã hiển thị mã này:

public class NoVisibility {  
    private static boolean ready;  
    private static int number;  
    private static class ReaderThread extends Thread {   
        public void run() {  
            while (!ready)   Thread.yield();  
            System.out.println(number);  
        }  
    }  
    public static void main(String[] args) {  
        new ReaderThread().start();  
        number = 42;  
        ready = true;  
    }  
}

Khả năng hiển thị của các biến không cục bộ không phụ thuộc vào việc chúng là biến tĩnh, trường đối tượng hay phần tử mảng, tất cả chúng đều có những cân nhắc giống nhau. (Với vấn đề là các phần tử mảng không thể biến động được.)
Paŭlo Ebermann

1
hỏi giáo viên của bạn loại kiến ​​trúc nào mà ông ấy cho là có thể nhìn thấy '0'. Tuy nhiên, về lý thuyết thì anh ấy đúng.
bestsss

4
@bestsss Đặt câu hỏi kiểu đó sẽ tiết lộ cho giáo viên rằng anh ta đã bỏ sót toàn bộ ý anh ta đang nói. Vấn đề là các lập trình viên có năng lực hiểu những gì được đảm bảo và những gì không và không dựa vào những thứ không được đảm bảo, ít nhất là không hiểu chính xác những gì không được đảm bảo và tại sao.
David Schwartz

Chúng được chia sẻ giữa mọi thứ được tải bởi cùng một trình tải lớp. Bao gồm các chủ đề.
Marquis of Lorne,

Giáo viên của bạn (và câu trả lời được chấp nhận) đúng 100%, nhưng tôi sẽ đề cập rằng điều đó hiếm khi xảy ra - đây là loại vấn đề sẽ che giấu trong nhiều năm và chỉ hiển thị khi nó có hại nhất. Ngay cả những bài kiểm tra ngắn cố gắng phơi bày vấn đề cũng có xu hướng hoạt động như thể mọi thứ đều ổn (Có thể là do họ không có thời gian để JVM thực hiện nhiều tối ưu hóa), vì vậy đó là một vấn đề thực sự tốt cần lưu ý.
Bill K

Câu trả lời:


75

Không có gì đặc biệt về các biến tĩnh khi nói đến khả năng hiển thị. Nếu chúng có thể truy cập được thì bất kỳ chuỗi nào cũng có thể truy cập chúng, vì vậy bạn có nhiều khả năng gặp các vấn đề đồng thời hơn vì chúng dễ bị lộ hơn.

Có một vấn đề về khả năng hiển thị do mô hình bộ nhớ của JVM áp đặt. Đây là một bài báo nói về mô hình bộ nhớ và cách ghi hiển thị đối với các luồng . Bạn không thể tin tưởng vào những thay đổi mà một chuỗi làm cho các chuỗi khác hiển thị kịp thời (thực tế JVM không có nghĩa vụ phải hiển thị những thay đổi đó với bạn, trong bất kỳ khung thời gian nào), trừ khi bạn thiết lập mối quan hệ xảy ra trước đó .

Đây là trích dẫn từ liên kết đó (được cung cấp trong nhận xét của Jed Wesley-Smith):

Chương 17 của Đặc tả ngôn ngữ Java định nghĩa quan hệ xảy ra trước trên các hoạt động bộ nhớ như đọc và ghi các biến được chia sẻ. Kết quả của việc ghi bởi một luồng được đảm bảo chỉ hiển thị cho một luồng khác đọc nếu thao tác ghi xảy ra trước khi thao tác đọc. Các cấu trúc đồng bộ và dễ bay hơi, cũng như các phương thức Thread.start () và Thread.join (), có thể hình thành các mối quan hệ xảy ra trước. Đặc biệt:

  • Mỗi hành động trong một chuỗi xảy ra trước mọi hành động trong chuỗi đó sau đó theo thứ tự của chương trình.

  • Mở khóa (khối đồng bộ hoặc lối ra phương thức) của một màn hình xảy ra trước mỗi lần khóa tiếp theo (khối đồng bộ hoặc lối vào phương thức) của cùng một màn hình đó. Và bởi vì quan hệ xảy ra trước khi có tính bắc cầu, tất cả các hành động của một luồng trước khi mở khóa xảy ra trước tất cả các hành động tiếp theo bất kỳ luồng nào khóa màn hình đó.

  • Việc ghi vào một trường dễ bay hơi xảy ra trước mỗi lần đọc tiếp theo của cùng trường đó. Việc ghi và đọc các trường dễ bay hơi có tác dụng nhất quán bộ nhớ tương tự như vào và ra khỏi màn hình, nhưng không đòi hỏi khóa loại trừ lẫn nhau.

  • Lời gọi bắt đầu trên một chuỗi xảy ra trước bất kỳ hành động nào trong chuỗi bắt đầu.

  • Tất cả các hành động trong một chuỗi xảy ra trước khi bất kỳ chuỗi nào khác trả về thành công từ một phép nối trên chuỗi đó.


3
Trong thực tế, "kịp thời" và "luôn luôn" đồng nghĩa với nhau. Rất có thể đoạn mã trên không bao giờ kết thúc.
TREE

4
Ngoài ra, điều này thể hiện một kiểu chống đối khác. Không sử dụng dễ bay hơi để bảo vệ nhiều hơn một phần của trạng thái được chia sẻ. Ở đây, số và sẵn sàng là hai phần trạng thái và để cập nhật / đọc cả hai một cách nhất quán, bạn cần đồng bộ hóa thực tế.
TREE

5
Phần về cuối cùng trở nên hiển thị là sai. Nếu không có bất kỳ rõ ràng xảy ra-trước khi mối quan hệ không có đảm bảo rằng bất kỳ ghi sẽ bao giờ được nhìn thấy bởi thread khác, như JIT là khá trong các quyền của mình để thêm vào đọc vào một thanh ghi và sau đó bạn sẽ không bao giờ nhìn thấy bất kỳ bản cập nhật. Bất kỳ tải cuối cùng nào là may mắn và không nên dựa vào.
Jed Wesley-Smith

2
"trừ khi bạn sử dụng từ khóa dễ bay hơi hoặc đồng bộ hóa." nên đọc "trừ khi có mối quan hệ xảy ra trước khi có liên quan giữa người viết và người đọc" và liên kết này: download.oracle.com/javase/6/docs/api/java/util/concurrent/…
Jed Wesley-Smith

2
@bestsss cũng phát hiện ra. Thật không may, ThreadGroup bị hỏng theo nhiều cách.
Jed Wesley-Smith

37

Anh ấy đang nói về khả năng hiển thị và không được hiểu theo nghĩa đen.

Các biến static thực sự được chia sẻ giữa các luồng, nhưng những thay đổi được thực hiện trong một luồng có thể không hiển thị với luồng khác ngay lập tức, làm cho nó có vẻ như có hai bản sao của biến.

Bài viết này trình bày một quan điểm phù hợp với cách anh ấy trình bày thông tin:

Đầu tiên, bạn phải hiểu một chút về mô hình bộ nhớ Java. Tôi đã đấu tranh một chút trong nhiều năm để giải thích nó một cách ngắn gọn và hay. Cho đến hôm nay, cách tốt nhất tôi có thể nghĩ ra để mô tả nó là nếu bạn hình dung nó theo cách này:

  • Mỗi luồng trong Java diễn ra trong một không gian bộ nhớ riêng biệt (điều này rõ ràng là không đúng sự thật, vì vậy hãy nhớ với tôi về điều này).

  • Bạn cần sử dụng các cơ chế đặc biệt để đảm bảo rằng giao tiếp diễn ra giữa các luồng này, như cách bạn làm trên hệ thống chuyển thư.

  • Việc ghi bộ nhớ xảy ra trong một luồng có thể "lọt qua" và bị một luồng khác nhìn thấy, nhưng điều này không có nghĩa là được đảm bảo. Nếu không có thông tin liên lạc rõ ràng, bạn không thể đảm bảo rằng các bài viết sẽ được các chủ đề khác nhìn thấy hoặc thậm chí là thứ tự mà chúng được nhìn thấy.

...

mô hình sợi

Nhưng một lần nữa, đây chỉ đơn giản là một mô hình tinh thần để suy nghĩ về phân luồng và dễ bay hơi, không phải theo nghĩa đen về cách hoạt động của JVM.


12

Về cơ bản thì đúng, nhưng thực ra vấn đề phức tạp hơn. Khả năng hiển thị của dữ liệu được chia sẻ có thể bị ảnh hưởng không chỉ bởi bộ nhớ đệm của CPU mà còn do việc thực hiện các lệnh không theo thứ tự.

Do đó, Java định nghĩa Mô hình bộ nhớ , mô hình đó cho biết các luồng có thể thấy trạng thái nhất quán của dữ liệu được chia sẻ trong trường hợp nào.

Trong trường hợp cụ thể của bạn, thêm volatileđảm bảo khả năng hiển thị.


8

Tất nhiên, chúng được "chia sẻ" theo nghĩa là cả hai đều tham chiếu đến cùng một biến, nhưng chúng không nhất thiết phải xem các cập nhật của nhau. Điều này đúng với bất kỳ biến nào, không chỉ static.

Và về lý thuyết, các lần ghi được thực hiện bởi một luồng khác có thể có thứ tự khác, trừ khi các biến được khai báo volatilehoặc các lần ghi được đồng bộ rõ ràng.


4

Trong một trình nạp lớp duy nhất, các trường tĩnh luôn được chia sẻ. Để xác định phạm vi dữ liệu một cách rõ ràng cho các chuỗi, bạn muốn sử dụng một phương tiện như ThreadLocal.


2

Khi bạn khởi tạo biến kiểu nguyên thủy tĩnh, mặc định java sẽ gán một giá trị cho các biến tĩnh

public static int i ;

khi bạn xác định biến như thế này, giá trị mặc định của i = 0; đó là lý do tại sao có khả năng bạn nhận được 0. sau đó luồng chính cập nhật giá trị của boolean sẵn sàng thành true. vì sẵn sàng là một biến tĩnh, luồng chính và luồng khác tham chiếu đến cùng một địa chỉ bộ nhớ nên biến sẵn sàng thay đổi. vì vậy luồng thứ cấp thoát ra khỏi vòng lặp while và giá trị in. khi in giá trị khởi tạo giá trị của number là 0. nếu quá trình luồng đã qua vòng lặp while trước biến số cập nhật luồng chính. thì có khả năng in 0


-2

@dontocsata bạn có thể quay lại với giáo viên của bạn và trường học của anh ấy một chút :)

vài ghi chú từ thế giới thực và bất kể những gì bạn nhìn thấy hoặc được cho biết. Xin LƯU Ý, các từ dưới đây liên quan đến trường hợp cụ thể này theo thứ tự chính xác được hiển thị.

2 biến sau sẽ nằm trên cùng một dòng bộ nhớ cache dưới hầu như bất kỳ kiến ​​trúc nào đã biết.

private static boolean ready;  
private static int number;  

Thread.exit(luồng chính) được đảm bảo thoát và exitđược đảm bảo gây ra hàng rào bộ nhớ, do việc loại bỏ luồng nhóm luồng (và nhiều vấn đề khác). (đó là một cuộc gọi được đồng bộ hóa và tôi thấy không có cách nào được thực hiện với phần đồng bộ vì ThreadGroup cũng phải kết thúc nếu không còn luồng daemon nào, v.v.).

Chuỗi bắt đầu ReaderThreadsẽ giữ cho quy trình tồn tại vì nó không phải là một trình nền! Do đó readynumbersẽ được xóa cùng nhau (hoặc số trước đó nếu xảy ra chuyển đổi ngữ cảnh) và không có lý do thực sự để sắp xếp lại trong trường hợp này, ít nhất tôi thậm chí không thể nghĩ ra một. Bạn sẽ cần một cái gì đó thực sự kỳ lạ để xem bất cứ điều gì nhưng 42. Một lần nữa, tôi cho rằng cả hai biến tĩnh sẽ nằm trong cùng một dòng bộ nhớ cache. Tôi chỉ không thể tưởng tượng một dòng bộ nhớ cache dài 4 byte HOẶC một JVM sẽ không gán chúng trong một vùng liên tục (dòng bộ nhớ cache).


3
@bestsss mặc dù điều đó hoàn toàn đúng ngày nay, nhưng nó dựa trên việc triển khai JVM hiện tại và kiến ​​trúc phần cứng để xác định sự thật của nó, chứ không phải dựa trên ngữ nghĩa của chương trình. Điều này có nghĩa là chương trình vẫn bị hỏng, mặc dù nó có thể hoạt động. Có thể dễ dàng tìm thấy một biến thể nhỏ của ví dụ này thực sự không thành công theo những cách được chỉ định.
Jed Wesley-Smith

1
Tôi đã nói rằng nó không tuân theo thông số kỹ thuật, tuy nhiên với tư cách là một giáo viên, ít nhất hãy tìm một ví dụ phù hợp thực sự có thể thất bại trên một số kiến ​​trúc hàng hóa, vì vậy ví dụ này là có thật.
bestsss

6
Có thể là lời khuyên tồi tệ nhất về việc viết mã an toàn theo luồng mà tôi đã thấy.
Lawrence Dol

4
@Bestsss: Câu trả lời đơn giản cho câu hỏi của bạn chỉ là: "Mã cho đặc điểm kỹ thuật và tài liệu, không phải cho các tác dụng phụ của hệ thống hoặc triển khai cụ thể của bạn". Điều này đặc biệt quan trọng trong một nền tảng máy ảo được thiết kế để không có phần cứng bên dưới.
Lawrence Dol,

1
@Bestsss: Điểm của giáo viên là (a) mã có thể hoạt động tốt khi bạn kiểm tra nó, và (b) mã bị hỏng vì nó phụ thuộc vào hoạt động phần cứng chứ không phải sự đảm bảo của thông số kỹ thuật. Vấn đề là nó có vẻ ổn, nhưng nó không ổn.
Lawrence Dol,
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.