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): 每个线程拥有自己的、独立的共享数据副本(线程隔离),无需同步就能保证线程安全
- 可重入代码:
- 可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身)
- 而在控制权返回后,原来的程序不会出现任何错误
参考文档:
- SimpleDateFormat线程不安全的5种解决方案!
- 短小精悍的Java学习笔记:线程安全
- 针对为何线程不安全的讲解:Java面试必问:ThreadLocal终极篇 淦!