Углубленный анализ сценариев использования ThreadLocal, принципов реализации и дизайнерских идей.

Предисловие

ThreadLocal можно использовать для хранения локальных данных потоков для изоляции данных потока.

Неправильное использование ThreadLocal может привести к утечкам памяти.Устранение утечек памяти требует не только знания JVM, умелого использования различных инструментов анализа, но и кропотливой работы.

Если вы сможете понять его принцип и правильно его использовать, это не приведет к различным несчастным случаям.

В этой статье будет проанализирован ThreadLocal с аспектов сценариев использования, принципов реализации, утечек памяти, дизайнерских идей и т. д., а также поговорим о InheritableThreadLocal.

Сценарии использования ThreadLocal

Что такое контекст?

Например, когда поток обрабатывает запрос, запрос будет проходить через процесс MVC. Поскольку процесс очень длинный, он будет проходить через множество методов. Эти методы можно назвать контекстами.

ThreadLocal используется для хранения часто используемых данных, информации о сеансе, хранения локальных переменных потока и т. д. в контексте.

Например, используйте перехватчик для получения информации о вошедшем в систему пользователе через токен в запросе перед обработкой запроса и сохраните информацию о пользователе в ThreadLocal, чтобы информацию о пользователе можно было получить непосредственно из ThreadLocal во время последующей обработки запроса.

Если поток будет использоваться повторно, во избежание путаницы данных данные должны быть удалены после использования (после обработки перехватчиком).

Обычно используемые методы ThreadLocal: set(), get(), remove()соответствующие хранению, получению и удалению соответственно.

ThreadLocal можно поместить в класс инструментов для удобства использования.

public class ContextUtils {
    public static final ThreadLocal<UserInfo> USER_INFO_THREAD_LOCAL = new ThreadLocal();
}

Псевдокод перехватчика

//执行前 存储
public boolean postHandle(HttpServletRequest request)  {
    //解析token获取用户信息
	String token = request.getHeader("token");
	UserInfo userInfo = parseToken(token);   
	//存入
	ContextUtils.USER_INFO_THREAD_LOCAL.set(userInfo);
    
    return true;
}


//执行后 删除
public void postHandle(HttpServletRequest request)  {
    ContextUtils.USER_INFO_THREAD_LOCAL.remove();
}

при его использовании

//提交订单
public void orderSubmit(){
    //获取用户信息
    UserInfo userInfo = ContextUtils.USER_INFO_THREAD_LOCAL.get();
    //下单
    submit(userInfo);
    //删除购物车
    removeCard(userInfo);
}

Чтобы лучше использовать ThreadLocal, мы должны понимать принципы его реализации и избегать несчастных случаев, вызванных неправильным использованием.

ThreadLocalMap

Поток. В потоке есть два поля, в которых хранится внутренний класс ThreadLocal ThreadLocalMap.

public class Thread implements Runnable {    
    
    ThreadLocal.ThreadLocalMap threadLocals = null;
    
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}

threadLocals используется для реализации ThreadLocal.

inheritableThreadLocals используется для реализации InheritableThreadLocal (наследуемый ThreadLocal будет обсуждаться позже)

изображение.png

Реализация ThreadLocalMap представляет собой хеш-таблицу, а ее внутренний класс Entry является узлом хеш-таблицы.Хеш-таблица ThreadLocalMap реализуется массивом Entry.

public class ThreadLocal<T> {
    //,,,
	static class ThreadLocalMap {
        //...
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
	}
}

Ключом в конструкции узла является ThreadLocal, а значением — значение, которое необходимо сохранить.

В то же время узел наследует слабые ссылки. С помощью дженериков и конструкторов вы можете знать, что он устанавливает для ThreadLocal слабую ссылку.

Студенты, которые не понимают слабые ссылки, могут прочитать эту статью: Углубленное введение в JVM (14). Переполнения памяти, утечки и ссылки.)

изображение.png

набор

В способе хранения данных

изображение.png

Получить ThreadLocalMap, если нет, инициализировать ThreadLocalMap (отложенная загрузка)

public void set(T value) {
    //获取当前线程
    Thread t = Thread.currentThread();
    //获取当前线程的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
   
    if (map != null) {
        //添加数据
        map.set(this, value);
    } else {
        //没有就初始化
        createMap(t, value);
    }
}

создать карту

Создайте ThreadLocalMap и назначьте его threadLocals текущего потока.

	void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

