Нажмите выше"Программист Сяоле«Подпишитесь, поставьте звездочку или закрепите, чтобы расти вместе
Каждый день в 00:00 утра я встречусь с вами как можно скорее
Ежедневный английский
Sometimes,you are not happy if you see through everything.It's better to be naive and inattentive.
Много раз не приятно видеть слишком ясно, лучше быть наивным и бессердечным.
ежедневноговорить
Жизнь, зачем дергаться. Жизнь, зачем бояться головы и хвоста. Когда вы устали, слушайте музыку, когда вам грустно, вы в хорошем настроении, если вы потерпели неудачу, начните все сначала, на всю жизнь вам нужно быть случайным.
От: Oo Lu Yi oO | Ответственный редактор: Леле
Ссылка: juejin.im/post/5e0d8765f265da5d332cde44
Программист Сяоле (ID: study_tech) 746-й твит Изображение через Pexels
Оглядываясь назад в прошлое:Ситуация в Интернете не оптимистична, Чжоу Хунъи, председатель 360, отправит «карту без сокращений» для стабилизации вооруженных сил на ежегодном собрании.
текст
1. Сценарии использования ThreadLocal
1.1 Сценарий 1
Каждому потоку нужен эксклюзивный объект (обычно класс инструмента, типичные классы, которые необходимо использовать, — это SimpleDateFormat и Random)
Каждый поток имеет свою собственную копию экземпляра, а не общий
Аналогия: существует только один учебник, а совместные записи имеют проблемы с безопасностью потоков. После копирования проблем не возникает, а использование ThradLocal эквивалентно копированию учебника.
История рассказывает о частной территории потоков: ThreadLocal
Глубокое понимание ThreadLocal (эти детали нельзя игнорировать)
ThreadLocal
1.2 Сценарий 2
Глобальные переменные необходимо сохранять в каждом потоке (например, получение информации о пользователе в перехватчике), которые можно использовать напрямую разными методами, избегая проблем с передачей параметров.
2. Потренируйтесь на приведенных выше сценариях.
2.1 Практический сценарий 1
/**
* 两个线程打印日期
*/
public class ThreadLocalNormalUsage00 {
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage00().date(10);
System.out.println(date);
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage00().date(104707);
System.out.println(date);
}
}).start();
}
public String date(int seconds) {
//参数的单位是毫秒,从1970.1.1 00:00:00 GMT 开始计时
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
return dateFormat.format(date);
}
}
результат операции
Поскольку Китай расположен в районе Дунба, время отсчитывается с 8:00 1 января 1970 года.
/**
* 三十个线程打印日期
*/
public class ThreadLocalNormalUsage01 {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 30; i++) {
int finalI = i;
new Thread(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage01().date(finalI);
System.out.println(date);
}
}).start();
//线程启动后,休眠100ms
Thread.sleep(100);
}
}
public String date(int seconds) {
//参数的单位是毫秒,从1970.1.1 00:00:00 GMT 开始计时
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
return dateFormat.format(date);
}
}
результат операции
Несколько потоков печатают свое собственное время (проблемы с производительностью, если потоков слишком много), поэтому используйте пул потоков.
/**
* 1000个线程打印日期,用线程池来执行
*/
public class ThreadLocalNormalUsage02 {
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int finalI = i;
//提交任务
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage02().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public String date(int seconds) {
//参数的单位是毫秒,从1970.1.1 00:00:00 GMT 开始计时
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
return dateFormat.format(date);
}
}
результат операции
Но при использовании пула потоков вы обнаружите, что каждый поток имеет свой собственный объект SimpleDateFormat, который не нужен, поэтому объявите SimpleDateFormat как статический, чтобы гарантировать, что существует только один объект.
/**
* 1000个线程打印日期,用线程池来执行,出现线程安全问题
*/
public class ThreadLocalNormalUsage03 {
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
//只创建一次 SimpleDateFormat 对象,避免不必要的资源消耗
static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int finalI = i;
//提交任务
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage03().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public String date(int seconds) {
//参数的单位是毫秒,从1970.1.1 00:00:00 GMT 开始计时
Date date = new Date(1000 * seconds);
return dateFormat.format(date);
}
}
результат операции
Появляется отпечаток с таким же количеством секунд, что явно неверно.
Причина проблемы
Задачи нескольких потоков указывают на один и тот же объект SimpleDateFormat, а SimpleDateFormat не является потокобезопасным.
Решение проблемы
Вариант 1: заблокировать
Код форматирования находится в последнем предложенииreturn dateFormat.format(date);
, так что вы можете добавить синхронизированный замок к последнему коду
public String date(int seconds) {
//参数的单位是毫秒,从1970.1.1 00:00:00 GMT 开始计时
Date date = new Date(1000 * seconds);
String s;
synchronized (ThreadLocalNormalUsage04.class) {
s = dateFormat.format(date);
}
return s;
}
результат операции
То же самое время не найдено в текущих результатах, что обеспечивает безопасность потоков.
Недостатки. Поскольку добавляется синхронизация, гарантируется, что одновременно может выполняться только один поток, что определенно не является хорошим выбором в сценариях с высоким параллелизмом, поэтому давайте рассмотрим другие решения.
Сценарий 2. Используйте ThreadLocal
/**
* 利用 ThreadLocal 给每个线程分配自己的 dateFormat 对象
* 不但保证了线程安全,还高效的利用了内存
*/
public class ThreadLocalNormalUsage05 {
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int finalI = i;
//提交任务
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage05().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public String date(int seconds) {
//参数的单位是毫秒,从1970.1.1 00:00:00 GMT 开始计时
Date date = new Date(1000 * seconds);
//获取 SimpleDateFormat 对象
SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
return dateFormat.format(date);
}
}
class ThreadSafeFormatter {
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new
ThreadLocal<SimpleDateFormat>(){
//创建一份 SimpleDateFormat 对象
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
}
};
}
результат операции
После использования ThreadLocal разные потоки не будут совместно использовать объекты SimpleDateFormat, поэтому проблем с безопасностью потоков не будет.
2.2 Практический сценарий 2
Информация о текущем пользователе должна использоваться всеми методами в потоке.
Сценарий 1: Передача параметров
Вы можете передать пользователя в качестве параметра в каждом методе,
Недостатки: но это создает проблемы с избыточностью кода и плохой ремонтопригодностью.
Вариант 2: использовать карту
Решение для улучшения этого заключается в использовании Map, сохранении информации в первом методе, а затем использовании прямого get() позже,
Недостатки: Если вы можете гарантировать безопасность в однопоточной среде, это невозможно в многопоточной среде. Если вы используете блокировку и ConcurrentHashMap будут проблемы с производительностью.
Вариант 3. Используйте ThreadLocal для обеспечения совместного использования ресурсов между различными методами.
Использование ThreadLocal позволяет избежать проблем с производительностью, вызванных блокировкой, а также избежать передачи параметров послойно для выполнения бизнес-требований, чтобы можно было выполнить требования по хранению различной информации в разных потоках.
/**
* 演示 ThreadLocal 的用法2:避免参数传递的麻烦
*/
public class ThreadLocalNormalUsage06 {
public static void main(String[] args) {
new Service1().process();
}
}
class Service1 {
public void process() {
User user = new User("鲁毅");
//将User对象存储到 holder 中
UserContextHolder.holder.set(user);
new Service2().process();
}
}
class Service2 {
public void process() {
User user = UserContextHolder.holder.get();
System.out.println("Service2拿到用户名: " + user.name);
new Service3().process();
}
}
class Service3 {
public void process() {
User user = UserContextHolder.holder.get();
System.out.println("Service3拿到用户名: " + user.name);
}
}
class UserContextHolder {
public static ThreadLocal<User> holder = new ThreadLocal<>();
}
class User {
String name;
public User(String name) {
this.name = name;
}
}
результат операции
3. Резюме ThreadLocal
Пусть объект, который нужно использовать, достигает изоляции между потоками (у каждого потока есть свой независимый объект)
Объект легко получить любым способом
Выберите метод initialValue или метод set в зависимости от времени создания общего объекта.
Метод initialValue используется, когда время инициализации объекта контролируется нами.
Если время генерации объекта не находится под нашим контролем, используйте метод set
4. Преимущества использования ThreadLocal
достичь безопасности потоков
Нет необходимости блокировки, высокая эффективность исполнения
Сэкономьте больше памяти и сократите накладные расходы
Избавьтесь от утомительной передачи параметров и уменьшите связывание кода
5. Принцип ThreadLocal
Thread
ThreadLocal
ThreadLocalMap
Внутри класса Thread есть ThreadLocal.ThreadLocalMap threadLocals = null; эта переменная используется для хранения ThreadLocal, поскольку в одном потоке может быть несколько ThreadLocal, а get() вызывается несколько раз, поэтому необходимо поддерживать ThreadLocalMap внутренне использовать Store несколько ThreadLocals
5.1 Методы, связанные с ThreadLocal
T initialValue()
Этот метод используется для установки начального значения и будет запускаться только при вызове метода get(), поэтому это отложенная загрузка.
Но если set() выполняется до get(), то initialValue() вызываться не будет.
Обычно каждый поток может вызвать этот метод только один раз, но его можно вызвать снова после вызова метода remove().
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
//获取到了值直接返回resule
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//没有获取到才会进行初始化
return setInitialValue();
}
private T setInitialValue() {
//获取initialValue生成的值,并在后续操作中进行set,最后将值返回
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
void set(T t)
установить новое значение для этого потока
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
T get()
Получить значение, соответствующее потоку
public T get() {
Thread t = Thread.currentThread();
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();
}
void remove()
удалить значение, соответствующее этому потоку
6. Точки внимания ThreadLocal
6.1 Утечки памяти
Утечка памяти; объект больше не будет использоваться, но память объекта не может быть восстановлена
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
//调用父类,父类是一个弱引用
super(k);
//强引用
value = v;
}
}
Сильная ссылка: запуск GC при нехватке памяти, скорее выбросит OOM, чем вернет память с сильными ссылками
Слабая ссылка: после запуска GC память слабой ссылки будет восстановлена.
нормальные обстоятельства
Когда поток завершит работу, значение в ThreadLocal будет переработано, поскольку нет сильных ссылок.
ненормальная ситуация
Когда Thread продолжает работать и никогда не заканчивается, сильная ссылка не будет переработана, и будет следующая цепочка вызововThread-->ThreadLocalMap-->Entry(key为null)-->value
Так как значение и поток в цепочке вызовов имеют строгие ссылки, это значение не может быть повторно использовано, и может возникнуть OOM.
В дизайне JDK эта проблема учтена, поэтому в методах set(), remove() и resize() запись с нулевым ключом будет сканироваться, и соответствующее значение будет установлено равным нулю, так что объект значения может быть переработан.
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
//当ThreadLocal为空时,将ThreadLocal对应的value也设置为null
if (k == null) {
e.value = null; // Help the GC
} else {
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
setThreshold(newLen);
size = count;
table = newTab;
}
Но эти операции выполняются только при вызове методов set(), remove() и resize().Если эти методы не вызываются и поток не останавливается, цепочка вызовов будет существовать всегда, поэтому могут возникнуть утечки памяти .
6.2 Как избежать утечек памяти (протокол Ali)
Вызов метода remove() удалит соответствующий объект Entry, что позволит избежать утечек памяти, поэтому после использования ThreadLocal вызовите метод remove().
class Service1 {
public void process() {
User user = new User("鲁毅");
//将User对象存储到 holder 中
UserContextHolder.holder.set(user);
new Service2().process();
}
}
class Service2 {
public void process() {
User user = UserContextHolder.holder.get();
System.out.println("Service2拿到用户名: " + user.name);
new Service3().process();
}
}
class Service3 {
public void process() {
User user = UserContextHolder.holder.get();
System.out.println("Service3拿到用户名: " + user.name);
//手动释放内存,从而避免内存泄漏
UserContextHolder.holder.remove();
}
}
6.3 Проблема исключения нулевого указателя ThreadLocal
/**
* ThreadLocal的空指针异常问题
*/
public class ThreadLocalNPE {
ThreadLocal<Long> longThreadLocal = new ThreadLocal<>();
public void set() {
longThreadLocal.set(Thread.currentThread().getId());
}
public Long get() {
return longThreadLocal.get();
}
public static void main(String[] args) {
ThreadLocalNPE threadLocalNPE = new ThreadLocalNPE();
//如果get方法返回值为基本类型,则会报空指针异常,如果是包装类型就不会出错
System.out.println(threadLocalNPE.get());
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
threadLocalNPE.set();
System.out.println(threadLocalNPE.get());
}
});
thread1.start();
}
}
6.4 Решение проблемы исключения нулевого указателя
Если возвращаемое значение метода get является базовым типом, будет сообщено об исключении нулевого указателя, а если это тип-оболочка, ошибки не будет. Это связано с тем, что существует связь упаковки и распаковки между примитивными типами и типами упаковки, а причина проблемы нулевого указателя заключается в пользователе.
6.5 Проблема с общим объектом
Если то, что ThreadLocal.set() вводит в каждом потоке, является одним и тем же объектом, совместно используемым несколькими потоками, например статическим объектом, то содержимое, полученное несколькими потоками, вызывающими ThreadLocal.get(), остается одним и тем же объектом, или потоки будут происходить безопасный вопрос.
6.6 Не форсируйте это без использования ThreadLocal
Если количество задач невелико, создание объекта в локальном методе может решить проблему, поэтому вам не нужно использовать ThreadLocal.
6.7 Используйте поддержку фреймворка вместо создания собственного
Например, в среде Spring, если вы можете использовать RequestContextHolder, вам не нужно самостоятельно поддерживать ThreadLocal, поскольку вы можете забыть вызвать метод remove() и т. д., что приведет к утечке памяти.
Добро пожаловать, чтобы оставить свое мнение в области сообщений, обсудить и улучшить вместе. Если сегодняшняя статья даст вам новое вдохновение и новое понимание улучшения способности к обучению, добро пожаловатьВпередПоделитесь с большим количеством людей.
Приглашаем читателей присоединиться к программисту XiaoleТехнологическая группа, ответьте на фоне официального аккаунта"Добавить группу"или"учиться" Сделаю.
Думаю, ты все еще хочешь увидеть
Сборник последних вопросов для интервью от Ali, Tencent, Baidu, Huawei и JD.com.
Сколько потоков разумно должен создать пул потоков? (Одна статья, чтобы помочь вам понять)
Анализ структуры памяти JVM, прочитайте все и скажите «хорошо»!
Уловки отладки IDEA так круты в использовании!
关注「程序员小乐」,收看更多精彩内容
эй ты смотришь?