Як реалізується ThreadLocal? Чи реалізовано це в Java (з використанням одночасної карти з ThreadID для об'єкта), чи використовується якийсь хук JVM, щоб зробити це ефективніше?
Як реалізується ThreadLocal? Чи реалізовано це в Java (з використанням одночасної карти з ThreadID для об'єкта), чи використовується якийсь хук JVM, щоб зробити це ефективніше?
Відповіді:
Всі відповіді тут правильні, але трохи розчаровують, оскільки вони дещо затьмарюють, наскільки розумною ThreadLocal
є реалізація. Я просто дивився на вихідний кодThreadLocal
і був приємно вражений тим, як він реалізований.
Наївна реалізація
Якби я попросив вас реалізувати ThreadLocal<T>
клас із API, описаним у javadoc, що б ви зробили? Початкова реалізація, швидше за все, буде ConcurrentHashMap<Thread,T>
використанням Thread.currentThread()
як його ключовим фактором. Це буде працювати досить добре, але має деякі недоліки.
ConcurrentHashMap
це досить розумний клас, але він, зрештою, все одно повинен мати справу з тим, щоб запобігти тому, щоб кілька потоків будь-яким чином втручалися в нього, і якщо різні потоки регулярно потрапляють на нього, будуть уповільнення.Впровадження 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 .
WeakHashMap<String,T>
призводить до кількох проблем, він не є безпечним для потоків, і він "призначений насамперед для використання з ключовими об'єктами, рівні методи яких перевіряють ідентичність об'єкта за допомогою оператора ==" - тому насправді використання Thread
об'єкта як ключа могло б стати кращим. Я б запропонував скористатися описаною вище картою Гуава слабких ключів для вашого випадку використання.
Thread.exit()
це викликається, і ви побачите threadLocals = null;
тут же. Коментар посилається на цю помилку, яку ви також можете із задоволенням прочитати.
Ви маєте на увазі java.lang.ThreadLocal
. Це дуже просто, насправді, це просто Карта пар імен-значення, що зберігається всередині кожного Thread
об’єкта (див. Thread.threadLocals
Поле). API приховує цю деталь реалізації, але це більш-менш все, що до цього є.
Змінні ThreadLocal у Java працюють шляхом доступу до HashMap, що зберігається екземпляром Thread.currentThread ().
Припустимо, ви збираєтеся реалізувати 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
змінних для потоку? Найпростіший спосіб - це зробити threadLocals
a 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();
}
Ось що виглядає на цілій картині:
Концептуально ви можете уявити a ThreadLocal<T>
як утримуючий a, Map<Thread,T>
який зберігає специфічні для потоку значення, хоча це не так, як це насправді реалізовано.
Значення, визначені потоком, зберігаються в самому об'єкті Thread; коли потік закінчується, специфічні для потоку значення можна збирати сміття.
Довідково: JCIP