Як реалізується Java ThreadLocal під капотом?


81

Як реалізується ThreadLocal? Чи реалізовано це в Java (з використанням одночасної карти з ThreadID для об'єкта), чи використовується якийсь хук JVM, щоб зробити це ефективніше?

Відповіді:


120

Всі відповіді тут правильні, але трохи розчаровують, оскільки вони дещо затьмарюють, наскільки розумною ThreadLocalє реалізація. Я просто дивився на вихідний кодThreadLocal і був приємно вражений тим, як він реалізований.

Наївна реалізація

Якби я попросив вас реалізувати ThreadLocal<T>клас із API, описаним у javadoc, що б ви зробили? Початкова реалізація, швидше за все, буде ConcurrentHashMap<Thread,T>використанням Thread.currentThread()як його ключовим фактором. Це буде працювати досить добре, але має деякі недоліки.

  • Суперечка в потоці - ConcurrentHashMapце досить розумний клас, але він, зрештою, все одно повинен мати справу з тим, щоб запобігти тому, щоб кілька потоків будь-яким чином втручалися в нього, і якщо різні потоки регулярно потрапляють на нього, будуть уповільнення.
  • Постійно зберігає вказівник як на Потік, так і на об'єкт, навіть після того, як Потік закінчений і може бути GC'ed.

Впровадження GC

Гаразд, спробуйте ще раз, дозволимо вирішити проблему вивезення сміття, використовуючи слабкі посилання . Робота з WeakReference може заплутати, але достатньо буде використовувати карту, побудовану так:

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

Або якщо ми використовуємо гуаву (а маємо!):

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

Це означає, що як тільки ніхто інший не тримається за Нитку (маючи на увазі, що вона закінчена), ключ / значення може бути зібраним сміттям, що є вдосконаленням, але все ще не вирішує проблему суперечок у потоці, тобто поки що ThreadLocalце не все дивовижний клас. Крім того, якби хтось вирішив затримати Threadоб'єкти після того, як вони закінчили, вони ніколи не будуть GC'ed, а отже, і наші об'єкти, навіть якщо вони технічно недосяжні зараз.

Розумна реалізація

Ми думали про те, ThreadLocalяк відображення ниток до значень, але, можливо, це насправді не правильний спосіб думати про це. Замість того, щоб думати про це як про відображення потоків із значеннями в кожному об’єкті ThreadLocal, що, якби ми думали про це як про відображення об’єктів ThreadLocal до значень у кожному потоці ? Якщо кожен потік зберігає відображення, а ThreadLocal просто забезпечує приємний інтерфейс для цього відображення, ми можемо уникнути всіх проблем попередніх реалізацій.

Реалізація буде виглядати приблизно так:

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

Тут не потрібно турбуватися про паралельність, оскільки лише одна нитка матиме доступ до цієї карти.

Розробники Java мають головну перевагу перед нами тут - вони можуть безпосередньо розвивати клас Thread і додавати до нього поля та операції, і саме це вони і зробили.

В java.lang.Threadє такі рядки:

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

Що, як підказує коментар, насправді є приватно-пакувальним відображенням усіх значень, що відстежуються ThreadLocalоб’єктами для цього Thread. Реалізація ThreadLocalMapне є WeakHashMap, але вона відповідає одному і тому ж базовому контракту, включаючи утримання ключів за допомогою слабких посилань.

ThreadLocal.get() потім реалізується так:

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()так:

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

По суті, використовуйте карту в цій темі, щоб вмістити всі наші ThreadLocalоб’єкти. Таким чином, нам ніколи не потрібно турбуватися про значення в інших потоках ( ThreadLocalбуквально ми можемо отримати доступ лише до значень у поточній темі) і, отже, не маємо проблем з паралельністю. Крім того, як тільки це Threadбуде зроблено, його карта буде автоматично оброблена GC, і всі локальні об'єкти будуть очищені. Навіть якщо Threadутримується за, ThreadLocalоб'єкти утримуються за слабким посиланням і можуть бути очищені, як тільки ThreadLocalоб'єкт виходить за межі зони дії.


Само собою зрозуміло, що мене дуже вразило таке впровадження, воно досить елегантно обіймає багато питань одночасності (правда, скориставшись тим, що є частиною базової Java, але це їм можна пробачити, оскільки це такий розумний клас) і дозволяє швидко і швидко безпечний для потоків доступ до об’єктів, до яких одночасно має доступ лише один потік.

ThreadLocalРеалізація tl; dr досить крута і набагато швидша / розумніша, ніж ви можете подумати на перший погляд.

Якщо вам сподобалась ця відповідь, ви можете також оцінити моє (менш детальне) обговоренняThreadLocalRandom .

Thread/ ThreadLocalфрагменти коду, взяті з реалізації Java 8 від Oracle / OpenJDK .


1
Ваші відповіді виглядають неймовірно, але зараз це занадто довго для мене. +1 і прийнято, і я додаю його до свого акаунта getpocket.com, щоб прочитати його пізніше. Дякую!
ripper234

Мені потрібна річ, схожа на ThreadLocal, яка також дозволить мені отримати доступ до повного списку значень, подібно до map.values ​​(). Отже, моя наївна реалізація - це WeakHashMap <String, Object>, де ключем є Thread.currentThread (). GetName (). Це дозволяє уникнути посилання на саму Нитку. Якщо нитка зникне, тоді нічого більше не містить імені потоку (припущення, я визнаю), і моє значення зникне.
bmauter

Я насправді відповів на таке запитання зовсім недавно . A WeakHashMap<String,T>призводить до кількох проблем, він не є безпечним для потоків, і він "призначений насамперед для використання з ключовими об'єктами, рівні методи яких перевіряють ідентичність об'єкта за допомогою оператора ==" - тому насправді використання Threadоб'єкта як ключа могло б стати кращим. Я б запропонував скористатися описаною вище картою Гуава слабких ключів для вашого випадку використання.
dimo414

1
Ну, слабкі клавіші не потрібні, але подумайте про використання ConcurrentHashMap над синхронізованою HashMap - перша призначена для багатопотокового доступу і буде працювати набагато краще у випадку, коли кожен потік, як правило, має доступ до іншого ключа.
dimo414

1
@shmosel цей клас дуже налаштований, тому я б почав з припущення, що такі міркування були зроблені. Швидкий погляд показує, що коли потік нормально закінчується, Thread.exit()це викликається, і ви побачите threadLocals = null;тут же. Коментар посилається на цю помилку, яку ви також можете із задоволенням прочитати.
dimo414

33

Ви маєте на увазі java.lang.ThreadLocal. Це дуже просто, насправді, це просто Карта пар імен-значення, що зберігається всередині кожного Threadоб’єкта (див. Thread.threadLocalsПоле). API приховує цю деталь реалізації, але це більш-менш все, що до цього є.


Я не можу зрозуміти, навіщо такі потрібні, враховуючи, що за визначенням дані видно лише в одному потоці.
skaffman

8
Правильно, немає синхронізації або блокування навколо або всередині ThreadLocalMap, оскільки доступ до нього здійснюється лише в потоці.
Коуен

8

Змінні ThreadLocal у Java працюють шляхом доступу до HashMap, що зберігається екземпляром Thread.currentThread ().


Це не правильно (або, принаймні, це вже не так). Thread.currentThread () - це власний виклик у класі Thread.class. Також у Thread є "ThreadLocalMap", що представляє собою одне сегмент (масив) хеш-кодів. Цей об’єкт не підтримує інтерфейс Map.
user924272

1
Це в основному те, що я сказав. currentThread () повертає екземпляр Thread, який містить карту ThreadLocals до значень.
Chris Vest

4

Припустимо, ви збираєтеся реалізувати ThreadLocal, як ви робите це специфічним для потоку? Звичайно, найпростіший метод - це створення нестатичного поля в класі Thread, назвемо його threadLocals. Оскільки кожен потік представлений екземпляром потоку, тож threadLocalsу кожному потоці теж буде по-різному. І це також те, що робить Java:

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

Що ThreadLocal.ThreadLocalMapтут? Оскільки у вас є лише threadLocalsдля потоку, тому якщо ви просто візьмете threadLocalsяк свій ThreadLocal(скажімо, визначте threadLocals як Integer), у вас буде лише один ThreadLocalдля конкретного потоку. Що робити, якщо вам потрібні кілька ThreadLocalзмінних для потоку? Найпростіший спосіб - це зробити threadLocalsa HashMap, keyкожен запис - це ім’я ThreadLocalзмінної, а valueкожен запис - значення ThreadLocalзмінної. Трохи заплутаний? Скажімо, у нас є дві нитки, t1і t2. вони беруть той самий Runnableекземпляр, що і параметр Threadконструктора, і обидва вони мають дві ThreadLocalзмінні з іменем tlAі tlb. Ось що це таке.

t1.tlA

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

t2.tlB

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

Зверніть увагу, що ці значення складаються мною.

Зараз це здається ідеальним. Але що таке ThreadLocal.ThreadLocalMap? Чому він просто не використовував HashMap? Щоб вирішити проблему, давайте подивимося, що відбувається, коли ми встановлюємо значення за допомогою set(T value)методу ThreadLocalкласу:

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)просто повертається t.threadLocals. Тому що t.threadLocalsбуло ініційовано null, тому ми вводимо createMap(t, value)спочатку:

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

Він створює новий ThreadLocalMapекземпляр, використовуючи поточний ThreadLocalекземпляр та значення, яке потрібно встановити. Давайте подивимось, що ThreadLocalMapце таке, насправді це частина ThreadLocalкласу

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

    ...

}

Основною частиною ThreadLocalMapкласу є те Entry class, що розширюється WeakReference. Це гарантує, що якщо поточний потік вийде, це буде сміття, яке збирається автоматично. Ось чому він використовує ThreadLocalMapзамість простого HashMap. Він передає струм ThreadLocalі його значення як параметр Entryкласу, тому, коли ми хочемо отримати значення, ми могли б отримати його з table, який є екземпляром Entryкласу:

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

Ось що виглядає на цілій картині:

Вся картина


-1

Концептуально ви можете уявити a ThreadLocal<T>як утримуючий a, Map<Thread,T>який зберігає специфічні для потоку значення, хоча це не так, як це насправді реалізовано.

Значення, визначені потоком, зберігаються в самому об'єкті Thread; коли потік закінчується, специфічні для потоку значення можна збирати сміття.

Довідково: JCIP


1
Так, так. Але, як ви бачите, інші відповіді над реалізацією зовсім інші.
Archit
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.