从SimpleDateFormat看线程安全的实现方法

1. SimpleDateFormat的线程安全

  • 之前的博客:java时间格式中大小写问题,记录了使用SimpleDateFormat时,时间格式小时的HH写成了hh导致的问题
  • 当时,关于时间差异的问题,同事还提醒过我SimpleDateFormat不是线程安全的,不要定义成类的成员变量
  • 自己平时使用SimpleDateFormat时,一般都是立即使用立即定义,也就是将其定义为局部变量
  • 定义为局部变量,自然也就不存在线程安全的问题

1.1 多线程下使用SimpleDateFormat

  • 多线程访问同一个SimpleDateFormat进行时间转换

    public class TimeUtilsTest {
          
          
        private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    
        public static void main(String[] args) {
          
          
            for (int i = 0; i < 10; ++i) {
          
          
                Thread thread = new Thread(() -> {
          
          
                    try {
          
          
                        System.out.println(sdf.parse("2021-12-13 15:17:27"));
                    } catch (ParseException e) {
          
          
                        e.printStackTrace();
                    }
                });
                thread.start();
            }
        }
    }
    
  • 程序运行起来以后,抛出NumberFormatException异常
    在这里插入图片描述

1.2 SimpleDateFormat线程不安全的解决方案

1.2.1 synchronized(互斥同步)

  • 既然多线程访问共享的SimpleDateFormat变量,存在线程安全问题

  • 那就加上synchronized,使得同一时刻只有一个线程使用SimpleDateFormat变量,从而保证线程安全

    Thread thread = new Thread(() -> {
          
          
        try {
          
          
            synchronized (sdf) {
          
           // 加锁,保证线程安全
                System.out.println(sdf.parse("2021-12-13 15:17:27"));
            }
        } catch (ParseException e) {
          
          
            e.printStackTrace();
        }
    });
    

1.2.2 局部变量(栈封闭)

  • synchronized的使用,使得程序的执行效率大大降低(锁的获取和释放、同一时刻只能有一个线程使用)

  • 可以在线程内部定义SimpleDateFormat局部变量

    public class TimeUtilsTest {
          
          
        public static void main(String[] args) {
          
          
            for (int i = 0; i < 10; ++i) {
          
          
                Thread thread = new Thread(() -> {
          
          
                    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                    try {
          
          
                        System.out.println(sdf.parse("2021-12-13 15:17:27"));
                    } catch (ParseException e) {
          
          
                        e.printStackTrace();
                    }
                });
                thread.start();
            }
        }
    }
    
  • 由于局部变量存储在虚拟机栈中,不存在多线程共享的问题,自然也就没有线程安全问题

  • 局部变量为线程私有,受益于于栈封闭

1.2.3 ThreadLocal(线程本地存储)

  • 受到局部变量的启发,我们还可以实现线程间的隔离:将SimpleDateFormat变量与线程绑定

  • 这时,大名鼎鼎的ThreadLocal就可以派上用场了:

    • ThreadLocal使得每个线程都有自己的SimpleDateFormat变量副本,彼此之间相互独立,不存在线程安全的问题
  • 代码如下:

    public class TimeUtilsTest {
          
          
        private static ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<>();
    
        public static void main(String[] args) {
          
          
            for (int i = 0; i < 10; ++i) {
          
          
                Thread thread = new Thread(() -> {
          
          
                    try {
          
          
                        threadLocal.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
                        System.out.println(threadLocal.get().parse("2021-12-13 15:17:27"));
                    } catch (ParseException e) {
          
          
                        e.printStackTrace();
                    }
                });
                thread.start();
            }
        }
    }
    
  • 阿里编程规范也建议使用ThreadLocal保证SimpleDateFormat的线程安全
    在这里插入图片描述

2. 如何实现线程安全

  • 在之前的学习中,对于如何实现线程安全,更多的关注于互斥同步
  • 通过synchronized或lock,保证同一时刻只有一个线程能访问共享数据
  • 其实,还有很多线程线程安全的方法

2.1 不可变

  • 变量一旦正确初始化,就不可改变
  • 多线程中,这个变量将永远处于一致的状态,
  • 不可变的类型:
    • final关键字修饰的基本数据类型
    • String
    • 枚举类型
    • Number的部分子类,如Integer、Long等包装类型
    • 对于集合类型,可以使用Collections.unmodifiableXXX()方法来获取一个不可变的集合

2.2 互斥同步

  • synchronized和各种lock

2.3 非阻塞同步

  • 互斥同步,又称阻塞同步
    • 最主要的问题是线程阻塞和唤醒所带来的性能问题
    • 互斥同步属于一种悲观并发策略:无论共享数据是否存在竞争,都需要加锁,不然肯定会出现问题
  • 相对的,是基于冲突检测的乐观并发策略
    • 先尝试进行操作,如果不存在多线程竞争,则操作成功;否则,采取补偿措施(不断重试,知道成功为止)
    • 乐观并发策略的许多实现都不需要将线程阻塞,因此又称非阻塞同步

非阻塞同步的方案:

  • CAS
  • 原子类: AtomicInteger、AtomicIntegerArray、AtomicReference等

2.4 无同步方案

  • 之所以需要同步,是因为多线程访问共享数据导致的
  • 若不存在数据共享,也就无需采取同步措施保证线程安全

无同步方案:

  • 栈封闭: 多线程访问同一个方法的局部变量,这些局部变量存储在栈中,是线程私有的,无需同步就能保证线程安全
  • 线程本地存储(ThreadLocal): 每个线程拥有自己的、独立的共享数据副本(线程隔离),无需同步就能保证线程安全
  • 可重入代码:
    • 可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身)
    • 而在控制权返回后,原来的程序不会出现任何错误

参考文档:

Guess you like

Origin blog.csdn.net/u014454538/article/details/121320639