Mô phỏng một rào cản bộ nhớ trong Java để thoát khỏi các lần đọc dễ bay hơi


8

Giả sử tôi có một trường được truy cập đồng thời và nó được đọc nhiều lần và hiếm khi được viết.

public Object myRef = new Object();

Giả sử một Chủ đề T1 sẽ đặt myRef thành một giá trị khác, mỗi phút một lần, trong khi N Chủ đề khác sẽ đọc myRef hàng tỷ lần liên tục và đồng thời. Tôi chỉ cần rằng myRef cuối cùng sẽ hiển thị cho tất cả các chủ đề.

Một giải pháp đơn giản là sử dụng AtomicReference hoặc đơn giản là dễ bay hơi như thế này:

public volatile Object myRef = new Object();

Tuy nhiên, afaik đọc không ổn định làm phát sinh chi phí hiệu suất. Tôi biết nó rất nhỏ, nó giống như một thứ tôi tự hỏi hơn là thứ tôi thực sự cần. Vì vậy, chúng ta đừng quan tâm đến hiệu suất và cho rằng đây là một câu hỏi lý thuyết đơn thuần.

Vì vậy, câu hỏi đặt ra: Có cách nào để bỏ qua các lần đọc dễ bay hơi cho các tài liệu tham khảo mà hiếm khi được viết ra, bằng cách làm một cái gì đó tại trang web viết không?

Sau khi đọc, có vẻ như rào cản bộ nhớ có thể là thứ tôi cần. Vì vậy, nếu một cấu trúc như thế này tồn tại, vấn đề của tôi sẽ được giải quyết:

  • Viết
  • Gọi rào cản (đồng bộ hóa)
  • Tất cả mọi thứ được đồng bộ hóa và tất cả các chủ đề sẽ thấy giá trị mới. (không có chi phí cố định tại các trang web đọc, nó có thể cũ hoặc phải chịu chi phí một lần vì bộ nhớ cache được đồng bộ hóa, nhưng sau đó tất cả trở lại trường thông thường cho đến lần ghi tiếp theo).

Có một cấu trúc như vậy trong Java, hay nói chung? Tại thời điểm này tôi không thể không nghĩ rằng nếu một cái gì đó như thế này tồn tại, nó sẽ được tích hợp vào các gói nguyên tử bởi những người thông minh hơn nhiều đang duy trì chúng. (Việc đọc thường xuyên so với viết thường xuyên có thể không phải là một trường hợp cần quan tâm?) Vì vậy, có thể có điều gì đó sai trong suy nghĩ của tôi và một cấu trúc như vậy là không thể?

Tôi đã thấy một số mẫu mã sử dụng 'dễ bay hơi' cho mục đích tương tự, khai thác nó xảy ra trước khi ký hợp đồng. Có một trường đồng bộ hóa riêng, ví dụ:

public Object myRef = new Object();
public volatile int sync = 0;

và tại viết chủ đề / trang web:

myRef = new Object();
sync += 1 //volatile write to emulate barrier

Tôi không chắc chắn điều này hoạt động, và một số tranh luận điều này chỉ hoạt động trên kiến ​​trúc x86. Sau khi đọc các phần liên quan trong JMS, tôi nghĩ rằng nó chỉ được đảm bảo để hoạt động nếu cách viết dễ bay hơi đó được kết hợp với đọc dễ bay hơi từ các luồng cần xem giá trị mới của myRef. (Vì vậy, không thoát khỏi đọc dễ bay hơi).

Trở lại câu hỏi ban đầu của tôi; cái này nó có hoàn toàn có thể xảy ra được không? Có thể có trong Java? Có thể có một trong các API mới trong Java 9 VarHandles không?


1
Đối với tôi nghe có vẻ như bạn đang đi sâu vào lãnh thổ nơi bạn cần viết và chạy một số điểm chuẩn thực tế mô phỏng khối lượng công việc của bạn.
NPE

1
JMM nói rằng nếu chủ đề nhà văn của bạn thực hiện sync += 1;và chủ đề người đọc của bạn đọc syncgiá trị, họ cũng sẽ thấy myRefcập nhật. Vì cuối cùng bạn chỉ cần người đọc thấy bản cập nhật , nên bạn có thể sử dụng điều này cho lợi thế của mình để chỉ đọc đồng bộ trên mỗi lần lặp thứ 1000 của luồng người đọc hoặc một cái gì đó tương tự. Nhưng bạn cũng có thể thực hiện một mẹo tương tự với volatile- chỉ cần lưu trữ myReftrường trong các trình đọc trong 1000 lần lặp, sau đó đọc lại bằng cách sử dụng dễ bay hơi ...
Petr Janeček

@ PetrJaneček Nhưng anh ta không phải đồng bộ hóa quyền truy cập vào biến đếm được chia sẻ giữa các luồng? Đó không phải là một nút cổ chai? Theo tôi đó sẽ còn tốn kém hơn nữa.
Ravindra Ranwala

