ThreadLocal của Java được triển khai như thế nào?


81

ThreadLocal được triển khai như thế nào? Nó được triển khai bằng Java (sử dụng một số bản đồ đồng thời từ ThreadID đến đối tượng) hay nó sử dụng một số móc nối JVM để làm việc đó hiệu quả hơn?

Câu trả lời:


120

Tất cả các câu trả lời ở đây đều đúng, nhưng hơi đáng thất vọng vì chúng hơi che lấp cách ThreadLocaltriển khai thông minh của nó. Tôi chỉ đang xem mã nguồnThreadLocal và rất ấn tượng về cách nó được triển khai.

Thực hiện Naive

Nếu tôi yêu cầu bạn triển khai một ThreadLocal<T>lớp được cung cấp bởi API được mô tả trong javadoc, bạn sẽ làm gì? Một triển khai ban đầu có thể sẽ là ConcurrentHashMap<Thread,T>sử dụng Thread.currentThread()làm khóa của nó. Điều này sẽ hoạt động hợp lý nhưng có một số nhược điểm.

  • Chủ đề tranh chấp - ConcurrentHashMaplà một lớp khá thông minh, nhưng cuối cùng nó vẫn phải đối phó với việc ngăn chặn nhiều luồng liên kết với nó theo bất kỳ cách nào và nếu các luồng khác nhau thường xuyên đánh nó, sẽ có hiện tượng chậm.
  • Giữ vĩnh viễn một con trỏ tới cả Thread và đối tượng, ngay cả sau khi Thread đã kết thúc và có thể được GC'ed.

Triển khai thân thiện với GC

Ok, hãy thử lại, hãy giải quyết vấn đề thu gom rác bằng cách sử dụng các tham chiếu yếu . Xử lý các tài liệu tham khảo yếu có thể khó hiểu, nhưng chỉ cần sử dụng một bản đồ được xây dựng như vậy là đủ:

 Collections.synchronizedMap(new WeakHashMap<Thread, T>())

Hoặc nếu chúng ta đang sử dụng Guava (và chúng ta nên làm như vậy!):

new MapMaker().weakKeys().makeMap()

Điều này có nghĩa là một khi không ai khác đang nắm giữ Chủ đề (ngụ ý rằng nó đã kết thúc) thì khóa / giá trị có thể được thu thập rác, đây là một cải tiến, nhưng vẫn không giải quyết được vấn đề tranh chấp chủ đề, có nghĩa là cho đến nay chúng ta ThreadLocalkhông phải là tất cả tuyệt vời của một lớp học. Hơn nữa, nếu ai đó quyết định giữ Threadcác đồ vật sau khi chúng hoàn thành, chúng sẽ không bao giờ bị GC'ed, và do đó các đồ vật của chúng ta cũng vậy, mặc dù về mặt kỹ thuật chúng không thể tiếp cận được.

Triển khai Thông minh

Chúng tôi đã từng nghĩ về việc ThreadLocalánh xạ các chuỗi tới các giá trị, nhưng có lẽ đó không thực sự là cách đúng để nghĩ về nó. Thay vì nghĩ về nó như một ánh xạ từ Threads đến các giá trị trong mỗi đối tượng ThreadLocal, điều gì sẽ xảy ra nếu chúng ta nghĩ về nó như một ánh xạ các đối tượng ThreadLocal đến các giá trị trong mỗi Thread ? Nếu mỗi luồng lưu trữ ánh xạ và ThreadLocal chỉ cung cấp một giao diện đẹp cho ánh xạ đó, chúng ta có thể tránh tất cả các vấn đề của các lần triển khai trước đó.

Một triển khai sẽ trông giống như sau:

// called for each thread, and updated by the ThreadLocal instance
new WeakHashMap<ThreadLocal,T>()

Không cần phải lo lắng về tính đồng thời ở đây, vì chỉ có một luồng sẽ truy cập vào bản đồ này.

Các nhà phát triển Java có lợi thế lớn hơn chúng tôi ở đây - họ có thể trực tiếp phát triển lớp Thread và thêm các trường cũng như hoạt động vào nó, và đó chính xác là những gì họ đã làm.

Trong java.lang.Threadđó có những dòng sau:

/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

Như nhận xét gợi ý thực sự là một ánh xạ gói-riêng của tất cả các giá trị đang được theo dõi bởi ThreadLocalcác đối tượng cho việc này Thread. Việc thực hiện ThreadLocalMapkhông phải là a WeakHashMap, nhưng nó tuân theo cùng một hợp đồng cơ bản, bao gồm việc nắm giữ các khóa của nó bằng cách tham chiếu yếu.

ThreadLocal.get() sau đó được thực hiện như vậy:

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

ThreadLocal.setInitialValue()như vậy:

private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

