Đồng nghiệp tôi vừa mới Golive và bùm, race condition

18 min read 194

Hôm nọ ông em đồng nghiệp mình có golive một hệ thống liên quan đến nâng cấp kết nối với bank M*, quá trình Pilot oke hết không vấn đề gì, QC không phát hiện ra lỗi gì cả, hệ thống trông có vẻ rất trơn tru,…

Cho đến khi tắt Pilot, khách hàng bắt đầu vào nạp tiền nhiều cùng lúc từ bank M*, chăm sóc khách hàng báo lỗi rằng khách không nạp tiền để sử dụng dịch vụ được, bla bla,….

Sau đấy thì anh em kĩ thuật của team ông em kia có ngồi khoanh vùng vấn đề và xử lý, mình có nắm được tình hình và nhận ra ngay bản chất cuối cùng thì cái vấn đề nó lại đến từ việc Race Condition.

Race condition là gì?

Trước khi đi sâu vào phân tích đoạn code bị lỗi, chúng ta hãy điểm qua một khái niệm cốt lõi trong lập trình đa luồng, đó là: Race Condition(tranh chấp tài nguyên)

Race Condition xảy ra khi hai hoặc nhiều luồng (thread) truy cập và thay đổi cùng một tài nguyên (shared resource) cùng lúc, khiến kết quả cuối cùng phụ thuộc vào thứ tự thực thi của các luồng, và vì thứ tự này không thể dự đoán trước nên dễ dẫn đến kết quả không mong muốn hoặc không ổn định.

Các bạn hãy thử hình dung Race Condition giống như việc hai người cùng lúc cập nhật một thông tin vào cùng một cuốn sổ chung.

Giả sử nhiệm vụ của QC NgàQC Huệ sẽ diễn ra như sau: “Ghi thêm một mục mới vào sổ, sau đó cập nhật tổng số mục ở trang bìa của cuốn sổ lên”.

Lúc này sẽ có hai kịch bản diễn ra, kịch bản lý tưởng là hai người làm tuần tự:

  1. QC Ngà đọc tổng số là 10, viết thêm mục của mình, rồi cập nhật tổng số lên 11
  2. QC Huệ đến sau, đọc tổng số là 11, viết thêm mục mới, rồi cập nhật tổng số lên 12
  3. Kết quả: Cuốn sổ ghi nhận đúng 12 mục

Tuy nhiên đó chỉ là kịch bản lý tưởng, giờ sẽ là kịch bản xảy ra tranh chấp giữa hai người:

  1. QC Ngà và QC Huệ cùng lúc đọc tổng số mục hiện tại là 10
  2. Sau đó cả hai cùng viết mục mới của mình vào các trang nháp riêng của mình, mỗi trang nháp ghi nhận là 10
  3. Sau đó QC Huệ nghe điện thoại, QC Ngà tiếp tục làm việc
  4. QC Ngà tiến hành sửa đổi dữ liệu trang bìa lên 11 dựa theo dữ liệu nháp vừa ghi của bản thân
  5. Ngay sau đó, QC Huệ cũng cập nhật tổng số lên 11 (vì lúc đầu cô ấy cũng đọc được là 10)

Hậu quả là: Có hai mục mới được thêm vào, nhưng trang bìa chỉ ghi tổng số là 11 thay vì đáng lẽ nó phải là 12, dữ liệu đã bị hỏng vĩnh viễn vì QC Huệ đã ghi vào sổ chung dựa trên thông tin cũ trên tờ giấy nháp cá nhân của mình.

Hiện trạng của đoạn code “gây lỗi”

Vì lý do bảo mật nên mình không show trực tiếp đoạn code thực tế đang bị lỗi lên, nhưng mình sẽ mô phỏng lại nó bằng 1 đoạn code bị lỗi tương đương để các bạn dễ hình dung.

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.springframework.stereotype.Service;

/**
 * hungtv27
 * cafeincode.com
 */

@Service
public class PaymentService {


    private String authMethod = null;


    public Map<String, Object> processPayment(long userId) throws InterruptedException {

        // Bước 1: Khởi tạo lại trạng thái cho request hiện tại
        authMethod = null;

        // Bước 2: Xác định phương thức xác thực
        authMethod = getAuthenticationMethod(userId);
        TimeUnit.MILLISECONDS.sleep(275);

        // Bước 3: Tạo và trả về kết quả
        Map<String, Object> response = new HashMap<>();
        response.put("status", "SUCCESS");
        response.put("authMethod", authMethod);

        return response;
    }

