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.
Mục Lục
- 1 Race condition là gì?
- 2 Hiện trạng của đoạn code “gây lỗi”
- 3 Phân tích lỗi
- 4 Phương án xử lý
- 5 Giờ nếu thay biến instance bằng static?
- 6 Bài học rút ra
- 6.1 Phân biệt các loại biến
- 6.2 Quy tắc vàng
- 6.2.1 Quy tắc 1: Giữ cho các Service/Component dùng chung phải “Stateless”
- 6.2.2 Quy tắc 2: Hiểu rõ khi nào được phép sử dụng biến Instance và Static
- 6.2.3 Quy tắc 3: Nếu một đối tượng không thay đổi, nó mặc định là thread-safe
- 6.2.4 Quy tắc 4: Thận trọng với các Collection dùng chung
- 6.2.5 Quy tắc 5: Sử dụng đồng bộ hoá ở phạm vi nhỏ nhất có thể: Synchronize, Lock,….
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à
và 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ự:
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 11QC 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- 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:
- QC
Ngà
và QCHuệ
cùng lúc đọc tổng số mục hiện tại là 10 - 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
- Sau đó
QC Huệ
nghe điện thoại,QC Ngà
tiếp tục làm việc 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- 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é:
- Đầ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
- Biến autheMethod được khai báo là biến instance của lớp
- 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
- Đầu tiên sẽ set lại giá trị authMethod về null
- Thực hiện tính toán giá trị cho authMethod dựa theo userId
- Sau đó put ngược kết quả vào Map kết quả
- Trả về Map dữ liệu
- 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:
ConcurrencyTest
tạo ra một ExecutorService với 20 luồng- 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ẻ
- 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ẻ). - 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
- 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é:




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 gian | Thread A (Request A) | Thread B (Request B) | Giá trị của this.authMethod |
---|---|---|---|
T1 | Gọi processPayment() | null | |
T2 | Gọi getAuthenticationMethod() và gán this.authMethod = “OTP” | “OTP” | |
T3 | Bị ngắt, làm tác vụ khác,… | Gọi processPayment() | “OTP” |
T4 | Reset và gán this.authMethod = “BIOMETRIC” | “BIOMETRIC” | |
T5 | Hoà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” | |
T8 | Trả 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:


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);
}
}

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ểm | Vị trí lưu trữ | Vòng đời | Mức độ an toàn luồng |
---|---|---|---|---|
Biến Instance | nằm ở bên trong class, ngoài method, không có static | Heap (chia sẻ giữa các thread) | gắn với đối tượng | không an toàn nếu đối tượng được chia sẻ |
Biến Static | nằm ở bên trong class, ngoài method và có static | Heap/Metaspace (chia sẻ toàn cục) | gắn với lớp | rất không an toàn nếu biến có thể thay đổi |
Biến Cục Bộ | nằm bên trong method | Stack (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ếnstatic
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


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:
- Tổng hợp dữ liệu cho 1 triệu tài khoản trong 4 phút
- Xử lý lỗi lệch múi giờ khi dùng Mapstruct
- Có CodeRabbit làm tôi review code cũng nhàn hơn
- Lỗi trên môi trường Staging mà không ai chịu sửa đến cùng
- Filter Spring và pha xử lý bug nhớ đời
- Những thói quen tốt trong ngành phát triển phần mềm
- Viết code với những thói quen tốt này sẽ giúp bạn giỏi hơn
- Kĩ năng quản lý căng thẳng cho Developer
- Làm việc trong môi trường Agile là như thế nào
- Là kĩ sư phần mềm hãy cố gắng giữ gìn sức khỏe bản thân
- Bạn không giỏi lắng nghe như bạn nghĩ đâu
- Rest API: Cách Ngăn Chặn Duplicate Request Hiệu Quả
- Sức mạnh của AI CLI – trợ thủ đắc lực cho dân lập trình