美文网首页多线程
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