В повседневной разработке мы часто используем время, и у нас есть много способов получить время в Java-коде. Однако форматы времени, полученные разными методами, неодинаковы, в настоящее время необходим инструмент форматирования для отображения времени в нужном нам формате.
Самый распространенный метод — использовать класс SimpleDateFormat. Это класс, который выглядит относительно простым, но при неправильном использовании может вызвать серьезные проблемы.
В Руководстве по разработке Java для Alibaba есть следующие четкие положения:
Затем в этой статье основное внимание будет уделено использованию и принципу SimpleDateFormat, чтобы глубоко проанализировать, как использовать его в правильной позе.
Использование SimpleDateFormat
SimpleDateFormat — это инструментальный класс, предоставляемый Java для форматирования и анализа дат. Он позволяет форматировать (дата -> текст), анализировать (текст -> дата) и нормализовать. SimpleDateFormat позволяет выбрать любой определяемый пользователем шаблон формата даты и времени.
В Java вы можете использовать метод форматирования SimpleDateFormat для преобразования типа Date в тип String и указать выходной формат.
// Date转String
Date data = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dataStr = sdf.format(data);
System.out.println(dataStr);
В приведенном выше коде результат преобразования: 2018-11-25 13:00:00, формат даты и времени задается строкой «шаблон даты и времени». Если вы хотите конвертировать в другие форматы, просто укажите другой режим времени.
В Java вы можете использовать метод разбора SimpleDateFormat для преобразования типа String в тип Date.
// String转Data
System.out.println(sdf.parse(dataStr));
Методы выражения шаблона даты и времени
При использовании SimpleDateFormat элементы времени должны быть описаны буквами и объединены в желаемый шаблон даты и времени. Таблица соответствия часто используемых элементов времени и букв выглядит следующим образом:
Буквы шаблона часто повторяются, количество которых определяет их точное представление. В следующей таблице представлены распространенные форматы вывода.
выходное время в разных часовых поясах
Часовой пояс — это область на Земле, в которой используется одно и то же определение времени. Раньше люди определяли время по положению солнца (часовой угол), что делало время на разных долготах разным (местное время). В 1863 году впервые было использовано понятие часовых поясов. Часовые пояса частично решают эту проблему, устанавливая поясное стандартное время.
Каждая страна в мире расположена в разных положениях на земле, поэтому в разных странах, особенно с большим расстоянием между востоком и западом, должны быть некоторые отклонения во времени восхода и захода солнца. Эти отклонения известны как джетлаг.
Сегодня в мире 24 часовых пояса. Поскольку практически одна страна или одна провинция охватывает два или более часовых пояса одновременно, для удобства администрирования одна страна или одна провинция часто группируются вместе. Поэтому часовые пояса делятся строго не по прямой линии север-юг, а по природным условиям. Например, Китай огромен и охватывает почти 5 часовых поясов, но ради удобства и простоты фактически используется только стандартное время Восточного 8-го часового пояса, то есть пекинское время.
Так как время в разных часовых поясах разное, и даже в разных городах одной страны время может быть разным, поэтому, когда вы хотите получить время на Java, вам следует сосредоточиться на вопросе часового пояса.
По умолчанию, если вы его не укажете, часовой пояс текущего компьютера будет использоваться как часовой пояс по умолчанию при создании даты, поэтому нам нужно использовать толькоnew Date()
Причина, по которой вы можете получить текущее время в Китае.
Итак, как получить время в разных часовых поясах в Java-коде? SimpleDateFormat может реализовать эту функцию.
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
sdf.setTimeZone(TimeZone.getTimeZone("America/Los_Angeles"));
System.out.println(sdf.format(Calendar.getInstance().getTime()));
В приведенном выше коде преобразованный результат: 2018-11-24 21:00:00 . Время в Китае 13:00 25 ноября, а время в Лос-Анджелесе на 16 часов медленнее, чем в Пекине, Китай (это также связано с зимним и летним временем, поэтому я не буду вдаваться в подробности).
Если вам интересно, вы также можете попробовать распечатать время в Нью-Йорке (America/New_York). Время в Нью-Йорке 2018-11-25 00:00:00. Нью-йоркское время на 13 часов опережает пекинское время в Китае.
Конечно, это не единственный способ отображения других часовых поясов, но эта статья в основном предназначена для ознакомления с SimpleDateFormat, а другие методы пока не будут представлены.
Потокобезопасность SimpleDateFormat
Так как SimpleDateFormat используется чаще, и в целом режим отображения времени в приложении одинаков, многие люди хотят определить SimpleDateFormat следующим образом:
public class Main {
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) {
simpleDateFormat.setTimeZone(TimeZone.getTimeZone("America/New_York"));
System.out.println(simpleDateFormat.format(Calendar.getInstance().getTime()));
}
}
Этот метод определения имеет большие риски для безопасности.
Проблема воспроизводится
Давайте посмотрим на фрагмент кода, следующий код использует пул потоков для выполнения вывода времени.
/** * @author Hollis */
public class Main {
/**
* 定义一个全局的SimpleDateFormat
*/
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
/**
* 使用ThreadFactoryBuilder定义一个线程池
*/
private static ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
.setNameFormat("demo-pool-%d").build();
private static ExecutorService pool = new ThreadPoolExecutor(5, 200,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());
/**
* 定义一个CountDownLatch,保证所有子线程执行完之后主线程再执行
*/
private static CountDownLatch countDownLatch = new CountDownLatch(100);
public static void main(String[] args) {
//定义一个线程安全的HashSet
Set<String> dates = Collections.synchronizedSet(new HashSet<String>());
for (int i = 0; i < 100; i++) {
//获取当前时间
Calendar calendar = Calendar.getInstance();
int finalI = i;
pool.execute(() -> {
//时间增加
calendar.add(Calendar.DATE, finalI);
//通过simpleDateFormat把时间转换成字符串
String dateString = simpleDateFormat.format(calendar.getTime());
//把字符串放入Set中
dates.add(dateString);
//countDown
countDownLatch.countDown();
});
}
//阻塞,直到countDown数量为0
countDownLatch.await();
//输出去重后的时间个数
System.out.println(dates.size());
}
}
Приведенный выше код на самом деле относительно прост и понятен. Он состоит в том, чтобы повторять сто раз, и каждый раз, когда он повторяется, добавлять количество дней к текущему времени (это количество дней зависит от количества циклов), а затем помещать все даты впотокобезопасный,С функцией дедупликациив наборе, а затем вывести количество элементов в наборе.
Пример выше намеренно немного усложнен, но я почти весь его прокомментировал. участие здесьСоздание пула потоков,CountDownLatch, лямбда-выражения, потокобезопасный HashSet и другие знания. Заинтересованные друзья могут узнать о них по одному.
При нормальных обстоятельствах вывод приведенного выше кода должен быть равен 100. Но фактический результат выполнения — число меньше 100.
Причина в том, что SimpleDateFormat как не потокобезопасный класс используется как общая переменная в нескольких потоках, что приводит к проблемам с потокобезопасностью.
Это также четко указано в главе 1, разделе 6 «Параллельная обработка» Руководства по разработке Java для Alibaba:
Итак, давайте разберемся, почему и как это исправить.
поток небезопасен
С помощью приведенного выше кода мы обнаружили, что использование SimpleDateFormat в параллельных сценариях будет иметь проблемы с безопасностью потоков. На самом деле в документации JDK четко указано, что SimpleDateFormat не следует использовать в многопоточных сценариях:
Date formats are not synchronized. It is recommended to create separate format instances for each thread. If multiple threads access a format concurrently, it must be synchronized externally.
Затем проанализируйте, почему возникает эта проблема и как реализован нижний слой SimpleDateFormat?
На самом деле мы можем найти подсказки, следуя реализации метода форматирования в классе SimpleDateFormat.
Метод форматирования в SimpleDateFormat использует календарь переменной-члена для экономии времени во время выполнения. Собственно в этом и суть проблемы.
Потому что, когда мы объявляем SimpleDateFormat, мы используем статическое определение. Затем этот SimpleDateFormat является общей переменной, а затем доступ к календарю в SimpleDateFormat может осуществляться несколькими потоками.
Предположим, что поток 1 только что завершил выполнениеcalendar.setTime
Установите время на 2018-11-11, прежде чем выполнение будет завершено, поток 2 будет выполнен снова.calendar.setTime
Измените время на 2018-12-12. В это время поток 1 продолжает выполняться вниз, и полученныйcalendar.getTime
Полученное время после того, как поток 2 изменил его.
В дополнение к методу форматирования та же проблема возникает с методом синтаксического анализа SimpleDateFormat.
Поэтому не используйте SimpleDateFormat в качестве общей переменной.
Как решить
Проблемы SimpleDateFormat и причины проблем были представлены ранее, так как же решить эту проблему?
Есть много решений, вот три наиболее часто используемых метода.
использовать локальные переменные
for (int i = 0; i < 100; i++) {
//获取当前时间
Calendar calendar = Calendar.getInstance();
int finalI = i;
pool.execute(() -> {
// SimpleDateFormat声明成局部变量
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//时间增加
calendar.add(Calendar.DATE, finalI);
//通过simpleDateFormat把时间转换成字符串
String dateString = simpleDateFormat.format(calendar.getTime());
//把字符串放入Set中
dates.add(dateString);
//countDown
countDownLatch.countDown();
});
}
SimpleDateFormat становится локальной переменной, поэтому к ней не будут обращаться несколько потоков одновременно, что позволяет избежать проблем с безопасностью потоков.
добавить блокировку синхронизации
В дополнение к переходу на локальные переменные существует еще один метод, с которым вы можете быть знакомы, а именно блокировка общих переменных.
for (int i = 0; i < 100; i++) {
//获取当前时间
Calendar calendar = Calendar.getInstance();
int finalI = i;
pool.execute(() -> {
//加锁
synchronized (simpleDateFormat) {
//时间增加
calendar.add(Calendar.DATE, finalI);
//通过simpleDateFormat把时间转换成字符串
String dateString = simpleDateFormat.format(calendar.getTime());
//把字符串放入Set中
dates.add(dateString);
//countDown
countDownLatch.countDown();
}
});
}
При блокировке несколько потоков ставятся в очередь для последовательного выполнения. Избегайте проблем безопасности потоков, вызванных параллелизмом.
На самом деле, в приведенном выше коде еще есть возможности для улучшения, то есть степень детализации блокировки можно установить на меньшее значение.simpleDateFormat.format
Эта линия заблокирована, что более эффективно.
Использовать ThreadLocal
Третий способ — использовать ThreadLocal. ThreadLocal может гарантировать, что каждый поток может получить отдельный объект SimpleDateFormat, поэтому, естественно, нет проблемы конкуренции.
/**
* 使用ThreadLocal定义一个全局的SimpleDateFormat
*/
private static ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
//用法
String dateString = simpleDateFormatThreadLocal.get().format(calendar.getTime());
Использование ThreadLocal для реализации на самом деле немного похоже на идею кэширования.Каждый поток имеет эксклюзивный объект, что позволяет избежать частого создания объекта и многопоточной конкуренции.
Конечно, приведенный выше код также имеет возможности для улучшения, то есть фактически процесс создания SimpleDateFormat можно изменить на ленивую загрузку. Подробно здесь описываться не будет.
Использование DateTimeFormatter
Если это приложение Java8, вы можете использовать DateTimeFormatter вместо SimpleDateFormat, который является потокобезопасным классом инструментов форматирования. Как говорится в официальной документации, этот класс является простым, красивым, сильным, неизменяемым, потокобезопасным.
//解析日期
String dateStr= "2016年10月25日";
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日");
LocalDate date= LocalDate.parse(dateStr, formatter);
//日期转换为字符串
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyy年MM月dd日 hh:mm a");
String nowStr = now .format(format);
System.out.println(nowStr);
Суммировать
В этой статье рассказывается об использовании SimpleDateFormat. SimpleDateFormat может в основном преобразовывать между строкой и датой, а также может преобразовывать время для вывода в разных часовых поясах. В то же время упоминается, что SimpleDateFormat не может гарантировать потокобезопасность в параллельных сценариях, и разработчикам необходимо обеспечить ее безопасность.
Основные методы — переход на локальные переменные, использование синхронизированной блокировки, использование Threadlocal для создания отдельного для каждого потока и т.д.
Я надеюсь, что с помощью этой статьи вы сможете более удобно использовать SimpleDateFormat.