Обзор
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
д., для простоты его можно рассматривать какMap
,вkey
да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
метод) или использовать собственный способ создания потоков (в обход пула потоков).