    //Todo: Giả lập việc lấy phương thức xác thực
    //Todo: Trong thực tế, đây sẽ là một đoạn logic phức tạp gọi vào DB hoặc API để xác định chính xác danh sách PTXT cho giao dịch
    private String getAuthenticationMethod(long userId) {
        return (userId % 2 == 0) ? "OTP" : "BIOMETRIC";
    }
}

Trước tiên là cần phải bóc tách và tìm hiểu tại sao người ta lại code như phía trên nhé:

  1. Đầu tiên là class PaymentService này được đánh dấu là 1 bean dùng chung, singleton cho 1 instance cụ thể khi khởi chạy
  2. Biến autheMethod được khai báo là biến instance của lớp
  3. Logic ở trong hàm processPayment thực hiện một số logic công việc xác định phương thức xác thực
  4. Đầu tiên sẽ set lại giá trị authMethod về null
  5. Thực hiện tính toán giá trị cho authMethod dựa theo userId
  6. Sau đó put ngược kết quả vào Map kết quả
  7. Trả về Map dữ liệu
  8. Hậu quả thực tế: dữ liệu trả về của authMethod cho các userId lúc đúng, lúc sai

Oke sau khi phân tích về luồng thực hiện của class bị sai kia thì có thể nhiều bạn sẽ chưa thực sự hiểu nó đang có vấn đề gì đâu, giờ mình sẽ viết một đoạn code ở class ConcurrencyTest để chạy giả lập xem khi có 20 khách hàng đồng thời vào sử dụng dịch vụ, thì lúc đó sẽ có vấn đề như thế nào.

import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class ConcurrencyTest {

    public static void main(String[] args) throws InterruptedException {
        var requestId = UUID.randomUUID().toString();
        System.out.println("cafeincode-Start checking concurrency issue PaymentService with requestID: " + requestId);
        concurrencyCalculator(new PaymentService());
    }


    private static void concurrencyCalculator(Object serviceInstance) throws InterruptedException {
        int numberOfThreads = 20;
        ExecutorService executor = Executors.newFixedThreadPool(numberOfThreads);
        AtomicInteger errorCount = new AtomicInteger(0);

        for (int i = 0; i < numberOfThreads; i++) {
            final int userId = 100 + i; // Mỗi luồng một user ID khác nhau

            executor.submit(() -> {
                try {
                    var expectedAuthMethod = (userId % 2 == 0) ? "OTP" : "BIOMETRIC";

                    Map<String, Object> result;
                    if (serviceInstance instanceof PaymentService) {
                        result = ((PaymentService) serviceInstance).processPayment(userId);
                    }

                    var actualAuthMethod = (String) result.get("authMethod");

                    if (!expectedAuthMethod.equals(actualAuthMethod)) {
                        errorCount.incrementAndGet();
                        System.err.printf("[cafeincode-INCORRECT!] User %d: Expected authMethod='%s' but got '%s'%n",
                            userId, expectedAuthMethod, actualAuthMethod);
                    } else {
                        System.out.printf("[cafeincode-CORRECT] User %d: authMethod='%s' as expected.%n",
                            userId, actualAuthMethod);
                    }
                } catch (Exception ex) {
                    log.error("[cafeincode-INCORRECT!] Error occurred while processing user {}: {}", userId, ex.getMessage(), ex);
                }
            });
        }

        executor.shutdown();
        executor.awaitTermination(5, TimeUnit.SECONDS);
        System.out.printf("=> SUMMARY: %d/%d requests returned incorrect results.%n", errorCount.get(), numberOfThreads);
    }
}

Đoạn code kiểm thử ở trên của mình nó sẽ làm những việc sau đây:

  1. ConcurrencyTest tạo ra một ExecutorService với 20 luồng
  2. Nó lặp 20 lần, mỗi lần tạo một tác vụ (task) cho một userId khác nhau, userId tăng dần để đảm bảo chẵn/lẻ
  3. Trong mỗi tác vụ, nó tính toán trước kết quả kì vọng expectedAuthMethod (“OTP” cho user chẵn, “BIOMETRIC” cho user lẻ).
  4. Sau đó, nó gọi phương thức processPayment và so sánh kết quả thực tế (actualAuthMethod) với kết quả kỳ vọng xem có khớp nhau hay không
  5. Nếu có sai lệch, nó sẽ in ra lỗi và tăng biến đếm lỗi errorCount

