Эта статья участвует в "Месяце тем Java - Заметки по отладке Java", подробности см.Ссылка на мероприятие
1. Что такое небезопасность потоков?
Небезопасный поток также называется небезопасным потоком.При многопоточном выполнении ситуация, когда результат выполнения программы не соответствует ожидаемому результату, называется небезопасным потоком..
код, небезопасный для потоков
SimpleDateFormat
Это типичный случай небезопасности потока, теперь давайте его реализуем. Во-первых, мы создаем потоки 10 для форматирования времени.Время форматирования отличается каждый раз, когда передается формат времени, поэтому программа будет печатать 10 различных значений, если она выполняется правильно.Далее давайте посмотрим на конкретный код выполнить:
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SimpleDateFormatExample {
// 创建 SimpleDateFormat 对象
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
public static void main(String[] args) {
// 创建线程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);
// 执行 10 次时间格式化
for (int i = 0; i < 10; i++) {
int finalI = i;
// 线程池执行任务
threadPool.execute(new Runnable() {
@Override
public void run() {
// 创建时间对象
Date date = new Date(finalI * 1000);
// 执行时间格式化并打印结果
System.out.println(simpleDateFormat.format(date));
}
});
}
}
}
Правильный результат, который мы ожидаем, таков (10 отпечатков с разными значениями):Однако результат выполнения вышеуказанной программы таков:
Из приведенных выше результатов видно, что при использовании
SimpleDateFormat
Выполнение форматирования времени небезопасно для потоков.
2. Решения
SimpleDateFormat
Всего существует 5 небезопасных для потоков решений:
- будет
SimpleDateFormat
Определяется как локальная переменная; - использовать
synchronized
исполнение блокировки; - использовать
Lock
выполнение блокировки (аналогично решению 2); - использовать
ThreadLocal
; - использовать
JDK 8
предоставлено вDateTimeFormat
.
Далее мы рассмотрим конкретную реализацию каждого решения отдельно.
① Превратите SimpleDateFormat в локальную переменную
будетSimpleDateFormat
Когда определено как локальная переменная, потому что каждый поток является исключительнымSimpleDateFormat
Объект эквивалентен превращению многопоточной программы в "однопотоковую" программу, поэтому не будет проблем с небезопасностью потока. Конкретный код реализации выглядит следующим образом:
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SimpleDateFormatExample {
public static void main(String[] args) {
// 创建线程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);
// 执行 10 次时间格式化
for (int i = 0; i < 10; i++) {
int finalI = i;
// 线程池执行任务
threadPool.execute(new Runnable() {
@Override
public void run() {
// 创建 SimpleDateFormat 对象
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
// 创建时间对象
Date date = new Date(finalI * 1000);
// 执行时间格式化并打印结果
System.out.println(simpleDateFormat.format(date));
}
});
}
// 任务执行完之后关闭线程池
threadPool.shutdown();
}
}
Результат выполнения вышеуказанной программы:Когда выведенные результаты отличаются, это означает, что выполнение программы выполнено правильно.Из приведенных выше результатов видно, что
SimpleDateFormat
После определения его как локальной переменной проблема небезопасности потока может быть успешно решена.
② Используйте синхронизацию для блокировки
Блокировки — наиболее распространенный способ решения проблем с небезопасностью потока.synchronized
Для блокировки форматирования времени код реализации выглядит следующим образом:
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SimpleDateFormatExample2 {
// 创建 SimpleDateFormat 对象
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
public static void main(String[] args) {
// 创建线程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);
// 执行 10 次时间格式化
for (int i = 0; i < 10; i++) {
int finalI = i;
// 线程池执行任务
threadPool.execute(new Runnable() {
@Override
public void run() {
// 创建时间对象
Date date = new Date(finalI * 1000);
// 定义格式化的结果
String result = null;
synchronized (simpleDateFormat) {
// 时间格式化
result = simpleDateFormat.format(date);
}
// 打印结果
System.out.println(result);
}
});
}
// 任务执行完之后关闭线程池
threadPool.shutdown();
}
}
Результат выполнения вышеуказанной программы:
③ Используйте Lock, чтобы заблокировать
В языке Java есть две общие реализации блокировок, за исключениемsynchronized
Кроме того, можно использовать ручные замки.Lock
, то используемLock
Чтобы преобразовать небезопасный для потоков код, код реализации выглядит следующим образом:
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* Lock 解决线程不安全问题
*/
public class SimpleDateFormatExample3 {
// 创建 SimpleDateFormat 对象
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
public static void main(String[] args) {
// 创建线程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);
// 创建 Lock 锁
Lock lock = new ReentrantLock();
// 执行 10 次时间格式化
for (int i = 0; i < 10; i++) {
int finalI = i;
// 线程池执行任务
threadPool.execute(new Runnable() {
@Override
public void run() {
// 创建时间对象
Date date = new Date(finalI * 1000);
// 定义格式化的结果
String result = null;
// 加锁
lock.lock();
try {
// 时间格式化
result = simpleDateFormat.format(date);
} finally {
// 释放锁
lock.unlock();
}
// 打印结果
System.out.println(result);
}
});
}
// 任务执行完之后关闭线程池
threadPool.shutdown();
}
}
Результат выполнения вышеуказанной программы:Как видно из приведенного выше кода, метод записи ручной блокировки сравнивается с
synchronized
Будьте сложнее.
④ Используйте ThreadLocal
Хотя схема блокировки может правильно решить проблему ненадежности потока, она также создает новые проблемы.Блокировка заставит программу войти в процесс выполнения в очереди, что в определенной степени снизит эффективность выполнения программы, как показано на следующем рисунке. :Есть ли решение, которое может не только решить проблему небезопасности потоков, но и избежать очереди выполнения?
Ответ - да, вы можете рассмотреть возможность использованияThreadLocal
.ThreadLocal
В переводе на китайский язык это значение локальных переменных потока, слова похожи на людей.ThreadLocal
Он используется для создания приватных (локальных) переменных потоков.Каждый поток имеет свой собственный приватный объект, так что можно избежать проблемы незащищенности потока.Реализация выглядит следующим образом:Зная план реализации, давайте воспользуемся конкретным кодом, чтобы продемонстрировать
ThreadLocal
Код реализации выглядит следующим образом:
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* ThreadLocal 解决线程不安全问题
*/
public class SimpleDateFormatExample4 {
// 创建 ThreadLocal 对象,并设置默认值(new SimpleDateFormat)
private static ThreadLocal<SimpleDateFormat> threadLocal =
ThreadLocal.withInitial(() -> new SimpleDateFormat("mm:ss"));
public static void main(String[] args) {
// 创建线程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);
// 执行 10 次时间格式化
for (int i = 0; i < 10; i++) {
int finalI = i;
// 线程池执行任务
threadPool.execute(new Runnable() {
@Override
public void run() {
// 创建时间对象
Date date = new Date(finalI * 1000);
// 格式化时间
String result = threadLocal.get().format(date);
// 打印结果
System.out.println(result);
}
});
}
// 任务执行完之后关闭线程池
threadPool.shutdown();
}
}
Результат выполнения вышеуказанной программы:
Разница между ThreadLocal и локальными переменными
Прежде всегоThreadLocal
Не равно локальным переменным, где «локальные переменные» относятся к локальным переменным, как в примере кода 2.1,ThreadLocal
Самое большое отличие от локальных переменных заключается в том, что:ThreadLocal
Частная переменная, принадлежащая потоку, если используется пул потоков, тоThreadLocal
Переменные в можно использовать повторно, а локальные переменные на уровне кода будут создавать новые локальные переменные при каждом выполнении.Разница между ними показана на следующем рисунке:больше о
ThreadLocal
содержание, вы можете посетить предыдущую статью Лэй Гэ«ThreadLocal не прост в использовании? Ты бесполезен! 》.
⑤ Используйте DateTimeFormatter
Все вышеперечисленные 4 решения связаны с тем, чтоSimpleDateFormat
является потокобезопасным, поэтому нам нужно заблокировать или использоватьThreadLocal
иметь дело, однако,JDK 8
После этого у нас появляются новые опции, если использоватьJDK 8+
версия, вы можете использовать ее напрямуюJDK 8
Новый безопасный класс инструментов форматирования времени вDateTimeFormatter
Давайте отформатируем время, давайте подробно это реализуем.
использоватьDateTimeFormatter
должны сотрудничатьJDK 8
Новый объект времени вLocalDateTime
использовать, поэтому перед операцией мы можем сначалаDate
объект, преобразованный вLocalDateTime
, а затем пройтиDateTimeFormatter
Для форматирования времени конкретный код реализации выглядит следующим образом:
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* DateTimeFormatter 解决线程不安全问题
*/
public class SimpleDateFormatExample5 {
// 创建 DateTimeFormatter 对象
private static DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("mm:ss");
public static void main(String[] args) {
// 创建线程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);
// 执行 10 次时间格式化
for (int i = 0; i < 10; i++) {
int finalI = i;
// 线程池执行任务
threadPool.execute(new Runnable() {
@Override
public void run() {
// 创建时间对象
Date date = new Date(finalI * 1000);
// 将 Date 转换成 JDK 8 中的时间类型 LocalDateTime
LocalDateTime localDateTime =
LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
// 时间格式化
String result = dateTimeFormatter.format(localDateTime);
// 打印结果
System.out.println(result);
}
});
}
// 任务执行完之后关闭线程池
threadPool.shutdown();
}
}
Результат выполнения вышеуказанной программы:
3. Анализ причин небезопасных потоков
понятьSimpleDateFormat
Почему поток небезопасен? Нам нужно видеть и анализироватьSimpleDateFormat
Исходный код в порядке, тогда давайте начнем с используемого методаformat
Для начала исходный код выглядит следующим образом:
private StringBuffer format(Date date, StringBuffer toAppendTo,
FieldDelegate delegate) {
// 注意此行代码
calendar.setTime(date);
boolean useDateFormatSymbols = useDateFormatSymbols();
for (int i = 0; i < compiledPattern.length; ) {
int tag = compiledPattern[i] >>> 8;
int count = compiledPattern[i++] & 0xff;
if (count == 255) {
count = compiledPattern[i++] << 16;
count |= compiledPattern[i++];
}
switch (tag) {
case TAG_QUOTE_ASCII_CHAR:
toAppendTo.append((char)count);
break;
case TAG_QUOTE_CHARS:
toAppendTo.append(compiledPattern, i, count);
i += count;
break;
default:
subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
break;
}
}
return toAppendTo;
}
Может быть, это удача, я не ожидал найти проблему небезопасности потока, когда начал анализировать первый метод.
Как видно из приведенного выше исходного кода, при выполненииSimpleDateFormat.format
метод, будем использоватьcalendar.setTime
Метод преобразует время ввода, поэтому давайте представим такой сценарий:
- Поток 1 выполняется
calendar.setTime(date)
метод преобразования времени, введенного пользователем, во время, необходимое для последующего форматирования; - Поток 1 приостанавливает выполнение, поток 2 получает
CPU
Временной интервал начинает выполняться; - Поток 2 выполняется
calendar.setTime(date)
метод, время было изменено; - Поток 2 приостанавливает выполнение, поток 1 получает
CPU
Квант времени продолжает выполняться, поскольку поток 1 и поток 2 используют один и тот же объект, а время было изменено потоком 2, поэтому при продолжении выполнения потока 1 возникнут проблемы с безопасностью потока.
В обычных условиях выполнение программы выглядит следующим образом:
Поток выполнения, не являющийся потокобезопасным, выглядит следующим образом:В случае многопоточного выполнения поток 1
date1
и нить 2 изdate2
, из-за порядка выполнения они в итоге форматируются какdate2 formatted
, а не поток 1date1 formatted
и нить 2date2 formatted
, что приведет к проблемам с безопасностью потоков.
4. Резюме преимуществ и недостатков каждой схемы
При использованииJDK 8+
версия, вы можете напрямую использовать потокобезопасныйDateTimeFormatter
для форматирования времени, если используетсяJDK 8
Следующая версия или изменить старуюSimpleDateFormat
код, рассмотрите возможность использованияsynchronized
илиThreadLocal
для решения проблемы небезопасности потоков. Поскольку решение реализации локальных переменных сценария 1 будет создавать новый объект при каждом выполнении, поэтому это не рекомендуется.synchronized
Реализация относительно проста, при использованииThreadLocal
Это может избежать проблемы блокировки и постановки в очередь выполнения.
Подпишитесь на официальный аккаунт «Сообщество китайского языка Java», чтобы увидеть больше интересных и полезных статей о параллельном программировании.