Multithreading: Race Conditions, Critical Sections và Thread Safety

Java_Multithreading_Live_Scenario.png

1. Race Conditions & Critical Sections

1.1. Race Conditions

Race conditions (Tình huống tương tranh). Là trường hợp thường xảy ra bên trong critical section. Khi có hai hay nhiều Thread cùng chia sẻ dữ liệu, hay đơn giản là cùng đọc và ghi vào một vùng dữ liệu. Khi đó vấn đề xảy ra là: Kết quả của việc thực thi multiple threads có thể thay đổi phụ thuộc vào thứ tự thực thi các thread. race_conditions.jpg

1.2. Critical Sections

Critical sections (Đoạn mã găng). Là đoạn mã mà khi khởi chạy chúng trên nhiều thread sẽ dẫn tới việc đọc ghi chung biến, file ... (Tổng quát hơn là dữ liệu). Lưu ý: việc khởi chạy nhiều hơn một thread bên trong cùng một ứng dụng không tự phát sinh vấn đề. Ví dụ:

public class Counter {
    protected long count = 0;

    public void add(long value) {
        this.count = this.count + value;
    }
}

Tưởng tượng rằng, nếu chúng ta có 2 thread A & B cùng thực thi add Method trên một instance object của Counter Class. Chúng ta sẽ không thể biết được khi nào hệ thống chuyển đổi giữa 2 thread. Các đoạn mã bên trong add() Method không được thực thi như một single atomic instruction (ngăn cản các processor khác hoặc thiết bị I/O từ việc ghi hay đọc bộ nhớ cho đến khi hoạt động atomic được hoàn tất) trong Java thread, thay vào đó, chúng được thực thi như một tập các instruction nhỏ hơn như sau:

  • Read this.count từ memory trong register.
  • Thêm value vào register
  • Write register vào memory.

Hiển nhiên khi đó, nếu 2 thread A & B cùng được thực thi add(2) & add(3) thì:

Khởi tạo this.count = 0;
A:  Đọc this.count trong register (0)
B:  Đọc this.count trong register (0)
B:  Thêm value = 2 vào register
B:  Ghi register value = 2 lại vào memory. (Now): this.count = 2
A:  Thêm value = 3 vào register
A:  Ghi register value = 3 lại vào memory. (Now): this.count = 3

2 threads add các giá trị 2 và 3 vào counter, như vậy giá trị mong muốn là 5 sau khi 2 thread được thực thi. Tuy nhiên, do việc thực thi Thread là xen kẽ, nên ta sẽ có kêt quả hoàn toàn khác. Thay vì output ra 5, thì giá trị sau cùng của this.count lại là giá trị được ghi bởi thread cuối cùng thực hiện.

1.3. Race Conditions trong Critical Sections

Đoạn code bên trong add() method ở ví dụ trên có chứa critical section. Khi multiple threads thực thi critical section này, race conditions sẽ xảy ra. Cụ thể hơn, khi 2 hay nhiều threads cùng sử dụng chung một resource, khi mà thứ tự thao tác với resource có ý nghĩa quan trọng được gọi là race conditions. Đoạn code dẫn tới race conditions được gọi là critical sections.

1.4. Solution

Để tránh xung đột tài nguyên khi chạy multiple-thread, Java đưa ra một cách giải quyết là dùng từ khoá synchronized cho phương thức hay đoạn code có chứa critical section.

Ví dụ:

public class TwoSums {
    private int sum1 = 0;
    private int sum2 = 0;

    public void add(int val1, int val2) {
        synchronized(this) {
            this.sum1 += val1;
            this.sum2 += val2;
        }
    }
}

Với cách này, chỉ có single thread mới có quyền thực thi việc cộng tổng trong cùng một khoảng thời gian. Chúng ta cũng có thể chia synchronized thành nhiều block, với mỗi block không phụ thuộc vào nhau:

public class TwoSums {
    private int sum1 = 0;
    private int sum2 = 0;

    public void add(int val1, int val2) {
        synchronized(this) {
            this.sum1 += val1;
        }
        synchronized(this) {
            this.sum2 += val2;
        }
    }
}

2. Thread Safety & Shared Resources

Khi multiple threads cùng khởi chạy và gọi tới một đoạn mã mà không xuất hiện race conditions được gọi là thread safe.

2.1. Local Variables

Các biến local được lưu trong mỗi Thread local stack, nghĩa là biến local thì không bị chia sẻ giữa các thread nên tất cả các biến local primitive được gọi là thread safe

public void someMethod() {
    long threadSafeInt = 0;
    threadSafeInt++;
}

2.2.Local Object References

Bản thân reference tới chính nó không được share. Tuy nhiên object referenced không được lưu trữ trong Thread local stack. Tất cả các objects được lưu trữ trong shared heap.