@RavindraRanwala Mỗi người đọc sẽ có bộ đếm riêng, nếu bạn muốn đếm đến 1000 lần lặp hoặc hơn. Nếu bạn có nghĩa là synctrường, không, độc giả sẽ không chạm vào synctrường trên mỗi lần lặp, họ sẽ thực hiện nó theo cơ hội, khi họ muốn kiểm tra xem đã có bản cập nhật chưa. Điều đó nói rằng, một giải pháp đơn giản hơn là lưu trữ bộ đệm trong myRef1000 vòng, sau đó đọc lại ...
Petr Janeček

@ PetrJaneček cảm ơn, tôi đã nghĩ về nó như là một giải pháp có thể. Nhưng tôi tự hỏi nếu điều này là có thể bằng cách sử dụng một triển khai chung, vững chắc.
sydnal

Câu trả lời:


2

Vì vậy, về cơ bản bạn muốn ngữ nghĩa của một volatilemà không có chi phí thời gian chạy.

Tôi không nghĩ rằng nó có thể.

Vấn đề là chi phí thời gian chạy volatilelà do các hướng dẫn thực hiện các rào cản bộ nhớ trong trình ghi và mã trình đọc. Nếu bạn "tối ưu hóa" người đọc bằng cách thoát khỏi rào cản bộ nhớ của nó, thì bạn không còn được đảm bảo rằng người đọc sẽ thấy giá trị mới "hiếm khi được viết" khi nó thực sự được viết.

FWIW, một số phiên bản của sun.misc.Unsafelớp cung cấp các phương thức và phương thức rõ ràng loadFence, nhưng tôi không nghĩ rằng việc sử dụng chúng sẽ mang lại bất kỳ lợi ích hiệu suất nào khi sử dụng a .storeFencefullFencevolatile


Theo giả thuyết ...

những gì bạn muốn là cho một bộ xử lý trong một hệ thống đa bộ xử lý để có thể nói với tất cả các bộ xử lý khác:

"Này! Dù bạn đang làm gì, hãy vô hiệu hóa bộ nhớ cache của bạn cho địa chỉ XYZ và thực hiện ngay bây giờ."

Thật không may, các ISA hiện đại không hỗ trợ điều này.

Trong thực tế, mỗi bộ xử lý kiểm soát bộ đệm riêng của mình.


Tôi thấy, phần giả thuyết trong câu trả lời của bạn là những gì tôi đã theo đuổi. Cảm ơn.
sydnal

1

Không hoàn toàn chắc chắn nếu điều này là chính xác nhưng tôi có thể giải quyết điều này bằng cách sử dụng hàng đợi.

Tạo một lớp bao bọc một thuộc tính ArrayBlockingQueue. Lớp có một phương thức cập nhật và phương thức đọc. Phương thức cập nhật đăng giá trị mới lên hàng đợi và xóa tất cả các giá trị ngoại trừ giá trị cuối cùng. Phương thức đọc trả về kết quả của thao tác nhìn trộm trên hàng đợi, tức là đọc nhưng không xóa. Chủ đề nhìn trộm phần tử ở phía trước của hàng đợi làm như vậy không bị cản trở. Chủ đề cập nhật hàng đợi làm như vậy sạch sẽ.


1
  • Bạn có thể sử dụng ReentrantReadWriteLockđược thiết kế cho một vài kịch bản đọc nhiều.
  • Bạn có thể sử dụng StampedLockđược thiết kế cho cùng một vài trường hợp viết nhiều lần đọc, nhưng cũng có thể cố gắng đọc một cách lạc quan. Thí dụ:

    private StampedLock lock = new StampedLock();
    
    public void modify() {            // write method
        long stamp = lock.writeLock();
        try {
          modifyStateHere();
        } finally {
          lock.unlockWrite(stamp);
        }
    } 
    
    public Object read() {            // read method
      long stamp = lock.tryOptimisticRead();
      Object result = doRead();       //try without lock, method should be fast
      if (!lock.validate(stamp)) {    //optimistic read failed
        stamp = lock.readLock();      //acquire read lock and repeat read
        try {
          result = doRead();
        } finally {
          lock.unlockRead(stamp);
        }
      }
      return result;
    }
  • Làm cho trạng thái của bạn không thay đổi và chỉ cho phép sửa đổi có kiểm soát bằng cách nhân bản đối tượng hiện có và chỉ thay đổi các thuộc tính cần thiết thông qua hàm tạo. Khi trạng thái mới được xây dựng, bạn gán nó cho tham chiếu được đọc bởi nhiều luồng đọc. Cách này đọc chủ đề phát sinh chi phí bằng không .


nếu bạn cảm thấy muốn hạ thấp, xin vui lòng cho biết lý do tại sao, để tác giả và cộng đồng có thể tìm hiểu
diginoise

Làm cho nó bất biến là không thể trong kịch bản của tôi. Và tôi sẽ khá ngạc nhiên nếu hộp khóa có dán tem phát sinh ít chi phí hơn so với việc đọc dễ bay hơi đơn giản. Tuy nhiên tôi sẽ thử nó, cảm ơn.
sydnal
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.