Giờ mình sẽ chạy thử 4 lần để xem kết quả của 4 lần sẽ như thế nào nhé:

Kết quả chạy thử lần 1
Kết quả chạy thử lần 2
Kết quả chạy thử lần 3
Kết quả chạy thử lần 4

Trong cả 4 lần chạy, thì cả 4 lần đều có 10/20 request bị sai kết quả đầu ra, user lẻ phải nhận được BIOMETRIC thì lại nhận được OTP và user chẵn đáng lẽ phải nhận được OTP thì lại nhận được BIOMETRIC.

Phân tích lỗi

Các bạn có thể thấy trong đoạn code ban đầu, class PaymentService được đánh dấu @Service, bean mặc định sẽ là singleton, nghĩa là sẽ chỉ có một đối tượng duy nhất của class này được tạo ra và dùng chung cho tất cả các request (mỗi request được xử lý bởi một luồng riêng).

Tiếp tục đến biến mà chúng ta đã khai báo, biến authMethod được khai báo là biến của đối tượng (instance variables).

private String authMethod = null;

Mỗi khi chúng ta tạo một đối tượng (instance) mới của class bằng từ khóa new, một bản sao riêng của các biến này sẽ được tạo ra cho đối tượng đó.

Nếu có 2 đối tượng paymentService1 và paymentService2, thì paymentService1.authMethod và paymentService2.authMethod là hai biến hoàn toàn độc lập, nằm ở hai vùng nhớ khác nhau trong Heap.

Tuy nhiên Vấn đề là ở đây là: chúng ta chỉ có một đối tượng duy nhất (singleton) của paymentService, nên nó sẽ được dùng chung cho nhiều luồng. Do đó, tất cả các luồng đều truy cập và thay đổi cùng một bản sao của authMethod thuộc về đối tượng duy nhất là paymentService.

Lúc này kịch bản gây ra race condition sẽ diễn ra như sau:

Thời gianThread A (Request A)Thread B (Request B)Giá trị của this.authMethod
T1Gọi processPayment()null
T2Gọi getAuthenticationMethod() và gán this.authMethod = “OTP”“OTP”
T3Bị ngắt, làm tác vụ khác,…Gọi processPayment()“OTP”
T4Reset và gán this.authMethod = “BIOMETRIC”“BIOMETRIC”
T5Hoàn thành và trả về kết quả chứa authMethod = “BIOMETRIC”“BIOMETRIC”
T6Được chạy tiếp“BIOMETRIC”
T7Đến bước tạo response, nó đọc this.authMethod“BIOMETRIC”
T8Trả về kết quả cho Request A với authMethod = “BIOMETRIC”Lẽ ra phải là “OTP” thì lại nhận được là “BIOMETRIC”

Hậu quả xảy ra: Dữ liệu của request này bị ghi đè bởi request khác, dẫn đến sai lệch thông tin nghiêm trọng, phương thức xác thực cuối cùng bị sai, ảnh hưởng đến luồng logic chính.

Các bạn có thể xem thêm bài viết về Bộ nhớ Heap và bộ nhớ Stack trong Java để hiểu thêm cách thức lưu trữ trong bộ nhớ Heap và Stack nhé.

Phương án xử lý

Ở bài viết về bộ nhớ Heap và Stack kia của mình, có một đoạn như sau:

Bộ nhớ Stack dùng để lưu các biến cục bộ trong hàm và lời gọi hàm ở runtime trong một thread java, theo từng luồng riêng biệt nhau và không được sử dụng lẫn nhau giữa các luồng khác nhau

stack memory

Để đảm bảo việc tách biệt dữ liệu và không chia sẻ dữ liệu của authMethod giữa các Thread, các bạn cần phải đưa biến instance (authMethod) trở thành biến cục bộ của hàm.