Nếu một object được tạo ra và chỉ được sử dụng bên trong method tạo ra nó, thì nó được coi là thread safe. Trong thực tế, bạn có thể truyền nó sang method khác, miễn là không có method hoặc đối tượng nào khiến object được truyền available trên các thread khác !

Ví dụ: Thread safe local object

public void someMethod() {
    LocalObject localObject = new LocalObject();

    localObject.callMethod();
    method2(localObject);
}

public void method2(LocalObject localObject) {
    localObject.setValue("value");
}

Ở ví dụ trên, LocalObject instance không được trả về bởi bất cứ method nào, Và cũng không được truyền vào các objects có thể truy cập bên ngoài phương thức someMethod(). Mỗi thread gọi tới someMethod() sẽ tạo ra một LocalObject instance riêng và gán cho chúng một localObject reference. Do đó cách sử dụng LocalObject như trên được coi là thread safe.

2.3. Object Member Variables

Các biến Object member thì được lưu trữ trên heap cùng với các object. Tuy nhiên nếu 2 threads cùng gọi tới một method trên cùng một object instance và method này cập nhật lại giá trị các Object member, thì method đó không được coi là thread safe.

Ví dụ:

public class NotThreadSafe {
    StringBuilder builder = new StringBuilder();

    public add(String text) {
        this.builder.append(text);
    }
}

Nếu 2 threads cùng gọi tới method add() trên một instance object NotThreadSafe đồng thời, sẽ dẫn tới race conditions.

NotThreadSafe sharedInstance = new NotThreadSafe();

new Thread(new MyRunnable(sharedInstance)).start();
new Thread(new MyRunnable(sharedInstance)).start();

public class MyRunnable implements Runnable {
    NotThreadSafe instance = null;

    public MyRunnable(NotThreadSafe instance) {
        this.instance = instance;
    }

    public void run() {
        this.instance.add("some text");
    }
}

Tuy nhiên, nếu 2 threads cùng gọi tới method add() trên các instances object khác nhau NotThreadSafe đồng thời, thì sẽ không dẫn tới race condition.

new Thread(new MyRunnable(new NotThreadSafe())).start();
new Thread(new MyRunnable(new NotThreadSafe())).start();

2.4. Thread Control Escape Rule

Resource có thể là object, array, file, database connection, socket, ... Ngay cả khi sử dụng một object thread save và object đó là file hoặc database, Ứng dụng có thể không phải là thread safe. Ví dụ, nếu thread 1 và thread 2 cùng create database connections cho riêng nó (connection 1 và connection 2), thì bản thân mỗi connection được coi là thread safe. Nhưng việc sử dụng database connections không được coi là thread safe.

Ví dụ: Nếu cả 2 threads cùng thực thi đoạn code sau:

check if record X exists
if not, insert record X

Nếu 2 thread cùng thực thi đồng thời công việc đó, và record X được tiến hành kiểm tra vào cùng 1 thời điểm, Sẽ có khả năng cả 2 threads cùng thêm mới(insert) record X. Đây là lý do tại sao:

Thread 1 checks if record X exists. Result = no
Thread 2 checks if record X exists. Result = no
Thread 1 inserts record X
Thread 2 inserts record X

Điều này cũng có thể xảy ra với thread đang hoạt động trên các tập tin hoặc các resource được chia sẻ khác. Vì vậy điều quan trọng là phải phân biệt giữa việc một đối tượng điều khiển bởi một thread là tài nguyên, hoặc nếu nó chỉ đơn thuần là references tới resource (ví dụ như kết nối tới một database).

3. Thread Safety & Immutability

Khởi đầu với ví dụ sau:

public class ImmutableValue{

    private int value = 0;

    public ImmutableValue(int value) {
        this.value = value;
    }

    public int getValue() {
        return this.value;
    }
}

Hãy xem cách mà value của ImmutableValue instance được truyền trong constructor.Ở đây chúng ta không có setter method. Một khi ImmutableValue instance được khởi tạo, bạn không thể thay đổi giá trị của nó. Đó chính là immutable (không thay đổi). Chúng ta có thể đọc nó, nhưng qua phương thức getValue().

Nếu bạn muốn thực hiện các thao tác với ImmutableValue instance, ta có thể làm điều đó bằng cách trả về new instance với giá trị kết quả là instance của ImmutableValue

public class ImmutableValue{

    private int value = 0;

    public ImmutableValue(int value){
        this.value = value;
    }

    public int getValue(){
        return this.value;
    }

    public ImmutableValue add(int valueToAdd){
        return new ImmutableValue (this.value + valueToAdd);
    }

}

Lưu ý rằng: method add() trả về new ImmutableValue instance với kết quả của phép toán cộng chứ không cộng thêm vào giá trị value của chính nó.

4. References


All Rights Reserved