Nói về ThreadLocal huyền thoại - tin tức bóng đá

Thực ra, mình cảm thấy hơi xấu hổ khi nói rằng mình chỉ hiểu một cách mơ hồ về ThreadLocal. Sau khi tìm hiểu kỹ hơn, mình nhận ra rằng mình đã hiểu sai hoàn toàn. Trong trí nhớ của mình, ThreadLocal hoạt động bằng cách sử dụng thread làm khóa (key) để lưu trữ nội dung liên quan đến luồng. Nhưng điều này hoàn toàn sai lầm. Cách tiếp cận đúng đắn nên sử dụng concurrentHashMap để lưu trữ và đồng bộ hóa các luồng, nhưng với cách hiểu sai lầm thì mọi thứ trở nên hỗn loạn.

Cấu trúc thực sự là gì?

Chẳng hạn, khi chúng ta khởi tạo một đối tượng ThreadLocal trong mã nguồn:

 1public static void main(String[] args) {
 2    ThreadLocal<Man> tl = new ThreadLocal<>();
 3    new Thread(() -> {
 4        try {
 5            TimeUnit.SECONDS.sleep(2);
 6        } catch (InterruptedException e) {
 7            e.printStackTrace();
 8        }
 9        System.out.println(tl.get());
10    }).start();
11    new Thread(() -> {
12        try {
13            TimeUnit.SECONDS.sleep(1);
14        } catch (InterruptedException e) {
15            e.printStackTrace();
16        }
17        tl.set(new Man());
18    }).start();
19}
20static class Man {
21    String name = "nick";
22}

Trong ví dụ trên, hai luồng được khởi tạo: một luồng thiết lập giá trị và một luồng khác truy xuất giá trị sau đó. Khi chạy chương trình, kết quả sẽ cho thấy rằng luồng không thể lấy được giá trị mà luồng kia đã thiết lập. Điều này dẫn chúng ta đến việc phân tích phương thức set.

Phương thức set hoạt động như sau:

1public void set(T value) {
2    Thread t = Thread.currentThread();
3    ThreadLocalMap map = getMap(t);
4    if (map != null)
5        map.set(this, value);
6    else
7        createMap(t, value);
8}

Ở đây, dòng đầu tiên Thread t = Thread.currentThread(); lấy ra luồng hiện tại. Dòng tiếp theo gọi phương thức getMap(t):

1ThreadLocalMap getMap(Thread t) {
2    return t.threadLocals;
3}

Phương thức này trả về biến thành viên threadLocals của luồng hiện tại. Vậy threadLocals là gì? Đây là một biến thành viên thuộc kiểu java.lang.ThreadLocal.ThreadLocalMap trong lớp Thread. Hãy xem cấu trúc của ThreadLocalMap:

 1static class ThreadLocalMap {
 2    static class Entry extends WeakReference<ThreadLocal<?>> {
 3        Object value;
 4
 5        Entry(ThreadLocal<?> k, Object v) {
 6            super(k);
 7            value = v;
 8        } [tin tức bóng đá](/post/1d57f9eec4b7da3c.html) 
 9    }
10}

Cấu trúc bên trong ThreadLocalMap bao gồm các phần tử Entry, mỗi phần tử chứa một tham chiếu yếu (weak reference) đến đối tượng ThreadLocal làm khóa và giá trị tương ứng.

Quay lại quá trình của phương thức set, nếu bản đồ (map) tồn tại, nó sẽ thêm hoặc cập nhật giá trị với khóa là chính đối tượng ThreadLocal hiện tại. Nếu bản đồ chưa tồn tại, nó sẽ tạo mới bản đồ này.

Mỗi luồng duy trì riêng một ThreadLocalMap như một biến thành viên của chính nó. Khi thiết lập giá trị thông qua ThreadLocal, thực chất là đang thêm vào bản đồ này với khóa là chính đối tượng ThreadLocal.

Rò rỉ bộ nhớ là chuyện gì vậy?

Để hiểu rõ hơn về vấn đề rò rỉ bộ nhớ, chúng ta cần nhìn lại cấu trúc của ThreadLocalMap. Mỗi phần tử trong bản đồ là một đối tượng Entry, và khóa của nó là một tham chiếu yếu đến đối tượng ThreadLocal. Phương thức set hoạt động như sau:

 1private void set(ThreadLocal<?> key, Object value) {
 2    Entry[] tab = table;
 3    int len = tab.length;
 4    int i = key.threadLocalHashCode & (len-1);
 5
 6    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
 7        ThreadLocal<?> k = e.get();
 8
 9        if (k == key) {
10            e.value = value;
11            return;
12        }
13
14        if (k == null) {
15            replaceStaleEntry(key, value, i);
16            return;
17        }
18    }
19
20    tab[i] = new Entry(key, value);
21    int sz = ++size;
22
23    if (!cleanSomeSlots(i, sz) && sz >= threshold)
24        rehash();
25}

Lý do sử dụng tham chiếu yếu (WeakReference) cho khóa là để đảm bảo rằng khi đối tượng ThreadLocal bị giải phóng khỏi bộ nhớ (được đặt thành null), khóa có thể được thu gom rác (garbage collected). Tuy nhiên, vấn đề nằm ở giá trị (value). Nếu đối tượng ThreadLocal đã bị hủy nhưng giá trị vẫn còn tham chiếu mạnh, nó sẽ không thể được thu gom rác, dẫn đến rò rỉ bộ nhớ.

Do đó, tốt nhất là luôn gọi phương thức remove() sau khi sử dụng xong ThreadLocal để đảm bảo cả khóa và giá trị đều được giải phóng đúng cách.

Hy vọng bài viết này giúp bạn hiểu rõ hơn về cơ chế hoạt động cũng như các nguy cơ tiềm ẩn khi sử dụng ThreadLocal.