Hàng rào bộ nhớ được sử dụng trong Java là gì?


18

Trong khi cố gắng hiểu cách SubmissionPublisher( mã nguồn trong Java SE 10, OpenJDK | docs ), một lớp mới được thêm vào Java SE trong phiên bản 9, đã được triển khai, tôi tình cờ thấy một vài lệnh gọi API VarHandlemà trước đây tôi không biết:

fullFence, acquireFence, releaseFence, loadLoadFencestoreStoreFence.

Sau khi thực hiện một số nghiên cứu, đặc biệt là về khái niệm hàng rào / hàng rào bộ nhớ (tôi đã nghe nói về chúng trước đây, vâng, nhưng chưa bao giờ sử dụng chúng, do đó khá lạ lẫm với ngữ nghĩa của chúng), tôi nghĩ rằng tôi có hiểu biết cơ bản về những gì chúng dành cho . Tuy nhiên, vì câu hỏi của tôi có thể nảy sinh từ một quan niệm sai lầm, tôi muốn đảm bảo rằng tôi đã hiểu đúng ngay từ đầu:

  1. Rào cản bộ nhớ đang sắp xếp lại các ràng buộc liên quan đến hoạt động đọc và viết.

  2. Rào cản bộ nhớ có thể được phân loại thành hai loại chính: rào cản bộ nhớ hai chiều và hai chiều, tùy thuộc vào việc chúng có đặt các ràng buộc đối với việc đọc hoặc ghi hoặc cả hai.

  3. C ++ hỗ trợ một loạt các rào cản bộ nhớ , tuy nhiên, những rào cản này không phù hợp với những rào cản được cung cấp bởi VarHandle. Tuy nhiên, một số trong những rào cản bộ nhớ còn trống trong VarHandlecung cấp các hiệu ứng đặt hàng đó là tương thích với các rào cản C ++ bộ nhớ tương ứng của họ.

    • #fullFence tương thích với atomic_thread_fence(memory_order_seq_cst)
    • #acquireFence tương thích với atomic_thread_fence(memory_order_acquire)
    • #releaseFence tương thích với atomic_thread_fence(memory_order_release)
    • #loadLoadFence#storeStoreFencekhông có phần truy cập C ++ tương thích

Từ tương thích dường như thực sự quan trọng ở đây vì ngữ nghĩa rõ ràng khác nhau khi nói đến các chi tiết. Chẳng hạn, tất cả các rào cản C ++ là hai chiều, trong khi các rào cản của Java không (nhất thiết).

  1. Hầu hết các rào cản bộ nhớ cũng có hiệu ứng đồng bộ hóa. Chúng đặc biệt phụ thuộc vào loại rào cản được sử dụng và hướng dẫn rào cản được thực hiện trước đó trong các luồng khác. Vì hàm ý đầy đủ mà một hướng dẫn về rào cản là dành riêng cho phần cứng, tôi sẽ gắn bó với các rào cản cấp cao hơn (C ++). Ví dụ, trong C ++, các thay đổi được thực hiện trước một lệnh rào cản phát hành được hiển thị cho một luồng thực hiện lệnh thu nhận rào cản.

Những giả định của tôi có đúng không? Nếu vậy, câu hỏi kết quả của tôi là:

  1. Do các rào cản bộ nhớ có sẵn trong VarHandlebất kỳ loại đồng bộ hóa bộ nhớ?

  2. Bất kể chúng có gây ra sự đồng bộ hóa bộ nhớ hay không, những ràng buộc sắp xếp lại có thể hữu ích cho Java? Mô hình bộ nhớ Java đã đưa ra một số đảm bảo rất mạnh về việc đặt hàng khi có các trường, khóa hoặc VarHandlehoạt động dễ bay hơi như #compareAndSetcó liên quan.

Trong trường hợp bạn đang tìm kiếm một ví dụ: Đã nói ở trên BufferedSubscription, một lớp bên trong SubmissionPublisher(nguồn được liên kết ở trên), đã thiết lập một hàng rào đầy đủ trong dòng 1079 (chức năng growAndAdd; vì trang web được liên kết không hỗ trợ định danh phân đoạn, chỉ cần CTRL + F cho nó ). Tuy nhiên, nó không rõ ràng cho tôi nó là gì cho.


