эта статьяgithub/JavaMapОн был включен, есть карта расширенных технических знаний Java-программистов и моя серия статей, добро пожаловать в Star.
Использование ThreadLocal не стандартизировано, и у учителя две строки слез
В группе был стажер, и, увидев этого молодого человека со светлым, полным энергии лицом и короткой прической, я обрадовался: он определенно потенциальный запас. Поэтому я попросил менеджера подать заявку, чтобы привести его лично. Чтобы помочь молодому человеку быстро повзрослеть, я дал ему требование. Это не требует, чтобы онлайн-проблема возникла всего через несколько дней после выхода в онлайн. Служба мониторинга обнаружила, что память медленно увеличивается, и первоначальное подозрение — утечка памяти.
Я узнал все PR стажеров, внимательно изучил их и, конечно же, нашел проблему. Поскольку внутренний код компании является конфиденциальным, вот простой сценарий восстановления демо (без учета проблем со стилем кода).
public class ThreadPoolDemo {
private static final ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>());
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 100; ++i) {
poolExecutor.execute(new Runnable() {
@Override
public void run() {
ThreadLocal<BigObject> threadLocal = new ThreadLocal<>();
threadLocal.set(new BigObject());
// 其他业务代码
}
});
Thread.sleep(1000);
}
}
static class BigObject {
// 100M
private byte[] bytes = new byte[100 * 1024 * 1024];
}
}
Анализ кода:
- Создайте пул потоков с основным числом потоков и максимальным числом потоков, равным 10, чтобы в пуле потоков всегда выполнялось 10 потоков.
- 100 задач отправляются в пул потоков с помощью цикла for.
- Определена переменная типа ThreadLocal, а тип Value — большой объект.
- Каждая задача будет помещать большой объект в переменную threadLocal, а затем выполнять другую бизнес-логику.
- Поскольку метод завершения работы пула потоков не вызывается, потоки в пуле потоков по-прежнему будут выполняться.
На первый взгляд кажется, что с этим кодом проблем нет, так почему же память остается высокой после обслуживания GC?
В коде потоку threadLocal назначается большой объект, но после выполнения бизнес-логики метод удаления не вызывается, наконец, большой объект, содержащийся в переменной threadLocals 10 потоков в пуле потоков, не освобождается, а память происходит утечка.
Скажите, может ли такой стажер остаться?
Где существует значение ThreadLocal?
Стажер сказал, что, по его мнению, объект, назначенный threadLocal, будет собираться JVM после завершения задачи потока, и ему интересно, почему произошла утечка памяти. Как учитель, я обязательно объясню ему принципы.
Класс ThreadLocal предоставляет методы set/get для хранения и получения значений значений, но на самом деле класс ThreadLocal не хранит значения значений. Реальное хранилище основано на классе ThreadLocalMap. ThreadLocalMap — это статический внутренний класс ThreadLocal. Его ключом является Объект экземпляра ThreadLocal, значение — любой объект Object.
Определение класса ThreadLocalMap
static class ThreadLocalMap {
// 定义一个table数组,存储多个threadLocal对象及其value值
private Entry[] table;
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);
}
// 定义一个Entry类,key是一个弱引用的ThreadLocal对象
// value是任意对象
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// 省略其他
}
Далее проанализируйте код класса ThreadLocal, чтобы увидеть, как методы set и get связаны со статическим внутренним классом ThreadLocalMap.
Метод набора классов ThreadLocal
public class ThreadLocal<T> {
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
// 省略其他方法
}
Логика set относительно проста, то есть получить ThreadLocalMap текущего потока, а затем добавить KV к карте, где K — текущий экземпляр ThreadLocal, а V — значение, которое мы передали. Здесь следует отметить, что получение карты нужно брать из объекта класса Thread, взгляните на определение класса Thread.
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
//省略其他
}
Класс Thread поддерживает ThreadLocalMap ссылок на переменные.
Метод получения класса ThreadLocal
get получает соответствующую приватную переменную текущего потока, которая является значением предыдущего набора или initialValue Код выглядит следующим образом:
class ThreadLocal<T> {
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();
}
}
Логический анализ кода:
- Получить экземпляр ThreadLocalMap текущего потока;
- Если он не пуст, используйте текущий экземпляр ThreadLocal в качестве ключа для получения значения;
- Если ThreadLocalMap пуст или значение, полученное из текущего экземпляра ThreadLocal, пусто, выполните setInitialValue();
Сводка отношений между классами, связанными с ThreadLocal
После прочтения приведенного выше анализа, не вызывает ли у вас головокружения взаимосвязь между Thread, ThreadLocal, ThreadLocalMap и Entry?
- Каждый поток является экземпляром Thread, который внутренне поддерживает член экземпляра threadLocals, тип которого — ThreadLocal.ThreadLocalMap.
- Создавая экземпляр ThreadLocal, мы можем установить некоторые частные переменные потока для текущего потока, к которым можно получить доступ, вызвав методы set и get ThreadLocal.
- ThreadLocal сам по себе не является контейнером, значение, к которому мы обращаемся, фактически хранится в ThreadLocalMap, а ThreadLocal используется только как ключ TheadLocalMap.
- Каждый экземпляр потока соответствует экземпляру TheadLocalMap. Мы можем создать несколько экземпляров ThreadLocal в одном потоке для хранения различных типов значений. Эти экземпляры ThreadLocal используются в качестве ключей, соответствующих их соответствующим значениям, и, наконец, сохраняются в массиве таблицы Entry.
- При вызове метода set/get ThreadLocal для операций присваивания/значения сначала получите экземпляр ThreadLocalMap текущего потока, а затем выполните размещение и получение точно так же, как при работе с обычной картой.
Принцип модели памяти ThreadLocal
После приведенного выше анализа мы очень четко понимаем дизайн класса, связанный с ThreadLocal.Давайте более подробно рассмотрим хранилище памяти ThreadLocal с помощью рисунка.
Левая сторона — это стек, а правая — куча. Память, используемая некоторыми локальными переменными и ссылками потока, относится к области Stack (стек), тогда как обычные объекты хранятся в области Heap (куча).
- Когда поток выполняется, определенный нами объект TheadLocal инициализируется и сохраняется в куче, а область стека, в которой выполняется поток, сохраняет ссылку на экземпляр, которым на рисунке является ThreadLocalRef.
- При вызове set/get ThreadLocal виртуальная машина находит соответствующий экземпляр в области кучи по ссылке текущего потока, то есть CurrentThreadRef, а затем проверяет, создан ли используемый для нее экземпляр TheadLocalMap. нет, он будет создан и инициализирован.
- После создания экземпляра Map можно получить дескриптор ThreadLocalMap, после чего текущий объект ThreadLocal можно использовать в качестве ключа для операций доступа.
- Пунктирная линия на рисунке указывает, что ссылка на экземпляр ThreadLocal, соответствующий ключу, является слабой ссылкой.
Концепция сильных ссылок и слабых ссылок
Ключ ThreadLocalMap является слабым ссылочным типом, исходный код выглядит следующим образом:
static class ThreadLocalMap {
// 定义一个Entry类,key是一个弱引用的ThreadLocal对象
// value是任意对象
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// 省略其他
}
Ниже поясняется несколько общих справочных концепций.
сильная цитата
были живы: Для таких ссылок, как «Object obj=new Object()», пока существует сильная ссылка, сборщик мусора никогда не вернет экземпляр объекта, на который указывает ссылка.
слабая ссылка
Переработка умрет: Экземпляры объектов, связанные со слабыми ссылками, могут существовать только до следующей сборки мусора. Когда сборщик мусора работает, экземпляры объектов, связанные только со слабыми ссылками, собираются независимо от того, достаточно ли текущей памяти. После JDK 1.2 для реализации слабых ссылок предоставляется класс WeakReference.
мягкая ссылка
есть шанс жить: объект, связанный с программной ссылкой, будет указан в диапазоне восстановления для второго восстановления до того, как система сгенерирует исключение переполнения памяти. Если для этой коллекции недостаточно памяти, будет выдано исключение нехватки памяти. После JDK 1.2 для реализации программных ссылок предоставляется класс SoftReference.
фантомная ссылка
Также известны как призрачные ссылки или фантомные ссылки., что является самым слабым типом отношения ссылки. Наличие у экземпляра объекта виртуальной ссылки никак не повлияет на время его жизни, а экземпляр объекта нельзя получить через виртуальную ссылку. Единственная цель установки ассоциации виртуальной ссылки для объекта — получение системного уведомления, когда экземпляр объекта повторно используется сборщиком. После JDK 1.2 для реализации фиктивных ссылок предоставляется класс PhantomReference.
Являются ли утечки памяти источником слабых ссылок?
На первый взгляд основной причиной утечки памяти является использование слабых ссылок, но стоит задуматься и над другим вопросом: почему ThreadLocalMap использует слабые ссылки вместо сильных?
Посмотрите официальную документацию сайта:
Чтобы помочь справиться с очень большими и длительными использованиями, записи хеш-таблицы используют WeakReferences для ключей. Для обработки очень больших и долгосрочных применений записи хэш-таблицы используют слабые ссылки в качестве ключей.
Обсудить в двух случаях:
(1) В ключе используются сильные ссылки
Объект, ссылающийся на ThreadLocal, перерабатывается, но ThreadLocalMap по-прежнему содержит строгую ссылку на ThreadLocal. Если его не удалить вручную, ThreadLocal не будет переработан, что приведет к утечке памяти Entry.
(2) В ключе используется слабая ссылка
Объект, ссылающийся на ThreadLocal, перерабатывается. Поскольку ThreadLocalMap содержит слабую ссылку на ThreadLocal, ThreadLocal будет переработан, даже если он не удален вручную. значение будет очищено при следующем вызове ThreadLocalMap set, get, remove.
Сравнивая два случая, мы можем обнаружить, что: поскольку жизненный цикл ThreadLocalMap такой же длинный, как и у Thread, если соответствующий ключ не будет удален вручную, это приведет к утечке памяти, но использование слабой ссылки может обеспечить дополнительный уровень защита: после очистки слабой ссылки ThreadLocal ключ имеет значение null, соответствующее значение может быть очищено при следующем вызове ThreadLocalMap set, get и remove.
Таким образом, основной причиной утечки памяти ThreadLocal является следующее: поскольку жизненный цикл ThreadLocalMap такой же длинный, как и у Thread, если соответствующий ключ не будет удален вручную, это приведет к утечке памяти, а не из-за слабых ссылок.
Рекомендации ThreadLocal
В предыдущих сводках мы проанализировали дизайн классов и модель памяти ThreadLocal, а также сосредоточились на условиях и конкретных сценариях утечек памяти. Наконец, в сочетании с опытом работы в проекте приведены рекомендуемые сценарии использования ThreadLocal:
- Когда вам нужно хранить частные переменные потока.
- Когда вам нужно реализовать потокобезопасные переменные.
- Когда необходимо уменьшить конкуренцию за ресурсы потока.
Основываясь на приведенном выше анализе, мы можем понять причины и последствия утечек памяти ThreadLocal, так как же избежать утечек памяти?
Ответ таков: после каждого использования ThreadLocal рекомендуется вызывать его метод remove() для очистки данных.
Кроме того, следует подчеркнуть, что не все места, где используется ThreadLocal, должны удалять() в конце, потому что их жизненный цикл может быть таким же продолжительным, как жизненный цикл проекта, поэтому необходимо сделать соответствующий выбор, чтобы избежать бизнес-процессов. логические ошибки. !
-- END --
Ежедневная похвала: привет, технари, сначала хвалите, а затем наблюдайте, чтобы выработать привычку, и сохраняйте первоначальное намерение верить в технологии.
Введение: Блогер окончил Хуачжунский университет науки и технологий со степенью магистра, он программист со страстью к технологиям и страстью к жизни. За последние несколько лет он побродил по многим интернет-компаниям первой линии и имеет многолетний опыт разработки.
Публичный номер поиска в Wechat [Архитектор, который любит смеяться], у меня есть технологии и истории, которые ждут вас.
Статья постоянно обновляется вgithub/JavaMapВы можете увидеть мою заархивированную серию статей в , с опытом интервью и техническими галантерейными товарами, добро пожаловать в Star.