Введение в ThreadLocal

Java

Обзор

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

Хотя использование переменных в разных методах этого потока может быть решено путем передачи параметров в метод, когда задействовано несколько методов или даже несколько классов, добавление одних и тех же параметров в каждый метод будет кошмаром.ThreadLocalможет хорошо решить эту проблему. Ему можно присвоить значение в любом месте потока, получить значение в любом месте, и он не передается в качестве параметра функции. Это похоже на статическую переменную-член, ноThreadLocalОдним из преимуществ переменных по сравнению со статическими переменными-членами является то, чтоThreadLocalЭто изолированный поток, на его значение не будет влиять другой поток, и нет необходимости рассматривать проблему блокировки или изменения значения другими потоками, что не может быть выполнено статическими переменными-членами. Поэтому, когда дело доходит до объекта, который необходимо передать между множеством различных методов, вам следует рассмотреть возможность использованияThreadLocalобъект для упрощения кода.

использовать

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

Пример кода выглядит следующим образом

public class TestThreadLocal
{
    private static ThreadLocal<StringBuilder> builder = ThreadLocal.withInitial(StringBuilder::new);

    public static void main(String[] args)
    {
        for (int i = 0; i < 5; i++)
        {
            new Thread(() -> {
                String threadName = Thread.currentThread().getName();
                for (int j = 0; j < 3; j++)
                {
                    append(j);
                    System.out.printf("%s append %d, now builder value is %s, ThreadLocal instance hashcode is %d, ThreadLocal instance mapping value hashcode is %d\n", threadName, j, builder.get().toString(), builder.hashCode(), builder.get().hashCode());
                }

                change();
                System.out.printf("%s set new stringbuilder, now builder value is %s, ThreadLocal instance hashcode is %d, ThreadLocal instance mapping value hashcode is %d\n", threadName, builder.get().toString(), builder.hashCode(), builder.get().hashCode());
            }, "thread-" + i).start();
        }
    }

    private static void append(int num) {
        builder.get().append(num);
    }

    private static void change() {
        StringBuilder newStringBuilder = new StringBuilder("HelloWorld");
        builder.set(newStringBuilder);
    }
}

В примере определитеbuilderизThreadLocalобъект, а затем запустить 5 потоков соответственноbuilderОперации доступа и модификации объекта, эти две операции размещены в двух разных функциях.append,changeв , доступ к двум функциямbuilderОбъект также получается напрямую, а не передается во входные параметры функции. Вывод кода выглядит следующим образом

thread-0 append 0, now builder value is 0, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 566157654
thread-0 append 1, now builder value is 01, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 566157654
thread-4 append 0, now builder value is 0, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 654647086
thread-3 append 0, now builder value is 0, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1803363945
thread-2 append 0, now builder value is 0, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1535812498
thread-1 append 0, now builder value is 0, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 2075237830
thread-2 append 1, now builder value is 01, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1535812498
thread-3 append 1, now builder value is 01, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1803363945
thread-4 append 1, now builder value is 01, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 654647086
thread-0 append 2, now builder value is 012, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 566157654
thread-0 set new stringbuilder, now builder value is HelloWorld, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1773033190
thread-4 append 2, now builder value is 012, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 654647086
thread-4 set new stringbuilder, now builder value is HelloWorld, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 700642750
thread-3 append 2, now builder value is 012, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1803363945
thread-3 set new stringbuilder, now builder value is HelloWorld, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1706743158
thread-2 append 2, now builder value is 012, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1535812498
thread-2 set new stringbuilder, now builder value is HelloWorld, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1431127699
thread-1 append 1, now builder value is 01, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 2075237830
thread-1 append 2, now builder value is 012, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 2075237830
thread-1 set new stringbuilder, now builder value is HelloWorld, ThreadLocal instance hashcode is 796465865, ThreadLocal instance mapping value hashcode is 1970695360

с выхода1~6Из строки видно, что разные потоки обращаются к одному и тому жеbuilderОбъект (вывод разными потокамиThreadLocal instance hashcodeзначение), но каждый поток получаетbuilderэкземпляр объектного хранилищаStringBuilderразные (вывод разными потокамиThreadLocal instance mapping value hashcodeзначения не совпадают).

с выхода1~2,9~10По строке видно, что этот же поток модифицировалbuilderКогда значение экземпляра, хранимого объектом, не влияет на значение других потоковbuilderЭкземпляры объектного хранилища (thread-4изменения потока сохраненыStringBuilderзначение не вызываетthread-0резьбовойThreadLocal instance mapping value hashcodeзначение изменилось)

с выхода9~13Из линии видно, что пара нитейThreadLocalКогда значение объектного хранилища изменится, это не повлияет на другие потоки (thread-0вызов потокаsetспособ изменить эту темуThreadLocalСохраненное значение объекта, этот потокThreadLocal instance mapping value hashcodeменяется, ноthread-4изThreadLocal instance mapping value hashcodeне изменился).

принцип

ThreadLocalМожет быть изолирован между каждым потоком, главным образом, полагаясь на каждыйThreadподдерживать объектThreadLocalMapбыть реализованным. Поскольку это объект в потоке, он невидим для других потоков, что позволяет достичь цели изоляции. Тогда почемуMapЧто насчет структуры. В основном потому, что в потоке может быть более одногоThreadLocalобъект, который требует коллекции для хранения различий и используетMapСвязанные объекты можно найти быстрее.

ThreadLocalMapдаThreadLocalСтатический внутренний класс объекта, который внутренне поддерживаетEntryмассив, который реализует что-то вродеMapизgetа такжеputд., для простоты его можно рассматривать какMapkeyдаThreadLocalпример,valueдаThreadLocalЗначение, сохраненное объектом экземпляра.

set

