Thực Hành An Toàn Đa Luồng: Công Cụ và Cơ Chế Khóa Trong Java

Các Phương Pháp Sử Dụng Container Đảm Bảo Tính Liên Thùng

Hệ sinh thái Java cung cấp nhiều cấu trúc dữ liệu hỗ trợ đa luồng, nhưng việc áp dụng sai cách vẫn có thể dẫn đến lỗi logic hoặc hiệu năng kém.

Vận Dụng Hiệu Quả ConcurrentHashMap

Xét tình huống xây dựng hệ thống quản lý phiên làm việc, chúng ta cần lưu trữ ánh xạ giữa ID khách hàng và danh sách trạng thái kết nối:

// Triển khai tiềm ẩn rủi ro
ConcurrentHashMap<String, List<ConnectionInfo>> activeConnections = new ConcurrentHashMap<>();

public void updateConnectionStatus(String clientId, ConnectionInfo info) {
    List<ConnectionInfo> connections = activeConnections.get(clientId);
    if (connections == null) { 
        // Kiểm tra lần đầu không đủ an toàn nếu nhiều luồng cùng thực hiện
        connections = new ArrayList<>();
        activeConnections.put(clientId, connections); // Có khả năng ghi đè giá trị của luồng khác
    }
    connections.add(info); // Danh sách ArrayList cơ bản không bảo vệ tính nguyên khối
}

Bài toán trên gặp hai vấn đề cốt lõi:

  1. Thao tác kiểm tra rồi chèn (check-then-act) không mang tính nguyên tử.
  2. Kiểu dữ liệu ArrayList mặc định không hỗ trợ đồng bộ hóa.

Giải pháp tối ưu sử dụng các phương thức tích hợp sẵn:

ConcurrentHashMap<String, List<ConnectionInfo>> activeConnections = new ConcurrentHashMap<>();

public void updateConnectionStatus(String clientId, ConnectionInfo info) {
    activeConnections.compute(clientId, (key, currentList) -> {
        List<ConnectionInfo> list = currentList;
        if (list == null) {
            // Khởi tạo danh sách có bảo vệ luồng
            list = Collections.synchronizedList(new ArrayList<>());
        }
        list.add(info);
        return list;
    });
}

Sử dụng phương thức compute giúp đảm bảo thao tác đóng gói thành một khối nguyên tử. Đồng thời, bao bọc danh sách bằng synchronizedList để đảm bảo an toàn khi truy xuất.

Lưu ý Khi Duyệt Và Biến Đổi集合

Một sai lầm phổ biến là cố gắng xóa phần tử trong quá trình lặp lại mà không tuân theo quy tắc chuẩn.

// Ví dụ không an toàn
List<String> items = Collections.synchronizedList(new ArrayList<>());
// ... nạp dữ liệu ...
for (String item : list) {
    if (isValid(item)) {
        list.remove(item); // Sẽ gây ra lỗi ConcurrentModificationException
    }
}

Cần áp dụng một trong hai chiến lược sau để xử lý đúng:

// Giải pháp 1: Tận dụng đối tượng Iterator
List<String> items = Collections.synchronizedList(new ArrayList<>());
Iterator<String> itr = items.iterator();
while (itr.hasNext()) {
    String element = itr.next();
    if (!needsRetention(element)) {
        itr.remove(); // Đây là cách loại bỏ chính thống
    }
}

// Giải pháp 2: Sử dụng CopyOnWriteArrayList cho cấu hình tĩnh
List<String> items = new CopyOnWriteArrayList<>();
for (String element : items) {
    if (!needsRetention(element)) {
        items.remove(element); // Loại trừ an toàn nhờ cơ chế copy-on-write
    }
}

Nguyên Lý Hoạt Động Và Rủi Ro Của ThreadLocal

Khác với các cơ chế khóa chia sẻ, ThreadLocal tiếp cận vấn đề bằng cách cô lập dữ liệu: mỗi luồng sở hữu một bản sao riêng biệt.

Cơ Chế Cung Cấp Context

public class AuthTokenHolder {
    private static final ThreadLocal<AuthenticationToken> tokenStorage = new ThreadLocal<>();
    
    public static void injectToken(AuthenticationToken token) {
        tokenStorage.set(token);
    }
    
    public static AuthenticationToken fetchToken() {
        return tokenStorage.get();
    }
    
    public static void clearState() {
        tokenStorage.remove(); // Bắt buộc phải gọi để giải phóng tài nguyên
    }
}