1
Tôi đã cố gắng trả lời, nhưng nói một cách đơn giản, chúng tồn tại bởi vì mọi người muốn có một chế độ yếu hơn những gì Java có. Theo thứ tự tăng dần, chúng sẽ là : plain -> opaque -> release/acquire -> volatile (sequential consistency).
Eugene

Câu trả lời:


11

Điều này chủ yếu là không trả lời, thực sự (ban đầu muốn đưa ra nhận xét, nhưng như bạn có thể thấy, nó quá dài). Chỉ là tôi đã tự hỏi điều này rất nhiều, đã đọc và nghiên cứu rất nhiều và tại thời điểm này tôi có thể nói một cách an toàn: điều này thật phức tạp. Tôi thậm chí đã viết nhiều bài kiểm tra với jcstress để tìm ra cách chúng thực sự hoạt động (trong khi nhìn vào mã lắp ráp được tạo ra) và trong khi một số trong số chúng có ý nghĩa nào đó , nói chung, chủ đề này không có nghĩa là dễ dàng.

Điều đầu tiên bạn cần hiểu:

Đặc tả ngôn ngữ Java (JLS) không đề cập đến các rào cản , ở bất cứ đâu. Điều này, đối với java, sẽ là một chi tiết triển khai: nó thực sự hoạt động về mặt xảy ra trước ngữ nghĩa. Để có thể chỉ định chính xác những điều này theo JMM (Mô hình bộ nhớ Java), JMM sẽ phải thay đổi khá nhiều .

Đây là công việc đang tiến triển.

Thứ hai, nếu bạn thực sự muốn làm trầy xước bề mặt ở đây, đây là điều đầu tiên cần xem . Cuộc nói chuyện thật khó tin. Phần yêu thích của tôi là khi Herb Sutter giơ 5 ngón tay lên và nói: "Đây là cách nhiều người có thể làm việc thực sự và chính xác với những thứ này." Điều đó sẽ cho bạn một gợi ý về sự phức tạp liên quan. Tuy nhiên, có một số ví dụ nhỏ dễ nắm bắt (như bộ đếm được cập nhật bởi nhiều luồng không quan tâm đến các đảm bảo bộ nhớ khác , nhưng chỉ quan tâm rằng chính nó được tăng lên một cách chính xác).

Một ví dụ khác là khi (trong java) bạn muốn một volatilecờ để điều khiển các luồng dừng / bắt đầu. Bạn biết đấy, cổ điển:

volatile boolean stop = false; // on thread writes, one thread reads this    

Nếu bạn làm việc với java, bạn sẽ biết rằng nếu không có volatile mã này bị hỏng (bạn có thể đọc lý do tại sao khóa kiểm tra kép bị hỏng mà không có nó chẳng hạn). Nhưng bạn có biết rằng đối với một số người viết mã hiệu suất cao thì điều này là quá nhiều? volatileđọc / ghi cũng đảm bảo tính nhất quán tuần tự - có một số đảm bảo mạnh mẽ và một số người muốn có phiên bản yếu hơn này.

Một chủ đề an toàn cờ, nhưng không dễ bay hơi? Vâng, chính xác : VarHandle::set/getOpaque.

Và bạn sẽ hỏi tại sao ai đó có thể cần điều đó chẳng hạn? Không phải ai cũng quan tâm đến tất cả những thay đổi được hỗ trợ bởi a volatile.

Chúng ta hãy xem làm thế nào chúng ta sẽ đạt được điều này trong java. Trước hết, những điều kỳ lạ như vậy đã tồn tại trong API : AtomicInteger::lazySet. Điều này không được chỉ định trong Mô hình bộ nhớ Java và không có định nghĩa rõ ràng ; mọi người vẫn sử dụng nó (LMAX, afaik hoặc cái này để đọc thêm ). IMHO, AtomicInteger::lazySetVarHandle::releaseFence(hoặc VarHandle::storeStoreFence).


Hãy thử trả lời tại sao ai đó cần những thứ này ?

