rtys18以下禁止:Java中为什么禁止把SimpleDateFormat定位为static变量以及如果非要使用static定位SimpleDateFormat时在多线程环境下的几种使用方式 2024-04-27 17:32:11 0 0 场景 Java中ExecutorService线程池的使用(Runnable和Callable多线程实现): Java中ExecutorService线程池的使用(Runnable和Callable多线程实现)_霸道流氓气质的博客-CSDN博客 Java中创建线程的方式以及线程池创建的方式、推荐使用ThreadPoolExecutor以及示例: Java中创建线程的方式以及线程池创建的方式、推荐使用ThreadPoolExecutor以及示例_霸道流氓气质的博客-CSDN博客 Java中使用CountDownLatch实现并发流程控制: Java中使用CountDownLatch实现并发流程控制_countdownlatch 并发_霸道流氓气质的博客-CSDN博客 以下会用到如上概念。 Java开发手册中对于SimpleDateFormat的使用的要求是: 【强制】SimpleDateFormat 是线程不安全的类,一般不要定义为 static 变量,如果定义为 static, 必须加锁,或者使用 DateUtils 工具类。 正例: 注意线程安全,使用 DateUtils。亦推荐如下处理: private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() { @Override protected DateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd"); } }; 说明:如果是 JDK8 的应用,可以使用 Instant 代替 Date,LocalDateTime 代替 Calendar, DateTimeFormatter 代替 SimpleDateFormat,官方给出的解释: simple beautiful strong immutable thread-safe。 注: 博客: 霸道流氓气质的博客_CSDN博客-C#,架构之路,SpringBoot领域博主 为了验证以上结论,首先需要了解下时区的概念。 时区是地球上的区域使用同一个时间定义。 以前,人们通过观察太阳的位置(时角)决定时间,这就使得不同经度的地方的时间有所不同(地方时)。 1863 年,首次使用时区的概念。时区通过设立一个区域的标准时间部分地解决了这个问题。 世界各个国家位于地球不同位置上,因此不同国家,特别是东西跨度大的国家日出、日落时间必定有所偏差。 这些偏差就是所谓的时差。现今全球共分为 24 个时区。 由于实用上常常 1 个国家,或 1 个省份同时跨着 2个或更多时区,为了照顾到行政上的方便,常将 1 个国家或 1 个省份划在一起。 所以时区并不严格按南北直线来划分,而是按自然条件来划分。 例如,中国幅员宽广,差不多跨 5 个时区,但为了使用方便简单,实际上在只用东八时区的标准时即北京时间为准。 由于不同的时区的时间是不一样的,甚至同一个国家的不同城市时间都可能不一样,所以, 在 Java 中想要获取时间的时候,要重点关注一下时区问题。 默认情况下,如果不指明,在创建日期的时候,会使用当前计算机所在的时区作为默认时区, 这也是为什么我们通过只要使用new Date()就可以获取中国的当前时间的原因。 Java中输出不同时区的时间 Java中可以通过SimpleDateFormat实现获取不同时区的时间SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");sdf.setTimeZone(TimeZone.getTimeZone("America/New_York"));System.out.println(sdf.format(Calendar.getInstance().getTime())); 以上输出纽约的时间比中国北京时间早了12个小时 验证如果将SimpleDateFormat声明为static会如何 新建测试类TestStaticSDFimport com.google.common.util.concurrent.ThreadFactoryBuilder;import java.text.SimpleDateFormat;import java.util.Calendar;import java.util.Collections;import java.util.HashSet;import java.util.Set;import java.util.concurrent.*;public class TestStaticSDF { //定义全局SimpleDateFormat private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); //使用guava的ThreadFactoryBuilder定义一个线程池 private static ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("demo-pool-%d").build(); private static ExecutorService pool = new ThreadPoolExecutor(5, 200, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(1024), threadFactory, new ThreadPoolExecutor.AbortPolicy()); //定义一个CountDownLatch,保证所有子线程执行完之后主线程再执行 private static CountDownLatch countDownLatch = new CountDownLatch(100); public static void main(String[] args) throws InterruptedException { //定义一个线程安全的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); String dateString = simpleDateFormat.format(calendar.getTime()); dates.add(dateString); countDownLatch.countDown(); }); } //堵塞,直到countDown数量为0 countDownLatch.await(); System.out.println(dates.size());//60 }} 就是循环100次,每次循环的时候都在当前时间基础上增加一个天数,然后把所有日期放在一个线程安全的、带有去重功能的Set中, 然后输出set的元素个数。 这里要注意: 在Java中,Collections类提供了许多线程安全的方法来处理集合类,其中一个重要的方法就是synchronizedSet()。 这是一个可以将任何Set集合转换为线程安全的Set集合的方法。 实际结果却是小于100的值,原因就是因为 SimpleDateFormat 作为一个非线程安全的类, 被当做了共享变量在多个线程中进行使用,这就出现了线程安全问题。 查看format的源码,方法在执行过程中,会使用一个成员变量calendar 来保存时间。 由于我们在声明 SimpleDateFormat 的时候,使用的是 static 定义的。 那么这 个 SimpleDateFormat 就 是 一 个 共 享 变 量, 随 之,SimpleDateFormat 中 的calendar 也就可以被多个线程访问到。 假设线程 1 刚刚执行完calendar.setTime把时间设置成 2018-11-11,还没等执行完,线程 2 又执行了calendar.setTime把时间改成了 2018-12-12。 这时候线程 1 继续往下执行,拿到的calendar.getTime得到的时间就是线程 2 改过之后的。 除了 format 方法以外,SimpleDateFormat 的 parse 方法也有同样的问题。所以,不要把 SimpleDateFormat 作为一个共享变量使用。 Java中多线程环境下使用static的SimpleDateFormat的解决方式 第一种方式,将SimpleDateFormat声明为局部变量,就不会被多个线程同时访问到了,避开线程安全问题public class TestStaticSDFSolve { //使用guava的ThreadFactoryBuilder定义一个线程池 private static ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("demo-pool-%d").build(); private static ExecutorService pool = new ThreadPoolExecutor(5, 200, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(1024), threadFactory, new ThreadPoolExecutor.AbortPolicy()); //定义一个CountDownLatch,保证所有子线程执行完之后主线程再执行 private static CountDownLatch countDownLatch = new CountDownLatch(100); public static void main(String[] args) throws InterruptedException { //定义一个线程安全的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(()->{ //SimpleDateFormat 声明成局部变量 SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); //时间增加 calendar.add(Calendar.DATE,finalI); String dateString = simpleDateFormat.format(calendar.getTime()); dates.add(dateString); countDownLatch.countDown(); }); } //堵塞,直到countDown数量为0 countDownLatch.await(); System.out.println(dates.size());//100 }} 第二种方式,对于共享变量加锁,通过加锁,使多个线程排队顺序执行,避免了并发导致的线程安全问题public class TestStaticSDFSolve2 { private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); //使用guava的ThreadFactoryBuilder定义一个线程池 private static ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("demo-pool-%d").build(); private static ExecutorService pool = new ThreadPoolExecutor(5, 200, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(1024), threadFactory, new ThreadPoolExecutor.AbortPolicy()); //定义一个CountDownLatch,保证所有子线程执行完之后主线程再执行 private static CountDownLatch countDownLatch = new CountDownLatch(100); public static void main(String[] args) throws InterruptedException { //定义一个线程安全的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(()->{ synchronized (simpleDateFormat){ //时间增加 calendar.add(Calendar.DATE,finalI); String dateString = simpleDateFormat.format(calendar.getTime()); dates.add(dateString); countDownLatch.countDown(); } }); } //堵塞,直到countDown数量为0 countDownLatch.await(); System.out.println(dates.size());//100 }} 第三种方式,就是使用ThreadLocal。可以确保每个线程都可以得到单独的一个SimpleDateFormat的对象public class TestStaticSDFSolve3 { private static ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = ThreadLocal .withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); //使用guava的ThreadFactoryBuilder定义一个线程池 private static ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("demo-pool-%d").build(); private static ExecutorService pool = new ThreadPoolExecutor(5, 200, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(1024), threadFactory, new ThreadPoolExecutor.AbortPolicy()); //定义一个CountDownLatch,保证所有子线程执行完之后主线程再执行 private static CountDownLatch countDownLatch = new CountDownLatch(100); public static void main(String[] args) throws InterruptedException { //定义一个线程安全的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); String dateString = simpleDateFormatThreadLocal.get().format(calendar.getTime()); dates.add(dateString); countDownLatch.countDown(); }); } //堵塞,直到countDown数量为0 countDownLatch.await(); System.out.println(dates.size());//100 }} 第四种方式,如果是java8应用,可以使用DateTimeFormatter代替SimpleDateFormat,这是一个线程安全的格式化工具类public class TestStaticSDFSolve4 { private static DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); //使用guava的ThreadFactoryBuilder定义一个线程池 private static ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("demo-pool-%d").build(); private static ExecutorService pool = new ThreadPoolExecutor(5, 200, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(1024), threadFactory, new ThreadPoolExecutor.AbortPolicy()); //定义一个CountDownLatch,保证所有子线程执行完之后主线程再执行 private static CountDownLatch countDownLatch = new CountDownLatch(100); public static void main(String[] args) throws InterruptedException { //定义一个线程安全的HashSet Set<String> dates = Collections.synchronizedSet(new HashSet<String>()); for (int i = 0; i < 100; i++) { //获取当前时间 LocalDateTime now = LocalDateTime.now(); int finalI = i; pool.execute(()->{ //时间增加 LocalDateTime localDateTime = now.plusDays(finalI); String dateString = localDateTime.format(format); dates.add(dateString); countDownLatch.countDown(); }); } //堵塞,直到countDown数量为0 countDownLatch.await(); System.out.println(dates.size());//100 }} 收藏(0)