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

интервью Java
Использование и принцип ThreadLocal

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

Однако независимо от того, какая блокировка, оптимистическая блокировка или пессимистическая блокировка, будет оказывать определенное влияние на производительность при возникновении конфликта параллелизма.

Есть ли способ полностью избежать конкуренции?

Ответ — да, это ThreadLocal.

Буквально ThreadLocal можно интерпретировать как локальную переменную потока, то есть переменная ThreadLocal может быть доступна только самому текущему потоку и не может быть доступна другим потокам, поэтому конкуренция потоков естественным образом избегается.

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

Основное использование ThreadLocal

Создайте объект ThreadLocal:

private ThreadLocal<Integer> localInt = new ThreadLocal<>();

Приведенный выше код создает переменную localInt.Поскольку ThreadLocal является универсальным классом, тип localInt указывается как целое число.

Ниже показано, как установить и получить значение этой переменной:

public int setAndGet(){
    localInt.set(8);
    return localInt.get();
}

Приведенный выше код устанавливает значение переменной равным 8, а затем извлекает это значение.

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

private ThreadLocal<Integer> localInt = ThreadLocal.withInitial(() -> 6);

Приведенный выше код устанавливает начальное значение ThreadLocal равным 6, которое видно всем потокам.

Принцип реализации ThreadLocal

Переменные ThreadLocal видны только внутри одного потока, так как же это сделать? Начнем с самого простого метода get():

public T get() {
    //获得当前线程
    Thread t = Thread.currentThread();
    //每个线程 都有一个自己的ThreadLocalMap,
    //ThreadLocalMap里就保存着所有的ThreadLocal变量
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        //ThreadLocalMap的key就是当前ThreadLocal对象实例,
        //多个ThreadLocal变量都是放在这个map中的
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            //从map里取出来的值就是我们需要的这个ThreadLocal变量
            T result = (T)e.value;
            return result;
        }
    }
    // 如果map没有初始化,那么在这里初始化一下
    return setInitialValue();
}

Как видите, в карте каждого потока хранится так называемая переменная ThreadLocal. Эта карта представляет собой поле threadLocals в объекте Thread. следующее:

ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocal.ThreadLocalMap — это специальная карта, ключ каждой записи которой является слабой ссылкой:

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;
    //key就是一个弱引用
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

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

Понимание проблем утечки памяти в ThreadLocal

Хотя ключ в ThreadLocalMap является слабой ссылкой, он будет автоматически переработан при отсутствии внешней сильной ссылки, но значение в Entry по-прежнему является сильной ссылкой. Цепочка ссылок для этого значения выглядит следующим образом:

Видно, что это значение имеет шанс быть перезапущенным только при повторном использовании потока, в противном случае, пока поток не завершится, всегда будет сильная ссылка на значение. Однако требование выхода из каждого потока является чрезвычайно строгим. Для пула потоков большинство потоков всегда будет существовать в течение всего жизненного цикла системы. В этом случае может произойти утечка объекта значения. Метод обработки заключается в том, что когда ThreadLocalMap выполняет set(), get() и remove(), он будет очищен:

Возьмите getEntry() в качестве примера:

private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        //如果找到key,直接返回
        return e;
    else
        //如果找不到,就会尝试清理,如果你总是访问存在的key,那么这个清理永远不会进来
        return getEntryAfterMiss(key, i, e);
}

Вот реализация getEntryAfterMiss():

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    while (e != null) {
        // 整个e是entry ,也就是一个弱引用
        ThreadLocal<?> k = e.get();
        //如果找到了,就返回
        if (k == key)
            return e;
        if (k == null)
            //如果key为null,说明弱引用已经被回收了
            //那么就要在这里回收里面的value了
            expungeStaleEntry(i);
        else
            //如果key不是要找的那个,那说明有hash冲突,这里是处理冲突,找下一个entry
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

Метод expangeStaleEntry() действительно используется для восстановления значения, а в методах remove() и set() этот метод прямо или косвенно вызывается для очистки значения:

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

Но из этого также видно, что ThreadLocal не гарантирует 100% отсутствия утечек памяти.

Например, к сожалению, ваш метод get() всегда обращается к фиксированному количеству ThreadLocals, которые всегда существовали, поэтому действие очистки не будет выполнено, и если у вас нет возможности вызвать set() и remove(), тогда утечка памяти все равно будет происходить.

Поэтому остается хорошая привычка:Когда вам не нужна эта переменная ThreadLocal, активно вызывайте remove(), что хорошо для всей системы..

Обработка конфликтов хэшей в ThreadLocalMap

Реализация ThreadLocalMap как HashMap и java.util.HashMap отличается. Для java.util.HashMap метод связанного списка используется для обработки конфликтов:

Однако для ThreadLocalMap используется простой линейный метод обнаружения: если возникает конфликт элементов, следующий слот используется для хранения:

В частности, весь процесс set() выглядит следующим образом:

ThreadLocal, который может быть унаследован — InheritableThreadLocal

В реальном процессе разработки мы можем столкнуться с таким сценарием. Основной поток открывает дочерний поток, но мы надеемся, что объект ThreadLocal в основном потоке может быть доступен в дочернем потоке, а это означает, что некоторые данные должны быть переданы между родительским и дочерним потоками. Например вот так:

public static void main(String[] args) {
    ThreadLocal threadLocal = new ThreadLocal();
    IntStream.range(0,10).forEach(i -> {
        //每个线程的序列号,希望在子线程中能够拿到
        threadLocal.set(i);
        //这里来了一个子线程,我们希望可以访问上面的threadLocal
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + ":" + threadLocal.get());
        }).start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
}

Выполните приведенный выше код, и вы увидите:

Thread-0:null
Thread-1:null
Thread-2:null
Thread-3:null

Потому что в дочернем потоке нет threadLocal. Если мы хотим, чтобы дочерний поток видел ThreadLocal родительского потока, можно использовать InheritableThreadLocal. Как следует из названия, это ThreadLocal, который поддерживает наследование родитель-потомок между потоками.Используйте InheritableThreadLocal для threadLocal в приведенном выше коде:

InheritableThreadLocal threadLocal = new InheritableThreadLocal();

Выполните снова, вы можете увидеть:

Thread-0:0
Thread-1:1
Thread-2:2
Thread-3:3
Thread-4:4

Как видите, каждый поток может получить доступ к данным, переданным от родительского процесса. Хотя InheritableThreadLocal кажется удобным, следует помнить о нескольких вещах:

  1. Передача переменных происходит при создании потока, если это не новый поток, а используется поток в пуле потоков, то он работать не будет.
  2. Назначение переменных состоит в том, чтобы скопировать карту из основного потока в дочерний поток, а их значением является тот же объект.Если сам объект не является потокобезопасным, возникнут проблемы с потокобезопасностью.

последние слова

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

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

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