Эта статья участвует в «Месяце тем Java — составление вопросов по Java», подробнее см.Ссылка на мероприятие
ThreadLocal в переводе на китайский язык означает локальную переменную потока, что означает, что это частная переменная в потоке.Каждый поток может работать только со своей собственной частной переменной, поэтому это не приведет к небезопасности потока.
Небезопасность потока означает, что, когда несколько потоков записывают в одну и ту же глобальную переменную одновременно (операции чтения не будут связаны с проблемами небезопасности потока), если результат выполнения не соответствует ожидаемому результату, это называется небезопасным потоком. называется потокобезопасным.
Обычно есть два способа решить проблему небезопасности потоков в языке Java.:
- использовать блокировки (используя synchronized или Lock);
- Используйте ThreadLocal.
Схема реализации блокировки состоит в том, чтобы записывать глобальные переменные одну за другой, ставя в очередь, когда несколько потоков записывают глобальные переменные, чтобы можно было избежать проблемы небезопасности потока. Например, когда мы используем SimpleDateFormat, небезопасный для потоков, для форматирования времени, если мы используем блокировки для решения проблемы, небезопасной для потоков, процесс реализации выглядит следующим образом:Как видно из приведенных выше рисунков, хотя проблема небезопасности потоков может быть решена блокировкой, она также приносит новые проблемы.При использовании блокировок потоки необходимо ставить в очередь на выполнение, поэтому это принесет определенные накладные расходы на производительность. Тем не мение,Если вы используете метод ThreadLocal, вы создадите объект SimpleDateFormat для каждого потока, чтобы избежать проблемы постановки в очередь выполнения., процесс его реализации показан на следующем рисунке:
PS: создание SimpleDateFormat также потребует определенного количества времени и места. Если частота повторного использования потока SimpleDateFormat относительно высока, преимущество использования ThreadLocal относительно велико. В противном случае вы можете рассмотреть возможность использования блокировок.
Однако в процессе использования ThreadLocal легко могут возникнуть проблемы с переполнением памяти, как в следующем примере.
Что такое переполнение памяти?
Нехватка памяти (сокращенно OOM) относится к бесполезным объектам (объектам, которые больше не используются), которые продолжают занимать память, или память бесполезных объектов не освобождается вовремя, что приводит к пустой трате пространства памяти, которое называется памятью. утечки. .
Демонстрация кода нехватки памяти
Прежде чем мы начнем демонстрировать проблему переполнения памяти ThreadLocal, давайте воспользуемся параметром «-Xmx50m» для установки идеи, что означает, что максимальный объем памяти для запуска программы установлен на 50 м. Если программа работает за пределами этого значения, будет проблема переполнения памяти. , метод настройки следующий:Окончательный эффект после настройки такой:
PS: Поскольку идея, которую я использую, является версией сообщества, она может отличаться от вашего интерфейса.Вам нужно только нажать «Редактировать конфигурации ...», чтобы найти параметр «Параметры виртуальной машины», и установить параметр «-Xmx50m».
После настройки Idea приступим к реализации бизнес-кода. В коде мы создадим большой объект, который будет иметь массив размером 10 м, а затем мы сохраним этот большой объект в ThreadLocal, а затем используем пул потоков для выполнения более 5 задач сложения, потому что максимальная рабочая память установлено значение 50 м, поэтому в идеальной ситуации после выполнения 5 операций сложения возникнет проблема переполнения памяти Код реализации выглядит следующим образом:
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadLocalOOMExample {
/**
* 定义一个 10m 大的类
*/
static class MyTask {
// 创建一个 10m 的数组(单位转换是 1M -> 1024KB -> 1024*1024B)
private byte[] bytes = new byte[10 * 1024 * 1024];
}
// 定义 ThreadLocal
private static ThreadLocal<MyTask> taskThreadLocal = new ThreadLocal<>();
// 主测试代码
public static void main(String[] args) throws InterruptedException {
// 创建线程池
ThreadPoolExecutor threadPoolExecutor =
new ThreadPoolExecutor(5, 5, 60,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(100));
// 执行 10 次调用
for (int i = 0; i < 10; i++) {
// 执行任务
executeTask(threadPoolExecutor);
Thread.sleep(1000);
}
}
/**
* 线程池执行任务
* @param threadPoolExecutor 线程池
*/
private static void executeTask(ThreadPoolExecutor threadPoolExecutor) {
// 执行任务
threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
System.out.println("创建对象");
// 创建对象(10M)
MyTask myTask = new MyTask();
// 存储 ThreadLocal
taskThreadLocal.set(myTask);
// 将对象设置为 null,表示此对象不在使用了
myTask = null;
}
});
}
}
Результат выполнения вышеуказанной программы выглядит следующим образом:Как видно из приведенного выше рисунка, когда программа выполняется для добавления объектов в пятый раз, возникает проблема переполнения памяти, потому что максимальная рабочая память установлена на 50 м, и каждый цикл будет занимать 10 м памяти, плюс программа запускается.Она будет занимать определенное количество памяти,поэтому при добавлении пятой задачи будет проблема переполнения памяти.
Анализ причин
Проблема и решение переполнения памяти относительно просты. Основное внимание уделяется "анализу причин". Нам нужно выяснить это через проблему переполнения памяти. Почему ThreadLocal делает это? Что вызвало переполнение памяти?
Чтобы разобраться в этой проблеме (проблеме переполнения памяти), нам нужно начать с исходного кода ThreadLocal, поэтому мы сначала открываем исходный код метода set (в примере используется метод set), как показано ниже:
public void set(T value) {
// 得到当前线程
Thread t = Thread.currentThread();
// 根据线程获取到 ThreadMap 变量
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value); // 将内容存储到 map 中
else
createMap(t, value); // 创建 map 并将值存储到 map 中
}
Из приведенного выше кода мы можем видеть связь между методами Thread, ThreadLocalMap и set**: каждый поток Thread имеет контейнер для хранения данных ThreadLocalMap.При выполнении метода ThreadLocal.set сохраняемое значение будет помещено в ThreadLocalMap. container.**, поэтому давайте взглянем на исходный код ThreadLocalMap:
static class ThreadLocalMap {
// 实际存储数据的数组
private Entry[] table;
// 存数据的方法
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)]) {
ThreadLocal<?> k = e.get();
// 如果有对应的 key 直接更新 value 值
if (k == key) {
e.value = value;
return;
}
// 发现空位插入 value
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 新建一个 Entry 插入数组中
tab[i] = new Entry(key, value);
int sz = ++size;
// 判断是否需要进行扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
// ... 忽略其他源码
}
Из приведенного выше исходного кода мы видим, что:В ThreadMap есть массив Entry[] для хранения всех данных, а Entry — это пара ключ-значение, содержащая ключ и значение, где ключ — это сам ThreadLocal, а значение — это значение, которое будет храниться в ThreadLocal..
В соответствии с приведенным выше содержанием мы можем нарисовать диаграмму отношений связанных объектов ThreadLocal следующим образом:то естьЭталонная связь между ними следующая: Thread -> ThreadLocalMap -> Entry -> Key, Value, поэтому, когда мы используем пул потоков для хранения объектов, поскольку пул потоков имеет длительный жизненный цикл, пул потоков всегда будет храниться. есть значение value, то сборщик мусора не сможет переработать это значение, поэтому память будет занята все время, что приведет к возникновению проблемы переполнения памяти..
решение
Решение проблемы переполнения памяти ThreadLocal очень простое.Нам нужно только выполнить метод удаления после использования ThreadLocal, чтобы избежать проблемы переполнения памяти, например следующий код:
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class App {
/**
* 定义一个 10m 大的类
*/
static class MyTask {
// 创建一个 10m 的数组(单位转换是 1M -> 1024KB -> 1024*1024B)
private byte[] bytes = new byte[10 * 1024 * 1024];
}
// 定义 ThreadLocal
private static ThreadLocal<MyTask> taskThreadLocal = new ThreadLocal<>();
// 测试代码
public static void main(String[] args) throws InterruptedException {
// 创建线程池
ThreadPoolExecutor threadPoolExecutor =
new ThreadPoolExecutor(5, 5, 60,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(100));
// 执行 n 次调用
for (int i = 0; i < 10; i++) {
// 执行任务
executeTask(threadPoolExecutor);
Thread.sleep(1000);
}
}
/**
* 线程池执行任务
* @param threadPoolExecutor 线程池
*/
private static void executeTask(ThreadPoolExecutor threadPoolExecutor) {
// 执行任务
threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
System.out.println("创建对象");
try {
// 创建对象(10M)
MyTask myTask = new MyTask();
// 存储 ThreadLocal
taskThreadLocal.set(myTask);
// 其他业务代码...
} finally {
// 释放内存
taskThreadLocal.remove();
}
}
});
}
}
Результат выполнения вышеуказанной программы выглядит следующим образом:Из приведенных выше результатов мы видим, что нам нужно только выполнить метод удаления ThreadLocal, наконец, и не будет проблемы с переполнением памяти.
удалить секрет
Так почему же в методе удаления столько волшебства? Давайте откроем исходный код удаления и посмотрим:
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
Из приведенного выше исходного кода мы видим, что при вызове метода удаления объект ThreadLocalMap в потоке будет удален напрямую, так что поток больше не содержит объект ThreadLocalMap, поэтому даже если поток живет все время, он не вызовет проблемы переполнения памяти (ThreadLocalMap), вызванной использованием памяти.
Суммировать
В этой статье мы используем код для демонстрации проблемы переполнения памяти ThreadLocal Строго говоря, переполнение памяти — это не проблема ThreadLocal, а проблема, вызванная неправильным использованием ThreadLocal. Чтобы избежать проблемы переполнения памяти ThreadLocal, просто вызовите метод удаления после использования ThreadLocal. Тем не менее, через проблему переполнения памяти ThreadLocal, давайте разберемся с конкретной реализацией ThreadLocal, чтобы мы могли лучше использовать ThreadLocal в будущем и лучше справляться с интервью.
Подпишитесь на официальный аккаунт «Сообщество китайского языка Java», чтобы увидеть больше интересных и полезных статей о параллельном программировании.