线程安全问题规避

在多线程环境下,日期时间处理是线程安全问题的高发区。Java8 之前的日期时间类(如 SimpleDateFormat、Calendar)存在严重的线程安全问题,主要原因是它们的内部状态可变,在多线程并发访问时会导致数据错乱。而 Java8 引入的新日期/时间 API 不允许修改内部状态,如 LocalData、LocalDataTime 等,每次修改均会返回一个新的实例,从根本上解决了这些问题。

传统 API

下面以 SimpleDateFormat 为例,演示 SimpleDateFormat 存在的线程安全隐患。代码如下:

package com.hxstrive.java_date_time.thread;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SDFThreadSafeDemo {
    // 共享的SimpleDateFormat实例
    private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(10);
        
        // 10个线程同时使用SDF格式化日期
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            executor.submit(() -> {
                try {
                    Date date = new Date(1000L * 60 * 60 * 24 * finalI);
                    System.out.println(SDF.format(date));
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }
        executor.shutdown();
    }
}

运行结果:

1970-01-04 08:00:00
1970-01-01 08:00:00
1970-01-04 08:00:00
1970-01-04 08:00:00
1970-01-01 08:00:00
1970-01-05 08:00:00
1970-01-05 08:00:00
1970-01-04 08:00:00
1970-01-04 08:00:00
1970-01-04 08:00:00

从运行结果可知,出现重复日期。还有可能抛出 NumberFormatException 或 IllegalArgumentException。导致这些问题的根本原因是 SimpleDateFormat 内部使用 Calendar 对象维护状态,多线程并发修改导致状态混乱。如下图:

线程安全问题规避

线程安全问题规避

为解决线程安全问题,有如下方案:

  • 每次使用时创建新实例:频繁创建销毁对象,性能开销大。

  • 使用 synchronized 同步:多线程竞争锁,导致性能下降。

  • ThreadLocal 存储:需要手动管理生命周期,代码复杂且易内存泄漏。

  

现代 API

Java8 引入的 java.time 包(如 LocalDateTime、DateTimeFormatter)采用不可变对象设计,从根本上保证了线程安全:

  1. 不可变对象特性:对象创建后状态无法修改,所有修改操作都返回新对象。因此,无需担心并发修改导致的状态不一致。

  2. 核心线程安全类:

    • LocalDate、LocalTime、LocalDateTime:日期时间对象

    • ZonedDateTime:带时区的日期时间对象

    • DateTimeFormatter:日期时间格式化器

    • Instant:时间戳对象

简单示例:

package com.hxstrive.java_date_time.thread;

import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ModernDateTimeThreadSafe {
    // 共享的DateTimeFormatter实例(线程安全)
    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(10);
        
        // 10个线程同时使用共享的格式化器
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            executor.submit(() -> {
                try {
                    // 创建时间戳(不可变对象)
                    Instant instant = Instant.EPOCH.plusSeconds(60 * 60 * 24L * finalI);
                    // 格式化操作(线程安全)
                    String result = FORMATTER.format(instant.atZone(java.time.ZoneId.systemDefault()));
                    System.out.println(result);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }
        executor.shutdown();
    }
}

运行结果:

1970-01-05 08:00:00
1970-01-08 08:00:00
1970-01-09 08:00:00
1970-01-03 08:00:00
1970-01-06 08:00:00
1970-01-04 08:00:00
1970-01-01 08:00:00
1970-01-07 08:00:00
1970-01-02 08:00:00
1970-01-10 08:00:00

从结果可知,输出的日期没有重复,是预期的结果。这是因为 DateTimeFormatter 是不可变的,可安全地作为静态常量在多线程间共享。

  

说说我的看法
全部评论(
没有评论
关于
本网站专注于 Java、数据库(MySQL、Oracle)、Linux、软件架构及大数据等多领域技术知识分享。涵盖丰富的原创与精选技术文章,助力技术传播与交流。无论是技术新手渴望入门,还是资深开发者寻求进阶,这里都能为您提供深度见解与实用经验,让复杂编码变得轻松易懂,携手共赴技术提升新高度。如有侵权,请来信告知:hxstrive@outlook.com
其他应用
公众号