Khi đó mỗi Thread A, Thread B, Thread C,…. khi gọi vào hàm processPayment sẽ lưu trữ dữ liệu ở từng khối bộ nhớ khác nhau trong Stack, tách biệt và không chung đụng, việc khởi tạo bộ nhớ sẽ tuân theo quy tắc LIFO (Last In First Out, đại khái là như kiểu là 1 cái hộp, bạn đẩy vào đó nhiều đĩa bánh xếp chồng nhau, thì cái đĩa xếp vào sau sẽ được lấy ra trước, vậy thôi).

Oke giờ mình sẽ chỉnh sửa lại code một chút để đáp ứng, clone class cũ ra và sửa lại logic trên class SafePaymentService nhé:

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

/**
 * hungtv27
 * cafeincode.com
 */

@Service
@Slf4j
public class SafePaymentService {

    // Không còn biến instance lưu trạng thái của request

    public Map<String, Object> processPayment(long userId) throws InterruptedException {

        // Bước 1: biến local
        String authMethod;

        // Bước 2: Xác định phương thức xác thực
        authMethod = getAuthenticationMethod(userId);
        TimeUnit.MILLISECONDS.sleep(275);

        // Bước 3: Tạo và trả về kết quả
        Map<String, Object> response = new HashMap<>();
        response.put("status", "SUCCESS");
        response.put("authMethod", authMethod);

        return response;
    }

    private String getAuthenticationMethod(long userId) {
        return (userId % 2 == 0) ? "OTP" : "BIOMETRIC";
    }
}

Sau đó sửa thêm một chút class ConcurrencyTest để chạy verify lại việc sửa ở trên

import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class ConcurrencyTest {

    public static void main(String[] args) throws InterruptedException {
        var requestId = UUID.randomUUID().toString();
        System.out.println("cafeincode-Start checking SafePaymentService with requestID: " + requestId);
        concurrencyCalculator(new SafePaymentService());
    }


    private static void concurrencyCalculator(Object serviceInstance) throws InterruptedException {
        int numberOfThreads = 20;
        ExecutorService executor = Executors.newFixedThreadPool(numberOfThreads);
        AtomicInteger errorCount = new AtomicInteger(0);

        for (int i = 0; i < numberOfThreads; i++) {
            final int userId = 100 + i; // Mỗi luồng một user ID khác nhau

            executor.submit(() -> {
                try {
                    var expectedAuthMethod = (userId % 2 == 0) ? "OTP" : "BIOMETRIC";

                    Map<String, Object> result;
                    if (serviceInstance instanceof PaymentService) {
                        result = ((PaymentService) serviceInstance).processPayment(userId);
                    } else {
                        result = ((SafePaymentService) serviceInstance).processPayment(userId);
                    }

                    var actualAuthMethod = (String) result.get("authMethod");

                    if (!expectedAuthMethod.equals(actualAuthMethod)) {
                        errorCount.incrementAndGet();
                        System.err.printf("[cafeincode-INCORRECT!] User %d: Expected authMethod='%s' but got '%s'%n",
                            userId, expectedAuthMethod, actualAuthMethod);
                    } else {
                        System.out.printf("[cafeincode-CORRECT] User %d: authMethod='%s' as expected %n",
                            userId, actualAuthMethod);
                    }
                } catch (Exception ex) {
                    log.error("[cafeincode-INCORRECT!] Error occurred while processing user {}: {}", userId,
                        ex.getMessage(), ex);
                }
            });
        }

        executor.shutdown();
        executor.awaitTermination(5, TimeUnit.SECONDS);
        System.out.printf("=> SUMMARY: %d/%d requests returned incorrect results.%n", errorCount.get(),
            numberOfThreads);
    }
}

Sau đó chạy thử xem kết quả đã chính xác chưa:

Chạy thử lần 1 sau khi sửa
Chạy thử lần 2 sau khi sửa

Kết quả đã hoàn toàn chính xác và không còn bị data race nữa rồi, giờ ngồi phân tích thêm một chút xem nếu đoạn code ban đầu không phải là biến instance mà là biến static thì sẽ thế nào nhỉ?

Giờ nếu thay biến instance bằng static?

// Dont do it, hehe
private static String authMethod = null;

Biến static: Là biến chỉ có một bản sao duy nhất tồn tại cho toàn bộ lớp trong toàn bộ máy ảo Java (JVM), bất kể có bao nhiêu đối tượng được tạo.