Về cơ bản, sử dụng một bản đồ trong Chủ đề này để chứa tất cả các ThreadLocalđối tượng của chúng ta . Bằng cách này, chúng ta không bao giờ cần phải lo lắng về các giá trị trong các Luồng khác (theo ThreadLocalnghĩa đen chỉ có thể truy cập các giá trị trong Luồng hiện tại) và do đó không có vấn đề về đồng thời. Hơn nữa, sau khi Threadhoàn thành, bản đồ của nó sẽ tự động được GC'ed và tất cả các đối tượng cục bộ sẽ được dọn dẹp. Ngay cả khi Threadđược giữ lại, các ThreadLocalđối tượng được giữ bằng tham chiếu yếu và có thể được dọn sạch ngay khi ThreadLocalđối tượng ra khỏi phạm vi.


Không cần phải nói, tôi khá ấn tượng bởi cách triển khai này, nó khá thanh lịch xoay quanh rất nhiều vấn đề đồng thời (phải thừa nhận bằng cách tận dụng lợi thế là một phần của Java lõi, nhưng điều đó có thể tha thứ cho chúng vì nó là một lớp thông minh) và cho phép nhanh chóng và truy cập an toàn luồng vào các đối tượng chỉ cần được truy cập bởi một luồng tại một thời điểm.

Việc triển khai tl; dr ThreadLocal khá tuyệt và nhanh hơn / thông minh hơn nhiều so với những gì bạn có thể nghĩ lúc đầu.

Nếu bạn thích câu trả lời này, bạn cũng có thể đánh giá cao cuộc thảo luậnThreadLocalRandom (ít chi tiết hơn) của tôi .

Thread/ ThreadLocalđoạn mã lấy từ việc triển khai Java 8 của Oracle / OpenJDK .


1
Câu trả lời của bạn trông tuyệt vời nhưng nó quá dài đối với tôi lúc này. +1 và được chấp nhận và tôi đang thêm nó vào tài khoản getpocket.com của mình để đọc nó sau. Cảm ơn!
ripper234

Tôi cần một thứ giống như ThreadLocal cũng cho phép tôi truy cập vào danh sách đầy đủ các giá trị, giống như map.values ​​(). Vì vậy, triển khai ngây thơ của tôi là WeakHashMap <String, Object> trong đó khóa là Thread.currentThread (). GetName (). Điều này tránh tham chiếu đến chính Thread. Nếu thread biến mất, thì không có gì còn giữ tên của Thread nữa (tôi thừa nhận là giả định) và giá trị của tôi sẽ biến mất.
bmauter

Tôi thực sự đã trả lời một câu hỏi cho hiệu ứng đó khá gần đây . A WeakHashMap<String,T>giới thiệu một số vấn đề, nó không phải là luồng an toàn và nó "chủ yếu được thiết kế để sử dụng với các đối tượng chính có phương thức bằng nhau kiểm tra nhận dạng đối tượng bằng toán tử ==" - vì vậy thực sự sử dụng Threadđối tượng làm khóa sẽ tốt hơn. Tôi sẽ đề xuất sử dụng bản đồ Guava thinKeys mà tôi mô tả ở trên cho trường hợp sử dụng của bạn.
dimo414

1
Chà, các khóa yếu là không cần thiết, nhưng hãy cân nhắc sử dụng Bản đồ ConcurrentHash trên HashMap được đồng bộ hóa - bản đồ trước đây được thiết kế để truy cập đa luồng và sẽ hoạt động tốt hơn nhiều trong trường hợp mỗi luồng thường truy cập một khóa khác nhau.
dimo414,

1
@shmosel lớp này được điều chỉnh cao , vì vậy tôi sẽ bắt đầu từ giả định rằng những cân nhắc như vậy đã được thực hiện. Xem nhanh cho thấy rằng khi một luồng kết thúc bình thường Thread.exit()được gọi và bạn sẽ thấy threadLocals = null;ngay tại đó. Một bình luận đề cập đến lỗi này mà bạn cũng có thể thích đọc.
dimo414

33

Ý bạn là java.lang.ThreadLocal. Nó khá đơn giản, thực sự, nó chỉ là một Bản đồ của các cặp tên-giá trị được lưu trữ bên trong mỗi Threadđối tượng (xem Thread.threadLocalstrường). API ẩn chi tiết triển khai đó, nhưng đó ít nhiều là tất cả.


Tôi không thể hiểu tại sao cần phải có bất kỳ thứ gì, vì theo định nghĩa, dữ liệu chỉ hiển thị cho một luồng duy nhất.
skaffman, 30/07/09

8
Đúng, không có đồng bộ hóa hoặc khóa xung quanh hoặc trong Bản đồ ThreadLocal, vì nó chỉ được truy cập trong luồng.
Cowan,

8

Các biến ThreadLocal trong Java hoạt động bằng cách truy cập vào HashMap do cá thể Thread.currentThread () nắm giữ.


Điều đó không chính xác (hoặc ít nhất nó không còn nữa). Thread.currentThread () là một cuộc gọi riêng trong lớp Thread.của. Ngoài ra, Thread có một "ThreadLocalMap" là một nhóm (mảng) mã băm đơn lẻ. Đối tượng này không hỗ trợ giao diện Bản đồ.
user924272

