Giới thiệu về Mô hình bộ nhớ Java

Mô hình bộ nhớ Java (JMM) mô tả hành vi của các luồng trong môi trường thời gian chạy Java. Mô hình bộ nhớ là một phần ngữ nghĩa của ngôn ngữ Java và mô tả những gì một lập trình viên có thể và không nên mong đợi khi phát triển phần mềm không phải cho một máy Java cụ thể mà cho toàn bộ Java.

Mô hình bộ nhớ Java ban đầu (đặc biệt là đề cập đến "bộ nhớ percolocal"), được phát triển vào năm 1995, được coi là một thất bại: không thể thực hiện nhiều tối ưu hóa mà không làm mất đi sự đảm bảo về an toàn mã. Đặc biệt, có một số tùy chọn để viết "đơn" đa luồng:

  • hoặc mọi hành động truy cập một singleton (ngay cả khi đối tượng đã được tạo từ lâu và không có gì có thể thay đổi) sẽ gây ra khóa liên luồng;
  • hoặc là dưới một số tình huống nhất định, hệ thống sẽ phát ra một cái chưa hoàn thành cô đơn;
  • hoặc trong một số tình huống nhất định, hệ thống sẽ tạo ra hai kẻ cô độc;
  • hoặc thiết kế sẽ phụ thuộc vào hành vi của một máy cụ thể.

Do đó, cơ chế bộ nhớ đã được thiết kế lại. Vào năm 2005, với việc phát hành Java 5, một cách tiếp cận mới đã được trình bày, cách tiếp cận này đã được cải thiện hơn nữa với việc phát hành Java 14.

Mô hình mới dựa trên ba quy tắc:

Quy tắc #1 : Các chương trình đơn luồng chạy giả tuần tự. Điều này có nghĩa là: trên thực tế, bộ xử lý có thể thực hiện một số thao tác trên mỗi đồng hồ, đồng thời thay đổi thứ tự của chúng, tuy nhiên, tất cả các phụ thuộc dữ liệu vẫn còn, do đó hành vi không khác với tuần tự.

Quy tắc số 2 : không có giá trị nào ngoài hư không. Đọc bất kỳ biến nào (ngoại trừ long và double không biến đổi, mà quy tắc này có thể không giữ) sẽ trả về giá trị mặc định (không) hoặc một cái gì đó được viết ở đó bởi một lệnh khác.

quy tắc số 3 : phần còn lại của các sự kiện được thực hiện theo thứ tự, nếu chúng được kết nối bởi một mối quan hệ thứ tự từng phần nghiêm ngặt “thực hiện trước” ( xảy ra trước đó ).

Xảy ra trước

Leslie Lamport đã đưa ra khái niệm Xảy ra trước đây . Đây là một mối quan hệ thứ tự từng phần nghiêm ngặt được giới thiệu giữa các lệnh nguyên tử (++ và -- không phải là nguyên tử) và không có nghĩa là "về mặt vật lý trước".

Nó nói rằng nhóm thứ hai sẽ "biết" về những thay đổi được thực hiện bởi nhóm thứ nhất.

Xảy ra trước

Ví dụ: một cái được thực thi trước cái kia cho các hoạt động như vậy:

Đồng bộ hóa và giám sát:

  • Chụp màn hình ( phương thức khóa , khởi động được đồng bộ hóa) và bất kỳ điều gì xảy ra trên cùng một luồng sau đó.
  • Sự trở lại của màn hình ( mở khóa phương thức , kết thúc đồng bộ hóa) và bất kỳ điều gì xảy ra trên cùng một luồng trước đó.
  • Trả lại màn hình và sau đó chụp nó bằng một luồng khác.

Viết và đọc:

  • Ghi vào bất kỳ biến nào và sau đó đọc nó trong cùng một luồng.
  • Mọi thứ trong cùng một luồng trước khi ghi vào biến dễ bay hơi và chính việc viết. dễ bay hơi đọc và mọi thứ trên cùng một chuỗi sau nó.
  • Ghi vào một biến dễ bay hơi và sau đó đọc lại. Ghi không ổn định tương tác với bộ nhớ giống như cách quay lại màn hình, trong khi đọc giống như chụp. Nó chỉ ra rằng nếu một luồng được ghi vào một biến dễ bay hơi và luồng thứ hai tìm thấy nó, thì mọi thứ trước khi ghi sẽ được thực thi trước mọi thứ xuất hiện sau khi đọc; xem hình.

