Multithreading: Java Memory Model

Java_Multithreading_Live_Scenario.png

Ở các bài viết trước, mình đã đề cập tới cách khởi tạo và xử lý đa luồng (multiple threads) trong Java, trong bài viết này chúng ta sẽ đi sâu hơn về kiến trúc, các cách tổ chức sắp xếp bộ nhớ trong Java.

Tìm hiểu về Java Memory Model (Mô hình bộ nhớ Java), Cách mà Java Virtual Machine (Máy ảo Java) làm việc với bộ nhớ của máy tính (RAM). Điều đó thực sự rất quan trọng nếu bạn muốn thiết kế và thực thi một cách chính xác ứng dụng. Dựa vào Java Memory Model, chúng ta sẽ hiểu cơ chế Thread quan sát giá trị các shared variables(các biến được chia sẻ) được ghi bởi một Thread khác và làm thế nào để đồng bộ hóa quyền truy cập vào các shared variables khi cần thiết.

1. The Internal Java Memory Model

Java Memory Model được sử dụng trong trong các JVM chia bộ nhớ thành 2 thành phần Thread stacksHeap. Biểu đồ sau minh họa Java Memory Model từ góc độ logic:

java-memory-model-1.png

  • Mỗi Thread khi chạy trên JVM sẽ có một Thread stack riêng. Thread stack chứa các thông tin về các methods mà thread đã gọi khi thực thi. Khi các thread thực thi mã của nó, các stack sẽ thay đổi.

  • Thread stack cũng bao gồm tất cả các local variables của mỗi method được execute (all methods on the call stack). Một thread có thể chỉ có quyền truy cập tới thread stack của nó. Local variables được tạo bởi thread chỉ available với chính Thread tạo ra nó. Ngay cả khi nếu 2 Threads cùng thực thi một đoạn code giống nhau thì 2 Threads vẫn tạo ra các local variables riêng bên trong Thread stack. Do đó, mỗi thread có một version cho các local variables.

Tất cả các local variables có kiểu dữ liệu gốc (boolean, byte, short, char, int, long, float, double) được lưu trữ đầy đủ trên Thread stack và không khả dụng với các thread khác. Một Thread có thể pass một bản copy của pritimive variable sang một Thread khác, Nhưng nó lại ko thể chia sẻ primitive local variable của chính nó.

  • Heap chứa tất cả các objects được tạo ra bởi Java application. Gồm có các object versions kiểu primitive (e.g. Byte, Integer, Long etc.). Không thành vấn đề nếu một object được tạo ra và gán cho một local variable, hoặc tạo ra như là một member variable của đối tượng khác, các object vẫn được lưu trên heap.

Dưới đây là sơ đồ minh họa các call stack, local variables được lưu trữ trên các Thread stacks, và các object được lưu trữ trên Heap:

java-memory-model-2.png

Nếu local variable có kiểu dữ liệu gốc, sẽ được lưu trữ hoàn toàn trong Thread stack.

Nếu local variable là một reference tới object. Trong trường hợp này, reference (local variable) được lưu trên Thread stack, nhưng bản thân object được lưu trữ trên Heap.

Một object có thể chứ các method và các method này có thể chứa các local variables. Mỗi local variables này cũng được lưu trữ trên Thread stack, ngay cả khi các object method được lưu trữ trên Heap.

Đối với một object's member variables được lưu trữ trên Heap.

Static class variables cũng được lưu trữ trên Heap cùng với class definition.

Objects trên Heap có thể được truy cập bởi tất cả các Threads có reference đến object. Khi một thread có truy cập tới object, nó có thể có quyền truy cập tới các object's member variables. Nếu 2 threads gọi một method trên một object vào cùng một thời điểm, chúng sẽ có cả 2 quyền truy cập tới object's member variables, Nhưng mỗi thread sẽ có một bản sao của local variables.

Sơ đồ dưới đây mô ta điều chúng ta vừa đề cập:

java-memory-model-3.png

2 Threads có một tập các local variables. Một trong số các local variables (Local Variable 2) trỏ tới shared object trên Heap (Object 3). 2 Threads có các reference khác nhau tới chung một object. References đó là các local variables và được lưu trên Thread stack (của mỗi thread). Do đó ta sẽ có 2 references cùng trỏ tới một object trên Heap.