Nếu dùng biến static ở đây, hiện tượng race condition vẫn sẽ xảy ra y hệt, nhưng phạm vi ảnh hưởng của nó không còn giới hạn trên một đối tượng singleton nữa, mà là toàn bộ ứng dụng.

Tình hình sẽ trở nên nghiêm trọng hơn biến instance rất nhiều, bất kỳ đoạn code nào, ở bất kỳ đâu, nếu tương tác với các biến static này đều sẽ tham gia vào tranh chấp tài nguyên, gây ra một mớ hỗn độn dữ liệu trên quy mô toàn cục.

Nói có sách mách có chứng, giờ mình sẽ thay đổi code một chút và chạy lại để xem hậu quả của việc sử dụng static trong trường hợp này nhé:

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.springframework.stereotype.Service;

/**
 * hungtv27
 * cafeincode.com
 */

@Service
public class PaymentService {

    //Dont do it :((
    private static String authMethod = null;


    public Map<String, Object> processPayment(long userId) throws InterruptedException {

        // Bước 1: Khởi tạo lại trạng thái cho request hiện tại
        authMethod = null;

        // Bước 2: Xác định phương thức xác thực
        authMethod = getAuthenticationMethod(userId);
        TimeUnit.MILLISECONDS.sleep(275);

        // Bước 3: Tạo và trả về kết quả
        Map<String, Object> response = new HashMap<>();
        response.put("status", "SUCCESS");
        response.put("authMethod", authMethod);

        return response;
    }


    private String getAuthenticationMethod(long userId) {
        return (userId % 2 == 0) ? "OTP" : "BIOMETRIC";
    }
}
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class ConcurrencyTest {

    public static void main(String[] args) throws InterruptedException {
        var requestId = UUID.randomUUID().toString();
        System.out.println("cafeincode-Start checking concurrency issue PaymentService with requestID: " + requestId);
        concurrencyCalculator(new PaymentService());

    }


    private static void concurrencyCalculator(Object serviceInstance) throws InterruptedException {
        int numberOfThreads = 20;
        ExecutorService executor = Executors.newFixedThreadPool(numberOfThreads);
        AtomicInteger errorCount = new AtomicInteger(0);

        for (int i = 0; i < numberOfThreads; i++) {
            final int userId = 100 + i; // Mỗi luồng một user ID khác nhau

            executor.submit(() -> {
                try {
                    var expectedAuthMethod = (userId % 2 == 0) ? "OTP" : "BIOMETRIC";

                    Map<String, Object> result;
                    if (serviceInstance instanceof PaymentService) {
                        result = ((PaymentService) serviceInstance).processPayment(userId);
                    } else {
                        result = ((SafePaymentService) serviceInstance).processPayment(userId);
                    }

                    var actualAuthMethod = (String) result.get("authMethod");

                    if (!expectedAuthMethod.equals(actualAuthMethod)) {
                        errorCount.incrementAndGet();
                        System.err.printf("[cafeincode-INCORRECT!] User %d: Expected authMethod='%s' but got '%s'%n",
                            userId, expectedAuthMethod, actualAuthMethod);
                    } else {
                        System.out.printf("[cafeincode-CORRECT] User %d: authMethod='%s' as expected %n",
                            userId, actualAuthMethod);
                    }
                } catch (Exception ex) {
                    log.error("[cafeincode-INCORRECT!] Error occurred while processing user {}: {}", userId,
                        ex.getMessage(), ex);
                }
            });
        }

        executor.shutdown();
        executor.awaitTermination(5, TimeUnit.SECONDS);
        System.out.printf("=> SUMMARY: %d/%d requests returned incorrect results.%n", errorCount.get(),
            numberOfThreads);
    }
}
Result

Bài học rút ra

Phân biệt các loại biến

Trước khi rút ra bài học cuối cùng thì mình tạo thêm một bảng so sánh về các loại biến chính trong Java, đặc biệt là nơi chúng được lưu trữ để từ đó hiểu và có thể tránh được những sai lầm đáng tiếc sau này.