Bảo trì đối tượng:

  • Khởi tạo tĩnh và bất kỳ hành động nào với bất kỳ phiên bản nào của đối tượng.
  • Ghi vào các trường cuối cùng trong hàm tạo và mọi thứ sau hàm tạo. Như một ngoại lệ, mối quan hệ xảy ra trước không kết nối quá độ với các quy tắc khác và do đó có thể gây ra cuộc chạy đua giữa các chuỗi.
  • Bất kỳ công việc nào với đối tượng và hoàn thiện() .

Dịch vụ truyền phát:

  • Bắt đầu một chuỗi và bất kỳ mã nào trong chuỗi.
  • Zeroing các biến liên quan đến luồng và bất kỳ mã nào trong luồng.
  • Mã trong chuỗi và tham gia() ; mã trong chuỗi và isAlive() == false .
  • ngắt () luồng và phát hiện ra rằng nó đã dừng.

Xảy ra trước sắc thái công việc

Phát hành một màn hình xảy ra trước xảy ra trước khi có được cùng một màn hình. Điều đáng chú ý là đó là bản phát hành chứ không phải lối thoát, nghĩa là bạn không phải lo lắng về sự an toàn khi sử dụng thời gian chờ.

Hãy xem kiến ​​thức này sẽ giúp chúng ta sửa ví dụ của mình như thế nào. Trong trường hợp này, mọi thứ rất đơn giản: chỉ cần xóa kiểm tra bên ngoài và để nguyên trạng thái đồng bộ hóa. Giờ đây, luồng thứ hai được đảm bảo nhìn thấy tất cả các thay đổi, bởi vì luồng này sẽ chỉ nhận được màn hình sau khi luồng kia giải phóng nó. Và vì anh ấy sẽ không phát hành nó cho đến khi mọi thứ được khởi tạo, nên chúng tôi sẽ thấy tất cả các thay đổi cùng một lúc chứ không phải riêng biệt:

public class Keeper {
    private Data data = null;

    public Data getData() {
        synchronized(this) {
            if(data == null) {
                data = new Data();
            }
        }

        return data;
    }
}

Việc ghi vào một biến dễ bay hơi xảy ra trước khi đọc từ cùng một biến. Tất nhiên, thay đổi mà chúng tôi đã thực hiện sẽ sửa lỗi, nhưng nó khiến bất kỳ ai đã viết mã ban đầu trở lại vị trí ban đầu - chặn mọi lúc. Từ khóa dễ bay hơi có thể tiết kiệm. Trên thực tế, câu lệnh được đề cập có nghĩa là khi đọc mọi thứ được khai báo là biến động, chúng ta sẽ luôn nhận được giá trị thực.

Ngoài ra, như tôi đã nói trước đó, đối với các trường dễ bay hơi, việc viết luôn (bao gồm cả dài và gấp đôi) là một thao tác nguyên tử. Một điểm quan trọng khác: nếu bạn có một thực thể dễ bay hơi có tham chiếu đến các thực thể khác (ví dụ: một mảng, Danh sách hoặc một số lớp khác), thì chỉ một tham chiếu đến chính thực thể đó sẽ luôn "mới", chứ không phải mọi thứ trong đó. nó đến.

Vì vậy, hãy quay lại với ram khóa kép của chúng tôi. Sử dụng dễ bay hơi, bạn có thể khắc phục tình trạng như thế này:

public class Keeper {
    private volatile Data data = null;

    public Data getData() {
        if(data == null) {
            synchronized(this) {
                if(data == null) {
                    data = new Data();
                }
            }
        }
        return data;
    }
}

Ở đây chúng tôi vẫn có khóa, nhưng chỉ khi dữ liệu == null. Chúng tôi lọc ra các trường hợp còn lại bằng cách đọc dễ bay hơi. Tính chính xác được đảm bảo bởi thực tế là cửa hàng biến động xảy ra trước khi đọc biến động và tất cả các hoạt động xảy ra trong hàm tạo đều hiển thị cho bất kỳ ai đọc giá trị của trường.