Создайте ThreadLocalMap и инициализируйте массив длиной 16.

	ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        //初始化数组 16
        table = new Entry[INITIAL_CAPACITY];
        //获取下标
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        //构建节点
        table[i] = new Entry(firstKey, firstValue);
        //设置大小
        size = 1;
        //设置负载因子
        setThreshold(INITIAL_CAPACITY);
   }

ThreadLocalMap.set

Получите индекс посредством хеширования. При возникновении конфликта хеширования пройдите по хеш-таблице (больше не используя метод цепочки адресов), пока в позиции не останется узла, а затем постройте.

Если во время обхода есть узел, ключ будет вынесен в соответствии с узлом для сравнения. Если да, то он будет перезаписан. Если узел не имеет ключа, это означает, что ThreadLocal узла был переработан (истек срок действия).Во избежание утечек памяти узел будет очищен.

Наконец, он проверит, есть ли просроченные узлы в других местах, очистит их и проверит наличие расширения.

private void set(ThreadLocal<?> key, Object value) {

    //获取哈希表
    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)]) {
        //获取key
        ThreadLocal<?> k = e.get();
		//key如果存在则覆盖
        if (k == key) {
            e.value = value;
            return;
        }
		//如果key不存在 说明该ThreadLocal以及不再使用(GC回收),需要清理防止内存泄漏
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    //构建节点
    tab[i] = new Entry(key, value);
    //计数
    int sz = ++size;
    //清理其他过期的槽,如果满足条件进行扩容
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

При получении значения хеш-функции используйте атомарный класс, который увеличивает значение хеш-функции, а размер шага — это количество приращений каждый раз (возможно, после исследования и тестирования, чтобы минимизировать конфликты хеш-функции).

	//获取哈希值
    private final int threadLocalHashCode = nextHashCode();
    //哈希值自增器
    private static AtomicInteger nextHashCode =
        new AtomicInteger();
    //增长步长
    private static final int HASH_INCREMENT = 0x61c88647;

    //获取哈希值
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

nextIndex предназначен для получения следующего индекса и возвращается к 0 при превышении верхнего предела.

		private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

получать

при получении данных

изображение.png

Получите ThreadLocalMap текущего потока, инициализируйте его, если он пуст, в противном случае получите узел

	public T get() {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取线程的ThreadLocalMap
        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();
    }

При получении узла сначала получите индекс на основе значения хеша, затем проверьте узел и сравните ключ, если он не совпадает, то это означает, что срок действия ключа истек и может произойти утечка памяти.Нужно очистить хэш стол.

		private Entry getEntry(ThreadLocal<?> key) {
            //获取下标
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            //如果匹配 则返回
            if (e != null && e.get() == key)
                return e;
            else
                //匹配不到 去清理
                return getEntryAfterMiss(key, i, e);
        }

утечка памяти

В процессе настройки и получения данных мы проверим, истек ли срок действия ключа, если срок его действия истек, очистите его.

Фактически, неправильное использование ThreadLocal может привести к утечкам памяти.

Чтобы избежать утечек памяти, вызванных неправильным использованием, дизайнеры стараются очистить устаревшие ThreadLocals обычными методами.

Как упоминалось ранее, узлы наследуют слабые ссылки и устанавливают ключ слабых ссылок в конструкции (то есть ThreadLocal).

Когда ThreadLocal нигде не используется, следующий сборщик мусора установит пустой ключ узла.

Если значение больше не используется, но поскольку узел Entry (null, value) существует, значение не может быть переработано, что приводит к утечке памяти.

изображение.png

Поэтому после использования данных попробуйте использовать команду Remove, чтобы удалить их.

Кроме того, дизайнеры будут проверять узлы с пустыми ключами и удалять их обычными методами, такими как set, get и delete, чтобы избежать утечек памяти.

Дизайнерское мышление

Почему для ключа в записи, то есть ThreadLocal, должна быть установлена ​​слабая ссылка?

Давайте сначала представим себе сценарий: потоки часто повторно используются в наших сервисах, а в некоторых сценариях ThreadLocal не используется в течение длительного времени.

Если ключ и значение записи узла являются сильными ссылками, то после того, как ThreadLocal больше не используется, ThreadLocal по-прежнему хранится в узле как сильная ссылка и не может быть переработан, что эквивалентно утечке памяти.

После установки для ThreadLocal слабой ссылки утечки памяти все равно будут происходить, если значение больше не используется в этом сценарии.Поэтому в методах set, get и delete узлы с пустыми ключами будут проверяться и удаляться, чтобы избежать утечек памяти.

Поскольку значение не может быть переработано, почему бы не установить его в качестве слабой ссылки?

Поскольку значение хранит данные, изолированные от потока, если для значения установлена ​​слабая ссылка, когда внешний уровень не использует объект, соответствующий значению, у него не будет сильной ссылки, и в следующий раз GC будет перезапущен, что приведет к потере данных.

НаследуемыйПотокЛокальный

InheritableThreadLocal наследует ThreadLocal и используется для передачи переменных потока между родительским и дочерним потоками.

	public void testInheritableThreadLocal(){
        InheritableThreadLocal<String> itl = new InheritableThreadLocal<>();

        itl.set("main");

        new Thread(()->{
            //main
            System.out.println(itl.get());
        }).start();
    }

Как упоминалось ранее, для InheritableThreadLocal используется другой ThreadLocalMap в потоке.

Если при создании потока inheritableThreadLocals в родительском потоке не пусто, оно передается

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        //....
    
        //如果父线程中inheritableThreadLocals 不为空 则传递
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        
    }