1
Về cơ bản đó là những gì tôi đã nói. currentThread () trả về một cá thể Thread, giữ một bản đồ các ThreadLocals cho các giá trị.
Chris Vest

4

Giả sử bạn sắp triển khai ThreadLocal, làm thế nào để bạn làm cho nó trở nên cụ thể? Tất nhiên, phương pháp đơn giản nhất là tạo một trường không tĩnh trong lớp Thread, chúng ta hãy gọi nó threadLocals. Bởi vì mỗi luồng được đại diện bởi một cá thể luồng, vì vậy threadLocalstrong mỗi luồng cũng sẽ khác nhau. Và đây cũng là những gì Java làm:

/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

Cái gì ThreadLocal.ThreadLocalMapở đây? Bởi vì bạn chỉ có một threadLocalscho một luồng, vì vậy nếu bạn chỉ đơn giản lấy threadLocalscủa bạn ThreadLocal(giả sử, định nghĩa threadLocals là Integer), bạn sẽ chỉ có một ThreadLocalcho một luồng cụ thể. Điều gì xảy ra nếu bạn muốn nhiều ThreadLocalbiến cho một chủ đề? Cách đơn giản nhất là tạo threadLocalsmột HashMap, keymỗi mục nhập là tên của ThreadLocalbiến và valuegiá trị của mỗi mục nhập là giá trị của ThreadLocalbiến. Một chút khó hiểu? Giả sử chúng ta có hai luồng, t1t2. chúng lấy cùng một Runnabletrường hợp là tham số của hàm Threadtạo và cả hai đều có hai ThreadLocalbiến có tên tlAtlb. Đây là những gì nó giống như.

t1.tlA

+-----+-------+
| Key | Value |
+-----+-------+
| tlA |     0 |
| tlB |     1 |
+-----+-------+

t2.tlB

+-----+-------+
| Key | Value |
+-----+-------+
| tlA |     2 |
| tlB |     3 |
+-----+-------+

Lưu ý rằng các giá trị được tạo ra bởi tôi.

Bây giờ nó có vẻ hoàn hảo. Nhưng là ThreadLocal.ThreadLocalMapgì? Tại sao nó không chỉ sử dụng HashMap? Để giải quyết vấn đề, hãy xem điều gì sẽ xảy ra khi chúng ta đặt một giá trị thông qua set(T value)phương thức của ThreadLocallớp:

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

getMap(t)chỉ đơn giản là trả lại t.threadLocals. Bởi vì t.threadLocalsđược bắt đầu để null, vì vậy chúng tôi nhập createMap(t, value)trước:

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

Nó tạo một phiên bản mới ThreadLocalMapbằng cách sử dụng phiên bản hiện tại ThreadLocalvà giá trị được đặt. Hãy xem ThreadLocalMapnó như thế nào, nó thực sự là một phần của ThreadLocallớp học

static class ThreadLocalMap {

    /**
     * The entries in this hash map extend WeakReference, using
     * its main ref field as the key (which is always a
     * ThreadLocal object).  Note that null keys (i.e. entry.get()
     * == null) mean that the key is no longer referenced, so the
     * entry can be expunged from table.  Such entries are referred to
     * as "stale entries" in the code that follows.
     */
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }

    ...

    /**
     * Construct a new map initially containing (firstKey, firstValue).
     * ThreadLocalMaps are constructed lazily, so we only create
     * one when we have at least one entry to put in it.
     */
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        table = new Entry[INITIAL_CAPACITY];
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
        setThreshold(INITIAL_CAPACITY);
    }

    ...

}

Phần cốt lõi của ThreadLocalMaplớp là phần Entry classmở rộng WeakReference. Nó đảm bảo rằng nếu luồng hiện tại thoát ra, nó sẽ được thu gom rác tự động. Đây là lý do tại sao nó sử dụng ThreadLocalMapthay vì đơn giản HashMap. Nó chuyển hiện tại ThreadLocalvà giá trị của nó làm tham số của Entrylớp, vì vậy khi chúng ta muốn lấy giá trị, chúng ta có thể lấy nó từ tableđó, là một thể hiện của Entrylớp:

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

Đây là những gì giống như trong toàn bộ bức tranh:

Toàn bộ hình ảnh


-1

Về mặt khái niệm, bạn có thể ThreadLocal<T>coi a giống như việc giữ một Map<Thread,T>lưu trữ các giá trị của luồng cụ thể, mặc dù đây không phải là cách nó thực sự được triển khai.

Các giá trị của thread-speci fi c được lưu trữ trong chính đối tượng Thread; khi luồng kết thúc, các giá trị chỉ định của luồng có thể được thu thập rác.

Tham khảo: JCIP


1
Về mặt khái niệm, có. Nhưng như bạn thấy các câu trả lời khác ở trên, việc triển khai hoàn toàn khác.
Archit
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.