Phát Hiện Rò Rỉ Bộ Nhớ Trong Môi Trường Server

Trong môi trường web, bộ lọc (Filter) thường được dùng để gắn dữ liệu vào luồng. Nếu quên dọn dẹp, ThreadLocalMap bên trong đối tượng luồng sẽ giữ tham chiếu lâu dài.

@WebFilter("/*")
public class SecurityChainFilter implements Filter {
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) 
            throws IOException, ServletException {
        try {
            // Gán thông tin xác thực
            AuthTokenHolder.injectToken(parseToken(req));
            chain.doFilter(req, res);
        } finally {
            // Cốt lõi: Luôn đảm bảo dọn dẹp trong khối finally
            AuthTokenHolder.clearState();
        }
    }
}

Nếu không thực hiện bước remove(), các giá trị cũ sẽ nằm đọng trong bộ nhớ luồng. Đặc biệt nguy hiểm khi sử dụng Thread Pool, vì luồng được tái sử dụng nhưng dữ liệu cũ vẫn còn đó.

Hiệu Suất Với Nguyên Tử Và Thao Tác CAS

Các lớp như AtomicInteger hay AtomicLong tận dụng kỹ thuật Compare-And-Swap (CAS) để đạt hiệu suất cao hơn so với cơ chế khóa nặng truyền thống.

// Cách tiếp cận cũ dựa trên synchronized
public class ManualThrottler {
    private int limitCount = 0;
    
    public synchronized void increase() {
        limitCount++;
    }
    
    public synchronized int getLimit() {
        return limitCount;
    }
}

// Cách tiếp cận tối ưu với Atomic
public class HighPerfThrottler {
    private final AtomicInteger limitCount = new AtomicInteger(0);
    
    public void increase() {
        limitCount.incrementAndGet();
    }
    
    public int getLimit() {
        return limitCount.get();
    }
}

Dù hiệu năng vượt trội ở tải trọng lớn, các biến nguyên tử vẫn tồn tại thách thức như vấn đề ABA hoặc tiêu tốn CPU do tự quay vòng (spinlock) trong trường hợp xung đột cực cao.

Chiến Lược Khóa Nâng Cao: Đọc Ghi và StampedLock

Ứng Dụng ReadWriteLock

Đối với dữ liệu có tần suất đọc nhiều nhưng viết ít, việc tách biệt khóa đọc và khóa viết giúp tăng độ song song.

public class DocumentIndexer {
    private final Map<String, Object> docs = new HashMap<>();
    private final ReentrantReadWriteLock lockObj = new ReentrantReadWriteLock();
    
    public Object retrieveDoc(String id) {
        lockObj.readLock().lock();
        try {
            return docs.get(id);
        } finally {
            lockObj.readLock().unlock();
        }
    }
    
    public void storeDoc(String id, Object data) {
        lockObj.writeLock().lock();
        try {
            docs.put(id, data);
        } finally {
            lockObj.writeLock().unlock();
        }
    }
}

Tối Ưu Bằng StampedLock

JDK 8 giới thiệu StampedLock bổ sung cơ chế đọc lạc quan (optimistic read), giúp giảm xung đột khi không có luồng nào đang ghi dữ liệu.

public class OptimizedConfigLoader {
    private final Map<String, Object> configMap = new HashMap<>();
    private final StampedLock accessLock = new StampedLock();
    
    public Object loadConfig(String key) {
        long stamp = accessLock.tryOptimisticRead();
        Object value = configMap.get(key);
        
        // Xác minh xem có thay đổi nào xảy ra trong lúc đọc
        if (!accessLock.validate(stamp)) {
            // Fallback sang đọc chặn nếu không ổn định
            stamp = accessLock.readLock();
            try {
                value = configMap.get(key);
            } finally {
                accessLock.unlockRead(stamp);
            }
        }
        return value;
    }
    
    public void saveConfig(String key, Object data) {
        long stamp = accessLock.writeLock();
        try {
            configMap.put(key, data);
        } finally {
            accessLock.unlockWrite(stamp);
        }
    }
}

Phương thức này mang lại lợi thế hiệu năng đáng kể trong các kịch bản chủ yếu là đọc và hiếm khi bị gián đoạn bởi thao tác ghi.

Thẻ: Java Concurrency multithreading concurrentcollection threadlocal

Đăng vào ngày 22 tháng 6 lúc 02:00