Подведем итог

ThreadLocal используется для изоляции данных между потоками и может хранить данные для использования в контексте. Поскольку потоки могут использоваться повторно, их необходимо удалять после использования, чтобы избежать путаницы в данных.

ThreadLocalMap хранится в потоке Thread. ThreadLocalMap — это хэш-таблица, которая использует метод открытой адресации для разрешения конфликтов хэша. Ключом хранилища узла является ThreadLocal, а значение хранит данные, которые будут храниться в потоке.

Узел наследует слабую ссылку и устанавливает ThreadLocal как слабую ссылку. Это приведет к повторному использованию ThreadLocal в следующий раз, когда сборщик мусора больше не будет использоваться. В это время ключ будет пустым. Если значение больше не используется, но узел не удаляется, это приведет к использованию значения, что приведет к утечке памяти

В обычных методах ThreadLocal, таких как set, get, delete и т. д., при обходе массива вернитесь назад и удалите узлы с истекшим сроком действия (ключ пуст), чтобы избежать утечек памяти.

Если для ThreadLocal установлена ​​сильная ссылка, утечки памяти будут происходить, когда ThreadLocal больше не используется; когда для ThreadLocal установлена ​​слабая ссылка, хотя утечки памяти также могут произойти, эти данные можно проверить и очистить с помощью общих методов; если значение установлен как слабая ссылка, потеря данных произойдет, если внешний уровень не использует значение

InheritableThreadLocal наследует ThreadLocal и используется для передачи данных ThreadLocal между родительским и дочерним потоками.

Наконец (не делайте это бесплатно, просто нажмите три раза подряд, чтобы попросить о помощи~)

Эта статья включена в колонку « От точки к линии и от линии к поверхности», чтобы упростить создание системы знаний по параллельному программированию на Java . Заинтересованные студенты могут продолжать обращать внимание.

Примечания и примеры из этой статьи включены в gitee-StudyJava и github-StudyJava . Заинтересованные студенты могут продолжать обращать внимание на статью ниже.

Адрес дела:

Gitee-JavaConcurrentProgramming/src/main/java/G_ThreadLocal

Github-JavaConcurrentProgramming/src/main/java/G_ThreadLocal

Если у вас есть какие-либо вопросы, вы можете обсудить их в области комментариев. Если вы считаете, что текст Цай Цая хорош, вы можете поставить лайк, подписаться на него и собрать его, чтобы поддержать его ~

Следуйте за Цай Цаем и делитесь более полезной информацией, общедоступный аккаунт: внутренняя кухня Цай Цая.

Эта статья опубликована OpenWrite, блогом, в котором публикуется множество статей !

Лэй Цзюнь: Официальная версия новой операционной системы Xiaomi ThePaper OS упакована. Всплывающее окно на странице лотереи приложения Gome App оскорбляет ее основателя. Правительство США ограничивает экспорт графических процессоров NVIDIA H800 в Китай. Интерфейс Xiaomi ThePaper OS Мастер использовал Scratch для очистки симулятора RISC-V, и он успешно запустился Ядро Linux Выпущено удаленный рабочий стол RustDesk 1.2.3, улучшена поддержка Wayland После отключения USB-приемника Logitech произошел сбой ядра Linux DHH острый обзор «упаковочных инструментов » »: интерфейс вообще не нужно собирать (без сборки) JetBrains запускает Writerside для создания технической документации. Официально выпущены инструменты для Node.js 21.
{{o.name}}
{{м.имя}}

рекомендация

отmy.oschina.net/u/6903207/blog/10114997
рекомендация