Эта статья призвана прояснить принцип ThreadLocal и объяснить его ясно. Полный текст в основном завершает следующие четыре части:
- Узнайте, как ThreadLocal может предотвратить доступ других потоков к значениям set() и get() в разных потоках;
- Добавлено применение слабой ссылки в ThreadLocalMap;
- Изучили, как ThreadLocalMap реализует функцию хеш-карты;
- Перечислил и проанализировал проблему утечки памяти, вызванную использованием ThreadLocal;
Во-первых, давайте посмотрим, что может сделать ThreadLocal:
public class ThreadLocalDemo {
public static void main(String[] args) {
final ThreadLocal<Integer> local = new ThreadLocal<>();
local.set(100);
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " local: " + local.get());
}
});
t.start();
System.out.println("Main local: " + local.get());
}
}
Результат печати следующий:
Thread-0 local: null
Main local: 100
Значение локального набора в основном потоке можно получить, вызвав метод get в основном потоке, но когда метод get вызывается в потоке t, результат равен нулю.
Далее в этой статье используется метод set, вызываемый локально, в качестве записи для изучения причин такого результата.
set() основы
В наборе исходного кода Threadlocal Code () реализован так:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
Сначала получите объект потока, в котором в данный момент выполняется оператор local.set(), то есть t, а затем получите объект ThreadLocalMap, удерживаемый t, с помощью локального getMap() и введите исходный код класса Thread для просмотра, который содержит поле с именем threadLocals :
ThreadLocal.ThreadLocalMap threadLocals = null;
И глядя на исходный код GetMap (), он возвращает ThreadLoCals:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
map != null
Если map! = Null, выполните map.set (это, значение), где это локально.
Конкретная реализация ThreadLocalMap будет расширена позже.Здесь мы кратко поймем ее как структуру данных для хранения данных в парах ключ-значение.Тогда мы легко обнаружим, что локальная по-прежнему локальная, и локальная копия не создается в каждый поток, но вызывается набор.При вызове метода сохраните его и входящее значение в объекте ThreadLocalMap, хранящемся каждым потоком, в виде пары ключ-значение.
map == null
Если map == null, выполнить createMap(t, value), исходный код выглядит следующим образом:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
Создайте объект ThreadLocalMap и назначьте его threadLocals.
Пока что основной принцип ThreadLocal очень ясен:Каждый поток работает с общим экземпляром ThreadLocal, фактически экземпляр используется в качестве ключа для работы с внутренним объектом ThreadLocalMap..
Помимо set(), ThreadLocal также предоставляет такие операции, как get(), remove() и т. д. Реализация относительно проста, поэтому этого недостаточно.
Структура ThreadLocalMap
Чтобы действительно понять ThreadLocal, вам также необходимо знать, что такое ThreadLocalMap.
Он представлен в комментариях: ThreadLocalMap — это настраиваемая хеш-карта, подходящая только для поддержки локальных значений потока.
ThreadLocalMap — это пользовательская карта, статический внутренний класс с хеш-функцией, не имеющий ничего общего с классом Map, предоставляемым в пакете java.util. Внутри находится статический класс Entry, который подробно анализируется ниже.
Принцип реализации входа
Во-первых, код класса выглядит следующим образом:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
Цитируя примечание, данное в коде здесь: записи в этой хеш-карте расширяют WeakReference, используя его основное поле ref в качестве ключа (который всегда является объектом ThreadLocal).Обратите внимание, что нулевые ключи (т.е. entry.get() == null) означает, что ключ больше не упоминается.
Первое предложение на самом деле говорит нам, что запись наследуется от WeakReference и использует поле, на которое ссылается основной метод, в качестве ключа записи.
Второе предложение означает, что когда entry.get() == null, это означает, что на ключ больше не будет ссылок.
Эти два комментария будут проанализированы позже.
Слабые справочные основы
Прежде чем приступить к этому резюме, необходимо уяснить две вещи:
- Что слабое. «Глубимое понимание виртуальной машины Java», пишет: «Слабый эталонный объект ассоциируется только для выживания, пока не возникает следующая коллекция мусора, когда коллектор мусора, независимо от адекватности текущей памяти, будет восстанавливаться, только чтобы быть слабыми ссылочные объекты, связанные ».
- Что такое эталонная передача параметров, что относится к базовым знаниям Java SE, поэтому я не буду вдаваться в подробности.
Затем сначала прочитайте исходный код.Когда конструктор передает параметры, k, представляющий ключ, будет передан в super(), то есть он сначала выполнит конструктор родительского класса:
public WeakReference(T referent) {
super(referent);
}
Конструктор WeakReference продолжает сначала вызывать конструктор родительского класса:
Reference(T referent) {
this(referent, null);
}
Reference(T referent, ReferenceQueue<? super T> queue) {
this.referent = referent;
this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}
Кроме того, мы не видим нативных методов в классе Reference, но можем видеть некоторые методы экземпляра, такие как get(), о которых мы поговорим позже.
В этот раз буду интересоваться как реализована функция слабой ссылки.В комментарии есть такое слово:"особая обработка сборщиком мусора".Видно что реализация функции WeakReference передана в сборщик мусора для обработки, поэтому здесь он не будет разворачиваться. , вы можете обратиться по ссылке в конце статьи, если вам интересно. Здесь нам просто нужно понять, как используется WeakReference.
Слабые и сильные ссылки не используются одинаково, вот пример слабой ссылки:
public class WeakReferenceDemo {
public static void main(String[] args) {
WeakReference<Fruit> fruitWeakReference = new WeakReference<>(new Fruit());
// Fruit f = fruitWeakReference.get();
if (fruitWeakReference.get() != null) {
System.out.println("Before GC, this is the result");
}
System.gc();
if (fruitWeakReference.get() != null) {
System.out.println("After GC, fruitWeakReference.get() is not null");
} else {
System.out.println("After GC, fruitWeakReference.get() is null");
}
}
}
class Fruit {
}
Результат выглядит следующим образом:
Before GC, this is the result
After GC, fruitWeakReference.get() is null
Через fruitWeakReference.get() можно получить объект, на который указывает слабая ссылка, после выполнения System.gc() объект перерабатывается.
Используйте диаграмму, чтобы представить взаимосвязь между сильными и слабыми ссылками:
Чтобы было ясно, такие ссылки, как "Object obj = new Object()", являются сильными ссылками, поэтому fruitWeakReference является строгой ссылкой. В настоящее время он указывает на объект WeakReference. Когда мы создаем этот объект, мы также передаем его после нового Объект Fruit создан, целью всей строки кода является создание слабой ссылки на этот объект Fruit. И эта слабая ссылка находится в объекте, на который указывает fruitWeakReference.
Если использовать грубую аналогию, слабая ссылка подобна коту Шрёдингера. Мы хотим знать его состояние, но не можем вызвать его самого, чтобы наблюдать за ним через обычный код Java. Комментарий в виде строки удаляется, а переменная f используется для указания к fruitWeakReference.get(), но он просто указывает сильную ссылку на объект, на который изначально указывает слабая ссылка.В это время запустите программу и получите следующие результаты:
Before GC, this is the result
After GC, fruitWeakReference.get() is not null
Process finished with exit code 0
Поскольку на объект строго ссылаются, он не будет собираться мусором.
Слабая ссылка на ключ Entry
С предыдущим фундаментом легко понять принцип построения Entry. Для иллюстрации предположим, что мы можем создать объект Entry с помощью следующего кода:
Entry entry = new Entry(local, 100);
В настоящее время соотношение между сильными и слабыми ссылками выглядит следующим образом:
На этом этапе вы можете понять два предыдущих комментария: запись наследуется от WeakReference и поддерживает внутреннюю слабую ссылку, указывая на объект, на который указывает local в основном методе; entry.get() возвращает объект, на который указывает слабая ссылка. , если entry.get() == null, что, естественно, означает, что на ключ больше не будет ссылок.
Поэтому, в отличие от класса Entry обычного Map, при создании экземпляра Entry ThreadLocalMap ключ является слабой ссылкой, поэтому базовая структура ThreadLocalMap внутри ThreadLocal ясна.
установить () продвинутый
Снова опубликуйте исходный код set() в 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);
}
Обратите внимание на оператор в строке 5. Когда локальный вызов set(), как только переменная threadLocals типа ThreadLocalMap, содержащаяся в текущем объекте потока, не равна нулю, будет выполнена строка map.set(this, value). ThreadLocalMap, в этом разделе основное внимание будет уделено методу операции set() в ThreadLocalMap.
Исходный код set() приведен ниже:
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 计算出hash表的位置i
int i = key.threadLocalHashCode & (len-1);
// 处理set方法关键逻辑
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 在hash表中保存新生成的Entry对象
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
В коде i — это индекс хеш-таблицы (также известной как хеш-ведро), то есть место, где хранится только что установленная запись.Конечно, перед сохранением требуется операция сравнения. threadLocalHashCode получается следующим образом:
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
private final int threadLocalHashCode = nextHashCode();
Для лучшего хеширования используется 0x61c88647. Всякий раз, когда новый объект ThreadLocal вызывает threadLocalHashCode, последний автоматически увеличивает значение 0x61c88647. Что касается того, почему 0x61c88647 может обеспечить лучшее хэширование, оно включает алгоритм хеширования Фибоначчи (инверсия двоичной формы этого числа и добавление 1 является константой хеширования Фибоначчи), конкретные детали можно перейти по справочной ссылке в конце статья.
Конечно, очень просто выполнить битовую операцию перед вычислением i. Например, до расширения len равно 16 (2 в 4-й степени), тогда двоичная форма len - 1 равна 1111, а побитовое И равно взять последние четыре бита.
Для предотвращения столкновений и конфликтов здесь используется метод линейного обнаружения, а метод застежки-молнии не используется. Правила индексации зондов следующие:
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
Логика выполнения цикла for следующая:
- Во-первых, получите вкладку элемента Entry [i], чья позиция индекса хеш-таблицы равна i;
- Судя по тому, является ли tab[i] нулевым, если tab[i] имеет значение null, это означает, что перед этой позицией нет экземпляра Entry, выйти из цикла и сохранить вновь сгенерированный объект Entry в этой позиции в хеш-таблице;
- Если tab[i] не равен нулю или имеется ключ, указывающий на тот же объект, в этом случае измените значение на значение, которое необходимо установить; или слабая ссылка указывает на значение null, если это случае выполнить метод replaceStaleEntry;
- Используйте метод nextIndex, чтобы изменить значение i, и перейдите ко второму шагу, чтобы продолжить оценку;
После выхода из цикла и сохранения вновь сгенерированного объекта Entry в соответствующей позиции хеш-таблицы размер также увеличится на 1, и снова будет выполнена обработка rehash() при условии, что !cleanSomeSlots(i, sz) && sz >= порог.
Основные функции replaceStaleEntry и cleanSomeSlots — удалить запись, чья слабая ссылка равна null.Время поиска последнего составляет log2(n), которое не будет расширено из-за нехватки места.Предустановленные функции, определенные в threshold и HashMap, аналогичны , в основном: Для расширения вот len * 2/3.
очистка памяти
По-прежнему используя исходный пример, если для local установлено значение null, новый объект ThreadLocal будет слабо ссылаться на экземпляр ThreadLocalMap в потоке.В это время, пока вызывается System.gc(), объект будет переработан. в следующей сборке мусора. Что делать, если вы хотите активно ломать слабые ссылки? Java предоставляет следующие методы:
clear()
Это абстрактный класс, предоставляющий метод ссылки.
Далее используйте пример, чтобы обсудить возможные утечки памяти в ThreadLocal.
пример утечки памяти
Исходный код примера выглядит следующим образом:
public class ThreadLocalTest throws InterruptedException{
public static void main(String[] args) {
MyThreadLocal<Create50MB> local = new MyThreadLocal<>();
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 5, 1,
TimeUnit.MINUTES, new LinkedBlockingQueue<Runnable>());
for (int i = 0; i < 5; i++) {
final int[] a = new int[1];
final ThreadLocal[] finallocal = new MyThreadLocal[1];
finallocal[0] = local;
a[0] = i;
poolExecutor.execute(new Runnable() {
@Override
public void run() {
finallocal[0].set(new Create50MB());
System.out.println("add i = " + a[0]);
}
});
}
Thread.sleep(50000);
local = null;
}
static class Create50MB {
private byte[] bytes = new byte[1024 * 1024 * 50];
}
static class MyThreadLocal<T> extends ThreadLocal {
private byte[] bytes = new byte[1024 * 1024 * 500];
}
}
Давайте сначала поговорим об идее дизайна этой небольшой программы:
Целью этой программы является создание ситуации утечки памяти: когда пул потоков находится в состоянии ожидания после выполнения текущей задачи, установить для локального значения значение null и перезапустить объект MyThreadLocal, вновь созданный в начале основного метода. экземпляр ThreadLocalMap одного потока в пуле потоков является слабой ссылкой на этот объект MyThreadLocal, но значение, хранящееся внутри, по-прежнему строго указано и не может быть повторно использовано.
В этой программе мы настроили MyThreadLocal, чтобы размер нового объекта MyThreadLocal достигал 500 МБ; Create50MB — это созданный пакет емкости, а последнее значение, удерживаемое каждым потоком, — это объект Create50MB размером 50 МБ; пул потоков также является передачей настраиваемых параметров, поэтому для достижения лучшего контроля он может работать с 5 потоками одновременно; в цикле for используются две временные переменные, чтобы избежать языкового ограничения, согласно которому анонимные внутренние классы должны объявлять внешние переменные как final .
Запустите программу, рабочий статус показан на следующем рисунке:
Размер используемой кучи составляет 750 МБ, что соответствует ожиданиям.Новый объект MyThreadLocal составляет 500 МБ, и есть пять потоков, каждый из которых имеет размер 50 МБ, что в сумме составляет 750 МБ.
Через 50 секунд установите для параметра local значение null. В это время уже нет строгой ссылки на новый объект MyThreadLocal. В это время выполняется сборка мусора. Результаты следующие:
Используемый размер кучи становится равным 250 МБ. Сам по себе этот результат не доказывает наличие слабой ссылки на объект MyThreadLocal в каждом потоке, но строгой ссылки быть не должно.
Я уже изучал исходный код пула потоков. Потоки в пуле потоков не уничтожаются после выполнения задачи. В этом примере они находятся в состоянии ожидания. Поэтому программа всегда поддерживается на уровне 250 МБ и не может быть освобождена. ● Как только условия в программе изменяются достаточно сильно, могут возникнуть серьезные проблемы с производительностью. Решение обычно состоит в том, чтобы вызвать метод удаления ThreadLocal в потоке.На самом деле публичных API, предоставляемых ThreadLocal, не так много, но этого метода достаточно для решения проблемы.
резюме
Я должен сказать, что благодаря анализу ThreadLocal я многому научился. Вся статья написана на одном дыхании (поэтому тоже может содержать ошибки), и предполагается, что если в будущем возникнет необходимость в приватной установке общих переменных, то можно будет и обратиться к этому методу для написания; я знал только о четыре ссылки ранее, на этот раз нужно выяснить, как его использовать; использовать линейное обнаружение для решения конфликта коллизий хеш-таблицы, который отличается от HashMap и также является функцией ThreadLocal; утечка памяти, указанная в конце это настоящая битва за контент, написанный выше.
cool.
Ссылаться на
Принцип и реализация JVM - Справочник
What is the meaning of 0x61C88647 constant in ThreadLocal.java
Напечатать GC: -XX:+PrintGCDetails, более наглядно:Параметры виртуальной машины, используемые при просмотре журналов сборщика мусора