Tên biếnĐặc điểmVị trí lưu trữVòng đờiMức độ an toàn luồng
Biến Instancenằm ở bên trong class, ngoài method, không có staticHeap (chia sẻ giữa các thread)gắn với đối tượngkhông an toàn nếu đối tượng được chia sẻ
Biến Staticnằm ở bên trong class, ngoài method và có staticHeap/Metaspace (chia sẻ toàn cục)gắn với lớprất không an toàn nếu biến có thể thay đổi
Biến Cục Bộnằm bên trong methodStack (riêng của từng thread)gắn với lời gọi hàm, xem lại phần stack nhéAn toàn cho đa luồng

Quy tắc vàng

Bài viết này không phải là bài viết chính về việc nên làm gì và không nên làm gì trong đa luồng, vì mình thấy nên đề cập ở bài viết khác sẽ chi tiết hơn, chỉ là tiện thể mình tóm gọn lại một số quy tắc để mọi người cùng nắm, cùng hiểu và có cách phòng tránh, coding hợp lý.

Quy tắc 1: Giữ cho các Service/Component dùng chung phải “Stateless”

Không bao giờ lưu trữ trạng thái dành riêng cho một request trong các biến instance hoặc biến static của các đối tượng được dùng chung (như Singleton Beans)

Luôn khai báo những biến này là biến cục bộ (local variables) bên trong các phương thức, trường hợp nếu cần truyền dữ liệu giữa các phương thức, hãy truyền qua tham số.

Quy tắc 2: Hiểu rõ khi nào được phép sử dụng biến Instance và Static

Tất nhiên trong những dự án làm việc hằng ngày, chúng ta không bao giờ có thể “cấm” hoàn toàn việc sử dụng biến instance và static, điều đó là không thể, vấn đề nằm ở chỗ chúng ta phải hiểu để sử dụng chúng đúng mục đích.

Chỉ sử dụng biến instance/static để lưu trữ trạng thái không thay đổi (immutable) hoặc các tài nguyên được cấu hình một lần và dùng chung (shared, read-only dependencies)

Một số ví dụ dùng static/instance đúng mục đích:

  • Cấu hình (Configuration): rất hợp lý khi dùng để lưu các giá trị từ file properties/yaml bằng @Value, những giá trị này được nạp một lần khi khởi động và chỉ để dùng để đọc thôi
  • Ví dụ: private final RestClient restClient; private final String cafeincodeKey;
  • Dùng public static final cho các giá trị không bao giờ thay đổi
  • Đối tượng logger thường được khai báo là private static final Logger log = … để tiết kiệm bộ nhớ, vì chỉ cần một logger cho cả lớp
  • Đôi khi các đối tượng thread-safe như ObjectMapper cũng có thể được khai báo static final nếu chúng không có cấu hình đặc biệt và có thể dùng chung toàn cục

Quy tắc 3: Nếu một đối tượng không thay đổi, nó mặc định là thread-safe

Quy tắc 4: Thận trọng với các Collection dùng chung

Nếu các bạn bắt buộc phải có một collection (ví dụ: một Map để cache dữ liệu) được chia sẻ và thay đổi bởi nhiều luồng, hãy sử dụng các phiên bản an toàn cho luồng, tuy nhiên sẽ phải đánh đổi một chút hiệu năng.

  • Ví dụ:
    • HashMap -> ConcurrentHashMap
    • ArrayList -> CopyOnWriteArrayList
    • HashSet -> ConcurrentHashMap.newKeySet()

Oke trường hợp cả đọc và ghi thì cần phải lưu tâm như trên, nhưng nếu trong trường hợp đa luồng mà các luồng chỉ đọc, thì việc dùng HashMap là ổn, ví dụ bên dưới có dùng một Map để cache lại danh sách T&C và được dùng chung cho nhiều luồng. (nhớ là Map này chỉ đọc, không được ghi).

Quy tắc 5: Sử dụng đồng bộ hoá ở phạm vi nhỏ nhất có thể: Synchronize, Lock,….

Khi sử dụng synchronized hoặc các loại khóa (Lock), các bạn cần phải giới hạn khối được đồng bộ hóa ở phạm vi nhỏ nhất có thể và chỉ đồng bộ hoá những phần cần thiết, để tránh gây ra tắc nghẽn

Using mutex object
synchronized with object

Bài viết hôm nay tới đây là kết thúc rồi, chúc các bạn học tập và làm việc thật tốt, ngoài ra hãy xem thêm một số bài viết nổi bật bên dưới:

Leave a Reply

Your email address will not be published. Required fields are marked *