Это продолжение моего поста на прошлой неделе, где я объяснил мотивы использования ThreadLocal . Из поста мы могли бы вспомнить, что ThreadLocal — действительно классная концепция, если вы хотите иметь независимо инициализированную копию переменной для каждого потока. Теперь любопытные могли уже начать спрашивать: «Как я могу реализовать такую концепцию в Java»?
Или вы можете почувствовать, что это не будет интересной темой — в конце концов, все, что вам нужно здесь, это Карта , не так ли? При работе с ThreadLocal <T> кажется, что в мире есть смысл реализовать решение в виде HashMap <Thread, T> с ключом Thread.currentThread () . На самом деле все не так просто. Поэтому, если у вас есть пять минут, потерпите меня, и я проведу вас через красивую концепцию дизайна.
Первая очевидная проблема с простым решением HashMap — это безопасность потоков. Поскольку HashMap не создан для поддержки одновременного использования, мы не можем безопасно использовать реализацию в многопоточной среде. К счастью, нам не нужно долго искать исправление — ConcurrentHashMap <Thread, T> выглядит как совпадение, созданное на небесах. Полный параллелизм поиска и настраиваемый ожидаемый параллелизм для обновлений — это именно то, что нам нужно в первую очередь.
Теперь, если бы вы применили решение на основе ConcurrentHashMap к реализации ThreadLocal в источнике JDK, вы бы столкнулись с двумя серьезными проблемами.
- Прежде всего, вы используете потоки в качестве ключей в структуре карты . Поскольку карта никогда не собирается сборщиком мусора, вы в конечном итоге навсегда сохраняете ссылку на поток, блокируя поток как GCd. Неохотно вы создали огромную утечку памяти в дизайне.
- Вторая проблема может занять больше времени, но даже благодаря продуманной сегментации под капотом, снижающей вероятность конфликта блокировок, ConcurrentHashMap по- прежнему несет накладные расходы на синхронизацию. С требованием синхронизации все еще есть, у вас все еще есть структура, которая является потенциальным источником узкого места.
Но давайте сначала начнем решать самую большую проблему. Наша структура данных должна позволять потокам собирать мусор, если наша ссылка является последней, указывающей на рассматриваемый поток. Опять же, первое возможное решение — смотреть прямо на нас — вместо наших обычных ссылок на объект, почему бы не использовать вместо этого WeakReferences ? Таким образом, реализация теперь будет выглядеть примерно так:
1
|
Collections.synchronizedMap( new WeakHashMap<Thread, T>()) |
Теперь мы избавились от проблемы утечки: если никто, кроме нас, не ссылается на поток , его можно завершить и собрать мусор. Но мы до сих пор не разобрались с вопросами параллелизма. Решение этой проблемы теперь действительно образец нестандартного мышления. До сих пор мы рассматривали переменные ThreadLocal как отображение потоков на переменные. Но что, если мы изменим мышление и вместо этого представим решение как отображение объектов ThreadLocal на значения в каждом потоке ? Если каждый поток хранит сопоставление, а ThreadLocal является просто интерфейсом этого сопоставления, мы можем избежать проблем с синхронизацией. Более того, мы также избегаем проблем с GC!
И действительно, когда мы открываем исходный код классов ThreadLocal и Thread, мы видим, что именно это решение действительно реализовано в JDK:
1
2
3
4
|
public class Thread implements Runnable { ThreadLocal.ThreadLocalMap threadLocals = null ; // cut for brevity } |
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
public class ThreadLocal<T> { static class ThreadLocalMap { // cut for brevity } ThreadLocalMap getMap(Thread t) { return t.threadLocals; } public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null ) { ThreadLocalMap.Entry e = map.getEntry( this ); if (e != null ) return (T) e.value; } return 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; } // cut for brevity } |
Итак, у нас это есть. Класс Thread хранит ссылку на экземпляр ThreadLocal.ThreadLocalMap , который создается с использованием слабых ссылок на ключи. Создавая структуру в обратном порядке, мы полностью избежали проблем с конфликтами потоков, поскольку наш ThreadLocal может получить доступ только к значению в текущем потоке. Кроме того, когда поток завершает работу, карта может собирать мусор, поэтому мы также избежали проблемы утечки памяти.
Я надеюсь, что вы почувствовали просветление при взгляде на дизайн, так как это действительно элегантное решение сложной проблемы. Я чувствую, что чтение исходного кода — идеальный способ узнать о новых концепциях. И если вы являетесь Java-разработчиком — что может быть лучше для получения знаний, чем чтение исходного кода Джошуа Блоха и Дуга Ли, интегрированного в JDK?