Notice how the shared object (Object 3) has a reference to Object 2 and Object 4 as member variables (illustrated by the arrows from Object 3 to Object 2 and Object 4). Via these member variable references in Object 3 the two threads can access Object 2 and Object 4.

Lưu ý: shared object (Object 3) có một tham chiếu đến Object 2Object 4 như các biến thành viên (minh họa bằng các mũi tên từ Object 3 đến Object 2Object 4). Qua các tham chiếu tới biến thành viên trong Object 3, 2 Threads có thể truy cập Object 2Object 4.

Biểu đồ trên cũng biểu diễn việc local variable trỏ tới 2 objects khác nhau trên Heap. Trong trường hợp này tham chiếu trỏ tới (Object 1Object 5). Theo lý thuyết, cả 2 threads có thể truy cập cả Object 1Object 5, nếu như cả 2 threads có tham chiếu tới cả 2 objects. Trong biểu đồ trên thì mỗi thread chỉ có một tham chiếu reference tới một object.

Chúng ta sẽ biểu diễn sơ đồ trên bằng đoạn mã Java sau:

public class MyRunnable implements Runnable() {

    public void run() {
        methodOne();
    }

    public void methodOne() {
        int localVariable1 = 45;
        MySharedObject localVariable2 = MySharedObject.sharedInstance;
        //... do more with local variables.
        methodTwo();
    }

    public void methodTwo() {
        Integer localVariable1 = new Integer(99);
        //... do more with local variable.
    }
}
public class MySharedObject {

    //static variable pointing to instance of MySharedObject
    public static final MySharedObject sharedInstance = new MySharedObject();

    //member variables pointing to two objects on the heap
    public Integer object2 = new Integer(22);
    public Integer object4 = new Integer(44);

    public long member1 = 12345;
    public long member1 = 67890;
}

Nếu 2 threads trên thực thi phương thức run(), methodOne()methodTwo() sẽ được gọi, từ đó ta có sơ đồ lưu trữ bộ nhớ tương tự như trên. localVariable2 = MySharedObject instance được lưu trữ trên Heap tương ứng với Object 3.

2. Hardware Memory Architecture

Hardware Memory Architecture (Kiến trúc bộ nhớ phần cứng) có đôi chút khác biệt với Java Memory Model. Trong phần này sẽ mô tả như Hardware memory & Java Memory Model làm việc với nhau như thế nào:

java-memory-model-4.png

Với mô hình trên, ta có một hình máy tính với 2 CPU, mỗi CPU có khả năng chạy một thread trong một thời điểm, mỗi CPU có chứa một tập thanh ghi register (thành phần tối thiểu trong CPU-memory), nhờ vào thanh ghi, CPU có thể thực hiện tính toán nhanh hơn là sử dụng bộ nhớ chính. Ngoài ra. mỗi CPU cũng bao gồm CPU cache memory layer. Thông thường, tốc độ truy cập dữ liệu sẽ có thứ tự sau: Main memory < Cache memory < Internal registers

Khi CPU cần truy cập main memory, nó sẽ đọc một phần main memory vào trong cache, rồi lại đọc một phần cache vào trong internal registers, sau đó mới tiến hành các tác vụ tính toán và xử lý dữ liệu.

3. Mối liên hệ giữa Java Memory Model và Hardware Memory Architecture

Như đã đề cập ở phần trước, Java memory model và hardware memory architecture không giống nhau. Hardware memory architecture không phân biệt giữa thread stacks và heap. Trên phương diện phần cứng, cả thread stack và heap được đặt trong main memory. Đôi khi, một phần thread stacks và heap xuất hiện trên CPU caches và internal CPU registers.

java-memory-model-5.png

Tuy nhiên, chính việc object và variables có thể được lưu trữ trên nhiều vùng nhớ của máy tính, dẫn tới phát sinh 2 vấn đề chính:

  • Tính khả thi của thread khi muốn cập nhật (writes) shared variables.
  • Race conditions xảy ra khi đọc, kiểm tra hoặc ghi giá trị lên shared variables.

4. References