5 решений для небезопасного потока SimpleDateFormat! | Заметки об отладке Java

Java задняя часть
5 решений для небезопасного потока SimpleDateFormat! | Заметки об отладке Java

Эта статья участвует в "Месяце тем 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 отпечатков с разными значениями):image.pngОднако результат выполнения вышеуказанной программы таков:image.pngИз приведенных выше результатов видно, что при использованииSimpleDateFormatВыполнение форматирования времени небезопасно для потоков. ​

2. Решения

SimpleDateFormatВсего существует 5 небезопасных для потоков решений:

  1. будетSimpleDateFormatОпределяется как локальная переменная;
  2. использоватьsynchronizedисполнение блокировки;
  3. использоватьLockвыполнение блокировки (аналогично решению 2);
  4. использоватьThreadLocal;
  5. использовать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();
    }
}

Результат выполнения вышеуказанной программы:image.pngКогда выведенные результаты отличаются, это означает, что выполнение программы выполнено правильно.Из приведенных выше результатов видно, что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();
    }
}

Результат выполнения вышеуказанной программы:image.png

③ Используйте 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();
    }
}

Результат выполнения вышеуказанной программы:image.pngКак видно из приведенного выше кода, метод записи ручной блокировки сравнивается сsynchronizedБудьте сложнее.

④ Используйте ThreadLocal

Хотя схема блокировки может правильно решить проблему ненадежности потока, она также создает новые проблемы.Блокировка заставит программу войти в процесс выполнения в очереди, что в определенной степени снизит эффективность выполнения программы, как показано на следующем рисунке. :image.pngЕсть ли решение, которое может не только решить проблему небезопасности потоков, но и избежать очереди выполнения? ​

Ответ - да, вы можете рассмотреть возможность использованияThreadLocal.ThreadLocalВ переводе на китайский язык это значение локальных переменных потока, слова похожи на людей.ThreadLocalОн используется для создания приватных (локальных) переменных потоков.Каждый поток имеет свой собственный приватный объект, так что можно избежать проблемы незащищенности потока.Реализация выглядит следующим образом:image.pngЗная план реализации, давайте воспользуемся конкретным кодом, чтобы продемонстрировать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();
    }
}

Результат выполнения вышеуказанной программы:image.png

Разница между ThreadLocal и локальными переменными

Прежде всегоThreadLocalНе равно локальным переменным, где «локальные переменные» относятся к локальным переменным, как в примере кода 2.1,ThreadLocalСамое большое отличие от локальных переменных заключается в том, что:ThreadLocalЧастная переменная, принадлежащая потоку, если используется пул потоков, тоThreadLocalПеременные в можно использовать повторно, а локальные переменные на уровне кода будут создавать новые локальные переменные при каждом выполнении.Разница между ними показана на следующем рисунке:image.pngбольше о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();
    }
}

Результат выполнения вышеуказанной программы:image.png

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. Поток 1 выполняетсяcalendar.setTime(date)метод преобразования времени, введенного пользователем, во время, необходимое для последующего форматирования;
  2. Поток 1 приостанавливает выполнение, поток 2 получаетCPUВременной интервал начинает выполняться;
  3. Поток 2 выполняетсяcalendar.setTime(date)метод, время было изменено;
  4. Поток 2 приостанавливает выполнение, поток 1 получаетCPUКвант времени продолжает выполняться, поскольку поток 1 и поток 2 используют один и тот же объект, а время было изменено потоком 2, поэтому при продолжении выполнения потока 1 возникнут проблемы с безопасностью потока.

В обычных условиях выполнение программы выглядит следующим образом:image.png

Поток выполнения, не являющийся потокобезопасным, выглядит следующим образом:image.pngВ случае многопоточного выполнения поток 1date1и нить 2 изdate2, из-за порядка выполнения они в итоге форматируются какdate2 formatted, а не поток 1date1 formattedи нить 2date2 formatted, что приведет к проблемам с безопасностью потоков.

4. Резюме преимуществ и недостатков каждой схемы

При использованииJDK 8+версия, вы можете напрямую использовать потокобезопасныйDateTimeFormatterдля форматирования времени, если используетсяJDK 8Следующая версия или изменить старуюSimpleDateFormatкод, рассмотрите возможность использованияsynchronizedилиThreadLocalдля решения проблемы небезопасности потоков. Поскольку решение реализации локальных переменных сценария 1 будет создавать новый объект при каждом выполнении, поэтому это не рекомендуется.synchronizedРеализация относительно проста, при использованииThreadLocalЭто может избежать проблемы блокировки и постановки в очередь выполнения.

Подпишитесь на официальный аккаунт «Сообщество китайского языка Java», чтобы увидеть больше интересных и полезных статей о параллельном программировании.