美文网首页多线程
SimpleDateFormat的进化之路

SimpleDateFormat的进化之路

作者: 刘一帆315 | 来源:发表于2020-08-10 11:35 被阅读0次

    在使用日期类的时候,我们常常需要将日期转化为我们方便查看的格式,这时我们就会使用到SimpleDateFormat类。由于SimpleDateFormat是线程不安全的,本文通过一个使用SimpleDateFormat的demo不断迭代,展示SimpleDateFormat在高并发情况下出现的问题,通过解决这些问题,学习使用线程池和ThreadLocal。

    一、两个线程打印SimpleDateFormat

    使用两个线程打印SimpleDateFormat
    /**
     * 两个线程打印日期并没有什么问题
     */
    public class ThreadLocalNormalUsage00 {
        public static void main(String[] args) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalNormalUsage00().date(10);
                    System.out.println(date);
                }
            }).start();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalNormalUsage00().date(104707);
                    System.out.println(date);
                }
            }).start();
        }
        public String date(int seconds){
            Date date = new Date(1000 * seconds);
            SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
            return dateFormat.format(date);
        }
    }
    

    开启两个线程,各自在自己的线程内创建自己的SimpleDateFormat对象,打印出的结果没有任何问题。

    1970-01-02 01:05:07
    1970-01-01 08:00:10
    
    Process finished with exit code 0
    

    二、十个以上线程打印SimpleDateFormat

    for循环开启线程

    当任务逐步增多以后,我们使用第一种创建线程的方式就变得十分麻烦了,所以我们使用for循环来创建线程。

    package threadlocal;
    
    import java.text.SimpleDateFormat;
    import java.util.Date;
    
    /**
     * 十个线程打印日期
     */
    public class ThreadLocalNormalUsage01 {
        public static void main(String[] args) throws InterruptedException {
            for (int i = 0; i < 30; i++) {
                int finalI = i;
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        String date = new ThreadLocalNormalUsage01().date(finalI);
                        System.out.println(date);
                    }
                }).start();
                Thread.sleep(100);
            }
        }
    
        public String date(int seconds) {
            Date date = new Date(1000 * seconds);
            SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
            return dateFormat.format(date);
        }
    }
    

    运行结果如下:

                  .
                  .
                  .
    1970-01-01 08:00:26
    1970-01-01 08:00:27
    1970-01-01 08:00:28
    1970-01-01 08:00:29
    
    Process finished with exit code 0
    

    三、1000个线程打印SimpleDateFormat

    使用线程池

    使用for循环的实现方法有点简陋,假如现在需求变成了1000个,需要如何实现呢?如果使用for循环,将会创建1000个线程,创建线程和销毁线程都是一笔不小的开销。实际上没有必要这样做,我们可以使用线程池将线程的生命周期和线程执行的内容解耦,所以我们继续升级代码。

    /**
     * 使用线程池打印日期
     */
    public class ThreadLocalNormalUsage02 {
        //使用固定数量的线程池
        public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
        public static void main(String[] args) throws InterruptedException {
    
            for (int i = 0; i < 1000; i++) {
    
                int finalI = i;
                threadPool.submit(new Runnable() {
                    @Override
                    public void run() {
                        String date = new ThreadLocalNormalUsage02().date(finalI);
                        System.out.println(date);
                    }
                });
            }
            threadPool.shutdown();
        }
    
        public String date(int seconds) {
            Date date = new Date(1000 * seconds);
            SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
            return dateFormat.format(date);
        }
    }
    

    运行结果如下:

                  .
                  .
                  .
    1970-01-01 08:00:26
    1970-01-01 08:00:27
    1970-01-01 08:00:28
    1970-01-01 08:00:29
    
    Process finished with exit code 0
    

    四、所有的线程都公用一个SimpleDateFormat对象

    上边的实现虽然可以使用线程池来复用线程减少开销,但是每创建一个线程就会new一个SimpleDateFormat对象,这样有几个线程就会创建几个对象,我们如果想让所有线程都使用同一个SimpleDateFormat对象,这时就出现了线程安全问题。

    /**
     * 使用线程池打印日期
     */
    public class ThreadLocalNormalUsage03 {
        public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
    
        static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
    
        public static void main(String[] args) throws InterruptedException {
    
            for (int i = 0; i < 1000; i++) {
    
                int finalI = i;
                threadPool.submit(new Runnable() {
                    @Override
                    public void run() {
                        String date = new ThreadLocalNormalUsage03().date(finalI);
                        System.out.println(date);
                    }
                });
            }
            threadPool.shutdown();
        }
    
        public String date(int seconds) {
            Date date = new Date(1000 * seconds);
            return dateFormat.format(date);
        }
    }
    

    由于SimpleDateFormat类本身是线程不安全的,当所有线程共用一个SimpleDateFormat对象时,就会出现问题。

                  .
                  .
                  .
    1970-01-01 08:00:26
    1970-01-01 08:00:30
    1970-01-01 08:00:30
    1970-01-01 08:00:29
    
    Process finished with exit code 0
    

    五、使用synchronized解决线程同步问题

    /**
     * 使用synchronized对format加锁,来保证线程安全
     */
    public class ThreadLocalNormalUsage04 {
        public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
    
        static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
    
        public static void main(String[] args) throws InterruptedException {
    
            for (int i = 0; i < 1000; i++) {
    
                int finalI = i;
                threadPool.submit(new Runnable() {
                    @Override
                    public void run() {
                        String date = new ThreadLocalNormalUsage04().date(finalI);
                        System.out.println(date);
                    }
                });
            }
            threadPool.shutdown();
        }
    
        public String date(int seconds) {
            Date date = new Date(1000 * seconds);
            String s = null;
            synchronized (ThreadLocalNormalUsage04.class) {
                s = dateFormat.format(date);
            }
            return s;
        }
    }
    

    使用synchronized确实可以解决时间重复的问题,然而synchronized属于重量级锁,它会无时无刻进行锁住对象,而不考虑到程序实际的竞争情况,大多数程序在都是进行交替执行,也就是说不存在资源的竞争。但是这样一个一个排队执行会严重影响效率,尤其是对于像SimpleDateFormat这样的工具类,很多地方都会使用,加锁效率很低。综上所述,我们继续优化代码。

    五、最好的解决方案,使用ThreadLocal

    ThreadLocal图示
    /**
     * 利用ThreadLocal
     */
    public class ThreadLocalNormalUsage05 {
        //使用固定数量的线程池
        public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
        public static void main(String[] args) throws InterruptedException {
    
            for (int i = 0; i < 1000; i++) {
    
                int finalI = i;
                threadPool.submit(new Runnable() {
                    @Override
                    public void run() {
                        String date = new ThreadLocalNormalUsage05().date(finalI);
                        System.out.println(date);
                    }
                });
            }
            threadPool.shutdown();
        }
    
        public String date(int seconds) {
            Date date = new Date(1000 * seconds);
    //        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
            SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
            return dateFormat.format(date);
        }
    }
    
    class ThreadSafeFormatter{
        public static ThreadLocal<SimpleDateFormat>dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>(){
            @Override
            protected SimpleDateFormat initialValue() {
                return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
            }
        };
    }
    

    使用ThreadLocal是线程安全的,而且没有synchronized那样的性能问题,多个线程可以并行执行,每一个线程都有自己独享的对象。

    小结:以上通过SimpleDateFormat类在多线程环境下出现的问题逐步解决,展示了ThreadLocal的其中一种使用场景。在每个类都需要各自的空间时可以通过ThreadLocal初始化方法初始化一个对象,将这个对象存入当前线程,当线程被线程池复用时不会重复创建对象,使工具类可以在保证安全的前提下达到更高的效率。

    相关文章

      网友评论

        本文标题:SimpleDateFormat的进化之路

        本文链接:https://www.haomeiwen.com/subject/srhrdktx.html