JMM về cơ bản có hai cách để truy cập vào một trường: đơn giảndễ bay hơi (đảm bảo tính nhất quán tuần tự ). Tất cả những phương pháp mà bạn đề cập đều có để mang lại một cái gì đó ở giữa hai thứ này - phát hành / thu nhận ngữ nghĩa ; Có những trường hợp, tôi đoán, nơi mọi người thực sự cần điều này.

Một thư giãn thậm chí nhiều hơn từ phát hành / mua lại sẽ mờ đục , mà tôi vẫn đang cố gắng để hiểu đầy đủ .


Do đó, điểm mấu chốt (sự hiểu biết của bạn là khá chính xác, btw): nếu bạn có kế hoạch sử dụng điều này trong java - họ không có thông số kỹ thuật tại thời điểm này, hãy tự chịu rủi ro. Nếu bạn muốn hiểu chúng, các chế độ tương đương C ++ của chúng là nơi để bắt đầu.


1
Đừng cố gắng tìm ra ý nghĩa của lazySetbằng cách liên kết với các câu trả lời cổ xưa, tài liệu hiện tại nói chính xác ý nghĩa của nó, ngày nay. Hơn nữa, thật sai lầm khi nói rằng JMM chỉ có hai chế độ truy cập. Chúng tôi có khả năng đọcviết dễ bay hơi , cùng nhau có thể thiết lập mối quan hệ xảy ra trước khi xảy ra .
Holger

1
Tôi đang ở giữa viết một cái gì đó nhiều hơn về nó. Hãy xem cas đó là cả hai, đọc và viết, hoạt động như một rào cản đầy đủ, và bạn có thể hiểu, tại sao thư giãn nó lại được mong muốn. Ví dụ: khi thực hiện khóa, hành động đầu tiên là cas (0, 1) về số lượng khóa, nhưng bạn chỉ cần có được ngữ nghĩa (như đọc dễ bay hơi), trong khi ghi cuối cùng là 0 để mở khóa phải phát hành ngữ nghĩa (như viết dễ bay hơi ), do đó, có một sự cố xảy ra trước khi mở khóa và khóa tiếp theo. Thu thập / phát hành thậm chí còn yếu hơn đọc / ghi dễ bay hơi liên quan đến các luồng sử dụng các khóa khác nhau.
Holger

1
@Peter Cordes: Phiên bản C đầu tiên có volatiletừ khóa là C99, năm năm sau Java, nhưng nó vẫn thiếu ngữ nghĩa hữu ích, thậm chí C ++ 03 không có Mô hình bộ nhớ. Những thứ mà C ++ gọi là "nguyên tử" cũng trẻ hơn nhiều so với Java. Và volatiletừ khóa thậm chí không ngụ ý cập nhật nguyên tử. Vậy tại sao nó phải được đặt tên như vậy.
Holger

1
@PeterCordes có lẽ, tôi nhầm lẫn với nó restrict, tuy nhiên, tôi nhớ những lần tôi phải viết __volatileđể sử dụng một phần mở rộng trình biên dịch không từ khóa. Vì vậy, có lẽ, nó đã không thực hiện C89 hoàn toàn? Đừng nói với tôi rằng tôi cũ. Trước Java 5, volatilegần gũi hơn với C. Nhưng Java không có MMIO, vì vậy mục đích của nó luôn là đa luồng, nhưng ngữ nghĩa tiền Java 5 không hữu ích cho điều đó. Vì vậy, phát hành / thu nhận như ngữ nghĩa đã được thêm vào, nhưng vẫn không phải là nguyên tử (cập nhật nguyên tử là một tính năng bổ sung được xây dựng trên nó).
Holger

2
@Eugene liên quan đến điều này , ví dụ của tôi là cụ thể cho việc sử dụng cas để khóa. Một chốt đếm ngược sẽ có các phân rã nguyên tử với ngữ nghĩa phát hành, theo sau là luồng đạt tới 0 chèn một hàng rào thu được và thực hiện hành động cuối cùng. Tất nhiên, có những trường hợp khác để cập nhật nguyên tử trong đó vẫn cần đầy đủ hàng rào.
Holger
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.