при звонкеThreadLocalизsetКогда метод устанавливает значение переменной,ThreadLocalОбъект сначала получит потокThreadLocalMapобъект, то текущийThreadLocalОбъект и устанавливаемое значение помещаются в виде пары ключ-значение.Mapсередина.

public void set(T value) {
    Thread t = Thread.currentThread();
    // 获取当前线程的 ThreadLocalMap 对象
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // this 指当前的 ThreadLocal 对象
        map.set(this, value);
    else
        // key 不存在,则创建 map 并设置值
        createMap(t, value);
}

ThreadLocalMap getMap(Thread t) {
    // threadLocals 是 Thread 中的一个变量,因此是线程隔离的,不会受其他线程影响
    // 其在 Thread 类中的定义如下:ThreadLocal.ThreadLocalMap threadLocals = null;
    return t.threadLocals;
}

get

ПолучатьThreadLocalПри сохранении значения объекта вам нужно вызватьgetметод. Этот метод также сначала получает этот потокThreadLocalMapобъект, то текущийThreadLocalобъект какkeyотMapПолучить соответствующее значение в , если нет, вернуть начальныйnull.

public T get() {
    Thread t = Thread.currentThread();
    // 获取当前线程的 ThreadLocalMap 对象
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // this 指当前的 ThreadLocal 对象
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

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

ThreadLocalMapсерединаkeyЯвляетсяThreadLocalобъект, и является слабой ссылкой, иvalueНо это сильная отсылка.

static class ThreadLocalMap {
    /**
      * The entries in this hash map extend WeakReference, using
      * its main ref field as the key (which is always a
      * ThreadLocal object).  Note that null keys (i.e. entry.get()
      * == null) mean that the key is no longer referenced, so the
      * entry can be expunged from table.  Such entries are referred to
      * as "stale entries" in the code that follows.
      */
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    // 其他代码
}

Нет сомнения, что если поток будет закрыт после выполнения, все объекты потока будут уничтожены, и в это время не будет проблемы с утечкой памяти. Кроме того, при выполненииget,setПри работе вызов поступаетThreadLocalMapвнутренняя функция, будетEntryпроверить, еслиkeyпусто, оно также будетvalueУстановите пустое значение, чтобы разрешить сборку мусора. Таким образом, при нормальных обстоятельствах это не приведет к утечке памяти.

// get 或 set 方法,满足一定条件时会进入 expungeStaleEntry 方法
// 此方法内部会将 key 为 null 的 Entry 的 value 设置为 null,从而使得其可以被垃圾回收
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // 设置 value 值为 null,清空引用,让其可以被 GC 回收
    // expunge entry at staleSlot
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // Rehash until we encounter null
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == null) {
            // 设置 value 值为 null,清空引用,让其可以被 GC 回收
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                tab[i] = null;

                // Unlike Knuth 6.4 Algorithm R, we must scan until
                // null because multiple entries could have been stale.
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}

Однако существует ситуация, которая может привести к утечке памяти. Если в какой-то моментThreadLocalэкземпляр установлен наnull, то есть безThreadLocalБольше нет сильных ссылок, если происходит GC из-заThreadLocalЭкземпляр имеет только слабые ссылки, поэтому он перерабатывается, ноvalueПо-прежнему существует сильная ссылка, связанная с текущим потоком, которая не будет переработана, только до тех пор, пока поток не завершится и не умрет или не будет очищен вручную.valueили дождаться другогоThreadLocalвозражать против выполненияgetилиsetСрабатывает только при выполнении операцииexpungeStaleEntryфункция и просто в состоянии проверить этоThreadLocalобъектkeyПустой (вероятно, слишком маленький), чтобы не было утечек памяти. в противном случае,valueНа него всегда есть ссылка, и он не будет восстановлен сборщиком мусора, поэтому это вызовет утечку памяти. Хотя вероятность утечек памяти относительно мала, для подстраховки также рекомендуется использоватьThreadLocalвызов после объектаremoveметод очистки значения.

内存泄漏

Использование с пулами потоков

Поскольку пул потоков будет повторно использовать потоки, если задача потокаThreadLocalЕсли значение считывается напрямую без сброса значения, может быть прочитан результат назначения предыдущей задачи потока, а не начальное значение этой задачи, что приводит к некоторым непредвиденным ошибкам. Как показано ниже, создайте пул потоков с фиксированным размером 3, но поместите 5 задач в пул потоков, а последние две задачи будут повторно использовать ранее созданные потоки.ThreadLocalизgetМетод получает результат назначения предыдущей задачи, а не начальное значение потока (первое значение вывода программы).4~5Линия для повторного использования потока11а также13, в первый раз это значение, присвоенное предыдущей задачей2, вместо начального значения потока1).

public class TestThreadLocalExecutor
{
    private static ThreadLocal<Integer> id = ThreadLocal.withInitial(() -> 1);

    public static void main(String[] args)
    {
        ExecutorService executor = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 5; i++)
        {
            executor.execute(() -> {
                long threadId = Thread.currentThread().getId();
                // 任务开始时重新赋值,否则可能读取到的是上一个任务的值
                // id.set(1);
                int before = id.get();
                increment();
                int after = id.get();

                System.out.printf("Thread id: %d, before increment: %d, after increment: %d\n", threadId, before, after);
            });
        }

        executor.shutdown();
    }

    private static void increment()
    {
        int result = id.get() + 1;
        id.set(result);
    }
}

Вывод программы следующий

Thread id: 11, before increment: 1, after increment: 2
Thread id: 13, before increment: 1, after increment: 2
Thread id: 12, before increment: 1, after increment: 2
Thread id: 13, before increment: 2, after increment: 3
Thread id: 11, before increment: 2, after increment: 3

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