Оригинальная статья и краткое изложение опыта и жизненные перипетии от набора в школу до фабрики А
Нажмите, чтобы узнать подробностиwww.codercc.com
1. Введение в ThreadLocal
В многопоточном программировании мы обычно решаем проблему безопасности потоков.Мы будем использовать синхронизацию или блокировку для управления последовательностью синхронизации потока с ресурсами критической секции, чтобы решить проблему безопасности потоков, но этот способ блокировки заблокирует поток, который не получил блокировку Подождите, очевидно, эффективность этого метода не очень высока.Суть проблемы безопасности потоков заключается в том, что несколько потоков будут работать с одними и теми же общими ресурсами критической секции., то, если каждый поток использует свои «общие ресурсы», каждый использует свой, и не влияет друг на друга, то есть достигается изоляция нескольких потоков, так что не будет проблемы безопасности потоков. На самом деле это "пространство для времени", каждый поток будет иметь свой "общий ресурс". Без сомнения, памяти будет намного больше, но поскольку нет необходимости в синхронизации, это уменьшит возможную блокировку и ожидание потока, тем самым повысив эффективность использования времени .
Хотя ThreadLocal находится не в пакете java.util.concurrent, а в пакете java.lang, я предпочитаю классифицировать его как своего рода параллельный контейнер (хотя ThreadLoclMap фактически содержит данные). отИмя класса ThreadLocal можно понимать так, как следует из названия, представляющего «локальную переменную» потока, то есть каждый поток имеет копию переменной для достижения эффекта одной копии для каждого человека, чтобы при каждом использовании можно было избежать конкуренция общих ресурсов..
2. Принцип реализации ThreadLocal
Если вы хотите изучить принцип реализации ThreadLocal, вы должны понимать несколько его основных методов, в том числе способы доступа и доступа и т. д. Давайте рассмотрим их один за другим.
void set(T value)
Метод set устанавливает значение переменной threadLocal в текущем потоке., исходный код этого метода:
public void set(T value) {
//1. 获取当前线程实例对象
Thread t = Thread.currentThread();
//2. 通过当前线程实例获取到ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
if (map != null)
//3. 如果Map不为null,则以当前threadLocl实例为key,值为value进行存入
map.set(this, value);
else
//4.map为null,则新建ThreadLocalMap并存入value
createMap(t, value);
}
Логика метода очень понятна, подробности см. в комментариях выше. Из исходного кода мы знаем, что значение хранится в ThreadLocalMap, В настоящее время его можно понимать как обычную карту, то естьЗначение данных фактически хранится в контейнере ThreadLocalMap, а в качестве ключа используется текущий экземпляр threadLocal.. Давайте вкратце рассмотрим, что такое ThreadLocalMap, хорошо иметь простое понимание, об этом я подробно расскажу ниже.
Прежде всего, как возникает ThreadLocalMap?? Исходный код очень ясен, черезgetMap(t)
Получить:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
Этот метод напрямую возвращает переменную-член threadLocals текущего объекта потока t:
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
то естьСсылка на ThreadLocalMap поддерживается Thread как переменная-член Thread. Вернитесь назад и посмотрите на метод set, который будет передан, когда карта равна Null.createMap(t,value)
метод:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
МетодСоздайте новый объект экземпляра ThreadLocalMap, а затем также используйте текущий экземпляр threadLocal в качестве ключа и сохраните значение в threadLocalMap, а затем назначьте threadLocals текущего объекта потока для threadLocalMap.
Теперь резюмируем метод set:Получите threadLocalMap, поддерживаемую потоком, через текущий поток объекта потока. Если threadLocalMap не равен null, экземпляр threadLocal используется в качестве ключа, а пара ключ-значение со значением сохраняется в threadLocalMap. Если threadLocalMap имеет значение null , создается новый threadLocalMap, а затем используется threadLocalMap., является ключом, и может быть сохранена пара ключ-значение, значение которой является значением.
T get()
Метод get должен получить значение переменной threadLocal в текущем потоке., а также взгляните на исходный код:
public T get() {
//1. 获取当前线程的实例对象
Thread t = Thread.currentThread();
//2. 获取当前线程的threadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
//3. 获取map中当前threadLocal实例为key的值的entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
//4. 当前entitiy不为null的话,就返回相应的值value
T result = (T)e.value;
return result;
}
}
//5. 若map为null或者entry为null的话通过该方法初始化,并返回该方法返回的value
return setInitialValue();
}
Поняв логику метода set, просто взгляните на метод get обратным мышлением. Пожалуйста, смотрите комментарии для логики кода.Кроме того, что в основном делает 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;
}
Логика этого метода почти такая же, как у метода set, а еще стоит обратить внимание на метод initialValue:
protected T initialValue() {
return null;
}
этоМетод защищен от модификации, то есть подклассы, наследующие ThreadLocal, могут переопределить метод и реализовать присвоение другим начальным значениям.. Подводя итог о методе get:
Получите threadLocalMap, поддерживаемый экземпляром потока текущего потока, а затем используйте текущий экземпляр threadLocal в качестве ключа для получения пары ключ-значение (запись) на карте. Если запись не равна нулю, значение записи равно вернулся. Если полученный threadLocalMap имеет значение null или Entry имеет значение null, текущий threadLocal используется в качестве ключа, значение равно null и сохраняется в карте, и возвращается значение null.
void remove()
public void remove() {
//1. 获取当前线程的threadLocalMap
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
//2. 从map中删除以当前threadLocal实例为key的键值对
m.remove(this);
}
Метод get, set реализует хранение и чтение данных, конечно, нам нужно научиться удалять данные**. Удаление данных — это, конечно, удаление данных с карты. Сначала получите threadLocalMap, связанный с текущим потоком, а затем удалите пару ключ-значение с экземпляром threadLocal в качестве ключа из карты**.
3. Детали ThreadLocalMap
Из приведенного выше анализа мы уже знаем, что данные на самом деле помещаются в threadLocalMap, а методы get, set и remove threadLocal на самом деле реализуются методами getEntry, set и remove threadLocalMap. Если вы хотите полностью понять threadLocal, вы должны понимать threadLocalMap.
3.1 Структура входных данных
ThreadLocalMap – это статический внутренний класс threadLocal. Как и большинство контейнеров, он поддерживает внутренний массив. Тот же threadLocalMap внутренне поддерживает табличный массив типа Entry.
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
Как видно из комментариев, длина массива таблиц является степенью числа 2. Далее, давайте посмотрим, что такое Entry:
static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value;
скопировать кодEntry(ThreadLocal<?> k, Object v) { super(k); value = v; }
}
Entry представляет собой пару ключ-значение с ThreadLocal в качестве ключа и Object в качестве значения. Кроме того, следует отметить, что **threadLocal здесь является слабой ссылкой, поскольку Entry наследует WeakReference. В методе построения Entry super(k) вызывается метод. Будет обернут экземпляр threadLocal в WeakReferenece. **Здесь мы можем использовать изображение (рисунок ниже взят из http://blog.xiaohansong.com/2016/08/06/ThreadLocal-memory-leak/), чтобы понять взаимосвязь между thread, threadLocal, threadLocalMap и Entry:
Обратите внимание, что сплошные линии на рисунке выше представляют сильные ссылки, а пунктирные линии — слабые ссылки. Как показано на рисунке, каждый экземпляр потока может получить threadLocalMap через threadLocals, а threadLocalMap фактически является массивом Entry с экземпляром threadLocal в качестве ключа и любым объектом в качестве значения. Когда мы присваиваем значение переменной threadLocal, мы фактически используем текущий экземпляр threadLocal в качестве ключа, а значение Entry сохраняется в этом threadLocalMap. Следует отметить, что ключ в **Entry является слабой ссылкой, когда для сильной ссылки вне threadLocal установлено значение null(threadLocalInstance=null
), то когда система является GC, согласно анализу достижимости, этот экземпляр threadLocal не имеет ссылки, которая может ссылаться на него, и этот ThreadLocal обязательно будет переработан. Таким образом, запись с нулевым ключом будет появляются в ThreadLocalMap. Невозможно получить доступ к значению записи, ключ которой равен нулю. Если текущий поток задерживается, всегда будет строгая цепочка ссылок для значения записи, ключ которой равен нулю: Thread Ref - > Thread -> ThreaLocalMap -> Entry -> значение никогда не может быть переработано, что приводит к утечке памяти. **Конечно, если текущий поток заканчивается, threadLocal, threadLocalMap и Entry недоступны для цепочки ссылок, они будут переработаны системой во время сборки мусора. В реальной разработке пулы потоков используются для поддержки создания и повторного использования потоков, таких как пулы потоков фиксированного размера.Потоки не будут автоматически завершаться для повторного использования.Поэтому проблема утечки памяти threadLocal должна быть достойна нашего рассмотрения и рассмотрения. Обратите внимание на проблему, вы можете прочитать эту статью по этому вопросу----Подробно объясните проблему утечки памяти threadLocal.
3.2 метод установки
Подобно concurrentHashMap, hashMap и другим контейнерам, threadLocalMap также реализован с использованием хеш-таблиц. Прежде чем понять метод set, давайте повторим знания о хеш-таблицах (отОбъясняющая часть threadLocalMap этой статьиа такжехэш этой статьи).
- хеш-таблица
В идеале хеш-таблица представляет собой массив фиксированного размера, содержащий ключи, которые сопоставляются с различными позициями в массиве с помощью хэш-функции. Ниже
В идеальном состоянии хэш-функция может равномерно распределить ключевые слова по разным позициям массива, и не будет ситуации, когда два ключевых слова имеют одинаковое хеш-значение (при условии, что количество ключевых слов меньше размера массива). Но на практике часто возникают ситуации, когда несколько ключевых слов имеют одно и то же значение хеш-функции (сопоставленное с одной и той же позицией в массиве), и мы называем эту ситуацию коллизией хэшей. Для разрешения коллизий хешей в основном используются следующие два метода:Метод отдельного связанного списка(отдельная цепочка) иоткрытая адресация(открытая адресация)
- Метод отдельного связанного списка
Метод разбросанного связанного списка использует связанный список для разрешения конфликтов и сохраняет элементы с одинаковым значением хеш-функции в связанный список. При запросе сначала найдите связанный список, в котором находится элемент, а затем пройдитесь по связанному списку, чтобы найти соответствующий элемент.Типичной реализацией является метод застежки-молнии hashMap и concurrentHashMap. Вот схематическая диаграмма:
Изображение с http://faculty.cs.niu.edu/~freedman/340/340notes/340hash.htm
- открытая адресация
Открытый метод адресации не создает связанный список, когда ячейка массива, к которой хешируется ключ, уже занята другим ключом, он будет пытаться найти другие ячейки в массиве, пока не будет найдена пустая ячейка. Есть много способов обнаружения пустых ячеек в массиве, вот один из самых простых — линейное обнаружение. Метод линейного обнаружения начинается с конфликтующего элемента массива, по очереди ищет пустой элемент и, если он достигает конца массива, начинает поиск с начала (круговой поиск). Как показано ниже:
Изображение с http://alexyyek.github.io/2014/12/14/hashCollapse/
Для сравнения двух методов вы можете обратиться кэта статья.ThreadLocalMap использует метод открытого адреса для обработки коллизий хэшей., а метод разделенного связанного списка, используемый в HashMap. Причина принятия другого подхода в основном в том, что хеш-значения в ThreadLocalMap очень равномерно рассредоточены, и конфликтов мало. А ThreadLocalMap часто нужно очищать бесполезные объекты, удобнее использовать чистые массивы.
Поняв эти связанные знания, давайте вернемся и посмотрим на метод set. Исходный код метода set:
private void set(ThreadLocal<?> key, Object value) {
скопировать код// We don't use a fast path as with get() because it is at // least as common to use set() to create new entries as // it is to replace existing ones, in which case, a fast // path would fail more often than not. Entry[] tab = table; int len = tab.length; //根据threadLocal的hashCode确定Entry应该存放的位置 int i = key.threadLocalHashCode & (len-1); //采用开放地址法,hash冲突的时候使用线性探测 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); //覆盖旧Entry if (k == key) { e.value = value; return; } //当key为null时,说明threadLocal强引用已经被释放掉,那么就无法 //再通过这个key获取threadLocalMap中对应的entry,这里就存在内存泄漏的可能性 if (k == null) { //用当前插入的值替换掉这个key为null的“脏”entry replaceStaleEntry(key, value, i); return; } } //新建entry并插入table中i处 tab[i] = new Entry(key, value); int sz = ++size; //插入后再次清除一些key为null的“脏”entry,如果大于阈值就需要扩容 if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash();
}
Ключевая часть метода setПожалуйста, смотрите примечание выше, следует отметить несколько основных моментов:
-
хэш-код threadLocal?
private final int threadLocalHashCode = nextHashCode(); private static final int HASH_INCREMENT = 0x61c88647; private static AtomicInteger nextHashCode =new AtomicInteger(); /** * Returns the next hash code. */ private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); }
Из исходного кода мы ясно видим, что hashCode экземпляра threadLocal реализуется методом nextHashCode(), который на самом деле всегда реализуется с помощью AtomicInteger плюс 0x61c88647. Число 0x61c88647 имеет особое значение, оно может гарантировать, что каждое хэш-сегмент хеш-таблицы может быть равномерно распределено, что
Fibonacci Hashing
, для получения дополнительной информации см.Хэш-раздел threadLocal этого поста. Также возможно равномерное распределение, поэтому threadLocal предпочитает использовать метод открытых адресов для решения проблемы конфликтов хэшей. -
Как определить, куда в хеш-таблицу вставляются новые значения?
Исходный код этой операции:
key.threadLocalHashCode & (len-1)
, так же, как контейнеры, такие как hashMap и ConcurrentHashMap, используйте хэш-код текущего ключа (то есть экземпляра threadLocal), чтобы добавить размер хэш-таблицы, потому что размер хэш-таблицы всегда является степенью двойки. , поэтому добавление эквивалентно процессу по модулю, поэтому его можно выделить для определенного хеш-сегмента через ключ. Что касается причины, по которой операция по модулю выполняется операцией побитового И, эффективность выполнения побитовой операции намного выше, чем у операции по модулю. -
Как разрешить конфликт хэшей?
через исходный код
nextIndex(i, len)
метод решения проблемы хеш-конфликта, этот метод((i + 1 < len) ? i + 1 : 0);
, то есть продолжить линейное обнаружение в обратном направлении и начать с 0, когда будет достигнут конец хэш-таблицы, образуя кольцо. -
Как решить "грязный" вход?
Анализируя взаимосвязь между threadLocal, threadLocalMap и Entry, мы уже знаем, что возможны утечки памяти при использовании threadLocal (после создания объекта объект не использовался в последующей логике, но сборщик мусора не может рекультивировать эту часть память), В исходном коде запись, ключ которой равен нулю, называется "stale entry", что дословно переводится как stale entry. Я понимаю это как "грязная запись". Естественно, мастера Джош Блох и Дуг Ли учли эту ситуацию , пройти в процессе нахождения той же переопределяемой записи, что и текущий ключ в цикле for метода setreplaceStaleEntryспособ решить проблему грязного входа. Если текущая таблица[i] имеет значение null, прямая вставка новой записи также пройдет.cleanSomeSlotsрешить проблему грязных записей, оМетоды cleanSomeSlots и replaceStaleEntry будут обсуждаться в подробном объяснении утечек памяти threadLocal, подробности см. в этой статье.
-
Как расширить емкость?
Определение порога
Как и большинство контейнеров, threadLocalMap имеет механизм расширения емкости, так как же определяется его порог? private int threshold; // По умолчанию 0 /** _ Начальная емкость -- ДОЛЖНА быть степенью двойки. _/ частный статический финал int INITIAL_CAPACITY = 16; ThreadLocalMap(ThreadLocal> firstKey, Object firstValue) { таблица = новая запись [INITIAL_CAPACITY]; int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY — 1); таблица [i] = новая запись (firstKey, firstValue); размер = 1; установить порог (INITIAL_CAPACITY); } /** _ Установите порог изменения размера, чтобы в худшем случае поддерживать коэффициент загрузки 2/3. _/ частная пустота setThreshold (int len) { порог = длина * 2/3; }
Согласно исходному коду, при первом назначении threadLocal создается threadLocalMap с начальным размером 16, а порог устанавливается методом setThreshold, а его значением является текущая длина массива хэшей, умноженная на (2/ 3), значит коэффициент загрузки 2/3(Коэффициент загрузки — это параметр для измерения плотности хеш-таблицы. Чем больше коэффициент загрузки, тем больше загружена хеш-таблица и выше вероятность конфликта хэшей. Чем меньше вероятность конфликта. В то же время, если он слишком мал, очевидно, что коэффициент использования памяти невысок. Значение этого значения должно учитывать баланс между коэффициентом использования памяти и вероятностью конфликта хэшей.Например, коэффициент загрузки hashMap и concurrentHashMap равны 0,75.). здесьИсходный размер threadLocalMap равен 16.,Коэффициент загрузки 2/3, поэтому доступный размер хэш-таблицы: 16*2/3=10, то есть доступная емкость хеш-таблицы равна 10.
Изменение размера расширения
Из метода set видно, что когда размер хеш-таблицы больше порогового значения, для расширения будет использоваться метод resize.
/** * Double the capacity of the table. */ private void resize() { Entry[] oldTab = table; int oldLen = oldTab.length; //新数组为原数组的2倍 int newLen = oldLen * 2; Entry[] newTab = new Entry[newLen]; int count = 0;
скопировать кодfor (int j = 0; j < oldLen; ++j) { Entry e = oldTab[j]; if (e != null) { ThreadLocal<?> k = e.get(); //遍历过程中如果遇到脏entry的话直接另value为null,有助于value能够被回收 if (k == null) { e.value = null; // Help the GC } else { //重新确定entry在新数组的位置,然后进行插入 int h = k.threadLocalHashCode & (newLen - 1); while (newTab[h] != null) h = nextIndex(h, newLen); newTab[h] = e; count++; } } } //设置新哈希表的threshHold和size属性 setThreshold(newLen); size = count; table = newTab;
}
логика методасмотрите примечание, создайте массив, размер которого в два раза превышает длину исходного массива, затем перейдите к записи в старом массиве и вставьте ее в новый хэш-массив, основное замечание заключается в том, чтоВ процессе расширения для грязных записей будет установлено значение null, чтобы его можно было переработать сборщиком мусора и решить проблему скрытых утечек памяти..
3.3 Метод getEntry
Исходный код метода getEntry:
private Entry getEntry(ThreadLocal<?> key) {
//1. 确定在散列数组中的位置
int i = key.threadLocalHashCode & (table.length - 1);
//2. 根据索引i获取entry
Entry e = table[i];
//3. 满足条件则返回该entry
if (e != null && e.get() == key)
return e;
else
//4. 未查找到满足条件的entry,额外在做的处理
return getEntryAfterMiss(key, i, e);
}
Логика метода очень проста, если ключ текущей записи совпадает с искомым ключом, то запись будет возвращена напрямую, в противном случае при наборе возникнет хеш-конфликт, который необходимо дополнительно обработать через получитьEntryAfterMiss. Метод getEntryAfterMiss:
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length;
скопировать кодwhile (e != null) { ThreadLocal<?> k = e.get(); if (k == key) //找到和查询的key相同的entry则返回 return e; if (k == null) //解决脏entry的问题 expungeStaleEntry(i); else //继续向后环形查找 i = nextIndex(i, len); e = tab[i]; } return null;
}
Этот метод также хорошо понятен. Он выполняет поиск в обратном направлении через nextIndex. Если он находит запись с тем же ключом, что и запрос, он возвращает его напрямую. Если в процессе поиска встречается грязная запись, используйте метод expungeStaleEntry для ее обработки. . Пока**, чтобы решить проблему потенциальных утечек памяти, эти грязные записи будут обрабатываться в set, resize, getEntry, и видно, что почти все время прилагаются усилия для решения этой проблемы настолько, насколько возможный. **
3.4 remove
/**
* Remove the entry for key.
*/
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
//将entry的key置为null
e.clear();
//将该entry的value也置为null
expungeStaleEntry(i);
return;
}
}
}
Логика этого метода очень проста: после нахождения записи с тем же ключом, что и указанный ключ, в обратном цикле, сначала установите ключ в значение null с помощью метода очистки, преобразуйте его в грязную запись, а затем вызовите метод expungeStaleEntry для установите для него значение null, чтобы его можно было очистить во время сборки мусора, а для table[i] установлено значение null.
4. Сценарии использования ThreadLocal
ThreadLocal не используется для решения проблемы многопоточного доступа к общим объектам, данные по существу помещаются в threadLocalMap, на который ссылается каждый экземпляр потока, то естьКаждый отдельный поток имеет свой собственный контейнер данных (threadLocalMap), которые не влияют друг на друга.. Таким образом, threadLocal работает только дляОбщие объекты потокобезопасныбизнес-сценарий. НапримерУправление сеансом через threadLocal в спящем режимеЭто типичный случай.Разные потоки запросов (пользователи) имеют свои собственные сеансы.Если сеанс используется совместно и к нему обращаются несколько потоков, это неизбежно приведет к проблемам с безопасностью потоков. Далее, давайте сами напишем пример.У метода SimpleDateFormat.parse будут проблемы с безопасностью потоков.Мы можем попробовать обернуть SimpleDateFormat с помощью threadLocal, чтобы экземпляр не использовался несколькими потоками.
public class ThreadLocalDemo { private static ThreadLocal<SimpleDateFormat> sdf = new ThreadLocal<>();
скопировать кодpublic static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(10); for (int i = 0; i < 100; i++) { executorService.submit(new DateUtil("2019-11-25 09:00:" + i % 60)); } } static class DateUtil implements Runnable { private String date; public DateUtil(String date) { this.date = date; } @Override public void run() { if (sdf.get() == null) { sdf.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); } else { try { Date date = sdf.get().parse(this.date); System.out.println(date); } catch (ParseException e) { e.printStackTrace(); } } } }
}
- Если текущий поток не содержит экземпляр объекта SimpleDateformat, создайте новый и установите его для текущего потока, если он уже существует, используйте его напрямую. Кроме того,от
if (sdf.get() == null){....}else{.....}
Видно, что назначение экземпляра объекта SimpleDateformat каждому потоку гарантируется на уровне приложения (логика бизнес-кода). - Выше мы сказали, что threadLocal может иметь утечки памяти, после его использования лучше всего использовать метод удаления для удаления этой переменной, как и при использовании подключения к базе данных, и вовремя закрыть соединение.
использованная литература