深入理解高并发编程 - SimpleDateFormat 类的线程安全问题
1、重现与解决
1.1、重现
import java.text.SimpleDateFormat;
import java.util.Date;public class UnsafeSimpleDateFormatExample {public static void main(String[] args) {SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");Runnable task = () -> {for (int i = 0; i < 5; i++) {String formattedDate = sdf.format(new Date());System.out.println(Thread.currentThread().getName() + ": Formatted Date: " + formattedDate);try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}};Thread thread1 = new Thread(task, "Thread-1");Thread thread2 = new Thread(task, "Thread-2");thread1.start();thread2.start();}
}
在这个示例中,创建了两个线程,它们共享同一个 SimpleDateFormat 实例来格式化当前日期和时间。由于 SimpleDateFormat 内部状态不是线程安全的,可能会观察到以下问题:
输出的日期格式可能交错出现,因为两个线程在竞争访问共享的 SimpleDateFormat 实例,导致格式化结果混乱。
可能会抛出异常,如 ArrayIndexOutOfBoundsException 或 StringIndexOutOfBoundsException,这是因为线程竞争导致 SimpleDateFormat 内部状态不一致。
这个示例中,每个线程都会尝试格式化当前日期和时间,并在输出时稍微延迟一段时间。由于没有对 SimpleDateFormat 进行线程保护,两个线程可能会相互干扰,导致输出的格式化结果不符合预期,甚至引发异常。
要避免这个问题,可以使用线程局部变量(ThreadLocal)来确保每个线程都有自己的 SimpleDateFormat 实例,或者使用线程安全的替代类,如 Java 8 中的 DateTimeFormatter。
1.2、使用线程局部变量(ThreadLocal)
package com.lfsun.main.basic.myjuc.depthstudy.threadsafequestion;import java.text.SimpleDateFormat;
import java.util.Date;public class ThreadLocalSimpleDateFormatExample {private static ThreadLocal<SimpleDateFormat> threadLocalSdf = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));public static void main(String[] args) {Runnable task = () -> {SimpleDateFormat sdf = threadLocalSdf.get();for (int i = 0; i < 5; i++) {String formattedDate = sdf.format(new Date());System.out.println(Thread.currentThread().getName() + ": Formatted Date: " + formattedDate);try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}};Thread thread1 = new Thread(task, "Thread-1");Thread thread2 = new Thread(task, "Thread-2");thread1.start();thread2.start();}
}
使用 ThreadLocal 的主要目的是为每个线程提供一个独立的实例,从而避免共享资源的线程安全问题。不需要额外的同步机制,除非在某些特定情况下需要跨线程共享某些资源。
1.3、使用线程安全的替代类,如 Java 8 中的 DateTimeFormatter。
package com.lfsun.main.basic.myjuc.depthstudy.threadsafequestion;import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;public class DateTimeFormatterExample {public static void main(String[] args) {DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");Runnable task = () -> {for (int i = 0; i < 5; i++) {String formattedDate = dtf.format(LocalDateTime.now());System.out.println(Thread.currentThread().getName() + ": Formatted Date: " + formattedDate);try {Thread.sleep(100); // 模拟一些处理时间} catch (InterruptedException e) {e.printStackTrace();}}};// 创建和启动多个线程以演示线程安全Thread thread1 = new Thread(task, "Thread-1");Thread thread2 = new Thread(task, "Thread-2");thread1.start();thread2.start();}
}
1.4、Joda-Time
在 Java 8 引入新的 java.time 包之前,Joda-Time被广泛用于处理日期和时间。Joda-Time 提供了一组线程安全的日期和时间类,可以帮助解决 SimpleDateFormat 在多线程环境中的线程安全问题。
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;public class JodaTimeExample {public static void main(String[] args) {DateTimeFormatter dtf = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss");Runnable task = () -> {for (int i = 0; i < 5; i++) {String formattedDate = dtf.print(DateTime.now());System.out.println(Thread.currentThread().getName() + ": Formatted Date: " + formattedDate);try {Thread.sleep(100); // 模拟一些处理时间} catch (InterruptedException e) {e.printStackTrace();}}};// 创建和启动多个线程以演示线程安全Thread thread1 = new Thread(task, "Thread-1");Thread thread2 = new Thread(task, "Thread-2");thread1.start();thread2.start();}
}
在这个示例中,使用了 Joda-Time 库的 DateTime 类和 DateTimeFormatter 来格式化和解析日期。Joda-Time 提供的日期和时间类都是线程安全的,因此可以在多线程环境中共享使用,无需担心线程安全问题。
需要注意的是,从 Java 8 开始,Java 标准库中引入了新的日期和时间 API(java.time 包),其中包含了线程安全的日期和时间类。所以,在 Java 8 及以后的版本中,也可以使用这个新的日期和时间 API 来避免 SimpleDateFormat 的线程安全问题。
2、SimpleDateFormat 类为何不是线程安全的?
SimpleDateFormat 类不是线程安全的主要原因在于其内部状态以及对共享资源的操作可能会导致竞争条件和数据不一致。以下是一些导致 SimpleDateFormat 类不是线程安全的原因:
内部状态共享: SimpleDateFormat 内部维护了日期格式化模式(pattern)、时区信息、语言环境等状态。多个线程同时使用同一个 SimpleDateFormat 实例时,它们会访问和修改相同的内部状态,可能导致数据混乱。线程间竞争: 在多线程环境中,多个线程同时对同一个 SimpleDateFormat 实例进行格式化或解析操作时,它们会竞争修改内部状态,导致输出结果混乱。非原子性操作: SimpleDateFormat 内部的状态修改操作可能不是原子性的,这意味着多个线程在同时修改状态时可能会产生竞争条件,导致数据错误或异常。共享的 Calendar 实例: SimpleDateFormat 内部使用了共享的 Calendar 实例,可能会在多线程环境中产生问题,因为 Calendar 本身也不是线程安全的。
由于上述原因,如果多个线程同时使用同一个 SimpleDateFormat 实例进行格式化和解析操作,就有可能导致线程不安全的问题,从而产生意外的结果、异常或混乱的输出。