Java中为什么禁止把SimpleDateFormat定位为static变量以及如果非要使用static定位SimpleDateFormat时在多线程环境下的几种使用方式

场景

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会如何

新建测试类TestStaticSDF

import 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
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

霸道流氓气质

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值