我把 SimpleDateFormat 设成了静态共享变量,本地测试一切正常,一上线高并发就开始解析出乱七八糟的日期、甚至直接抛异常,我对着随机错误查了好几天的深度复盘

为了"性能",我把 SimpleDateFormat 设成了静态全局共享的常量,省去每次 new 的开销。本地、单测、小流量预发全都正常。可一上生产高并发,日志里就冒出乱七八糟的日期(年份变几万年后)、还频繁抛 NumberFormatException、ArrayIndexOutOfBoundsException,而且毫无规律、本地死活复现不出。盯着"高并发才出问题"我才醒悟去查文档:SimpleDateFormat 有状态、非线程安全,内部共享一个 Calendar,多线程并发读写时状态被踩踏。这篇从它非线程安全的根源,讲到改用不可变的 DateTimeFormatter、ThreadLocal、局部 new 的正解、Java 常见非线程安全类、保证线程安全的四种手段取舍,以及那句最戳心的——单测过了不代表并发安全,共享可变状态是万恶之源。

我把 SimpleDateFormat 设成了静态共享变量,本地测试一切正常,一上线高并发就开始解析出乱七八糟的日期、甚至直接抛异常的深度复盘

这是一个让我对"线程安全"刻骨铭心的故事。我在一个高并发的接口里,需要频繁地格式化和解析日期。为了"性能",我自作聪明地,把 SimpleDateFormat 声明成了一个静态的、全局共享的常量——想着这样就不用每次都 new 一个,省点开销。本地开发、单元测试、甚至小流量的预发环境,它都跑得好好的,没有任何异常。我以为这是一段既高效又优雅的代码。

可一上生产、流量一大,诡异的事情就接连不断地发生了:日志里,时不时冒出格式化/解析出来的乱七八糟的日期——有的年份变成了几万年后,有的月份日期完全对不上输入;更频繁的,是直接抛出 NumberFormatExceptionArrayIndexOutOfBoundsException 这种莫名其妙的异常,堆栈直指 SimpleDateFormat 内部。最让我抓狂的是,这些错误毫无规律:同样的日期字符串,大部分时候解析得好好的,偶尔就突然出错;我在本地怎么都复现不出来。我一度怀疑是数据脏了、是 JVM 有 bug。直到我盯着"高并发才出问题、单线程从不出问题"这个现象,猛然醒悟,去查了 SimpleDateFormat 的文档——那一行被我忽略了无数次的警告,赫然在目:"Date formats are not synchronized.(日期格式化器不是同步的)它建议为每个线程创建独立的实例。我这才明白:SimpleDateFormat有状态的、非线程安全的!它内部持有一个用来做日期计算的 Calendar 对象,而格式化/解析的过程,会读写这个共享的 Calendar。当我把它设成全局共享、又被多个线程同时调用时,这些线程就在同时读写同一个 Calendar——彼此的中间状态相互覆盖、踩踏,于是,要么算出一个被别的线程污染了的、错误的日期,要么在内部数组操作时,因为状态错乱,直接抛出异常。我为了省一点 new 的开销,把一个非线程安全的对象拿去多线程共享,亲手埋下了这颗在高并发下随机引爆的炸弹。

故障现场:一个被多线程共享的 SimpleDateFormat

我把那段"自以为高效、实则致命"的代码,简化出来给你看:

public class DateUtil {
    // ✗ 灾难: 静态共享的 SimpleDateFormat, 被多个线程同时使用!
    private static final SimpleDateFormat SDF =
        new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static String format(Date date) {
        return SDF.format(date);          // 多线程同时调用 → 内部 Calendar 被踩踏
    }

    public static Date parse(String s) throws ParseException {
        return SDF.parse(s);              // 多线程同时调用 → 解析出错乱日期 / 抛异常
    }
}

// 在高并发下会发生什么:
//   线程A: SDF.parse("2024-01-15 ...") —— 正在用内部的 Calendar 计算
//   线程B: 同时 SDF.parse("2024-06-20 ...") —— 也在改同一个 Calendar!
//   → 两个线程的中间状态互相覆盖:
//     - A 算到一半, Calendar 被 B 改了 → A 得到一个错误的、混合的日期
//     - 或内部 digitList 等共享数组被并发改写 → NumberFormatException
//       / ArrayIndexOutOfBoundsException / 莫名其妙的乱日期

// 根因: SimpleDateFormat 是"有状态"的(内部持有可变的 Calendar),
//       且"非线程安全"。多线程共享同一个实例, 状态必然被踩踏。

看着这段代码,我后背直冒冷汗。问题的核心,是我犯了一个并发编程里最经典的错误:把一个"有状态、非线程安全"的对象,当成全局共享变量,给多个线程同时使用SimpleDateFormat 之所以"非线程安全",根源在于它是有状态的:它内部,持有一个 Calendar 类型的成员变量,用来在格式化/解析的过程中,做日期的计算和中转;此外还有 digitList 等用于数字解析的可变缓冲。这些,都是实例级别的、可变的共享状态format()/parse() 执行时,它们会读写这个内部的 Calendar问题就出在这里:当我把 SimpleDateFormat 设成静态共享、又被多个线程并发调用时,这些线程,就在同时读写同一个 Calendar 实例——线程 A 刚把 Calendar 设置成 1 月,还没来得及取结果,线程 B 就把它改成了 6 月;A 再去取,拿到的,就是被 B 污染过的、错误的日期。更糟的是,如果两个线程同时在操作内部那个用于解析数字的共享数组(digitList),就会把它的状态彻底搞乱,导致 NumberFormatExceptionArrayIndexOutOfBoundsException 这种内部异常。这就完美解释了我的所有困惑:为什么单线程(本地、单测)从不出错?——因为单线程下,没有第二个线程来踩踏 Calendar,状态始终是一致的。为什么高并发下随机出错?——因为出错与否,取决于两个线程是否恰好在同一时刻读写了那个共享的 Calendar,这是一种竞态(race condition),本质上是随机的、难以复现的。我为了省那一点点 new 的开销,把一个非线程安全的对象做成了共享单例,等于在高并发的大门口,埋了一颗随机引爆的地雷。

第一件事:搞懂"有状态 + 多线程共享 = 线程不安全"

定位到根源,我必须把"为什么 SimpleDateFormat 不是线程安全的、什么样的对象不能多线程共享",彻底搞清楚:

// 为什么 SimpleDateFormat 非线程安全? 看它内部:

// public class SimpleDateFormat extends DateFormat {
//     // DateFormat 里有这个: 可变的、实例级的共享状态!
//     protected Calendar calendar;         // ← 罪魁祸首: 格式化/解析都读写它
//     // 还有用于数字解析的 DigitList 等可变缓冲...
// }

// format/parse 的过程(简化), 会读写这个共享的 calendar:
//   parse(s):
//     1. calendar.clear();              // 清空内部 calendar
//     2. ...解析字符串, 一点点 calendar.set(...)  // 往里写
//     3. return calendar.getTime();     // 从里读出结果
//   ↑ 这"清空→写入→读出"的几步, 不是原子的!
//     多线程并发时, 一个线程的中间状态, 会被另一个线程冲掉。

// 判断"一个对象能不能多线程共享", 看两点:
//   1. 它有没有"可变的状态"(可变的成员变量)?
//   2. 它的方法在执行时, 会不会"读写"这些可变状态?
//   → 两者都是"是", 且没做同步 → 它就是"非线程安全"的, 不能裸着共享!

// 反之, "无状态"或"不可变"的对象, 天生线程安全:
//   - 无状态: 没有可变成员, 方法只依赖入参(如纯工具方法)
//   - 不可变(immutable): 创建后状态不再改变(如 String, 新的 DateTimeFormatter)
//   → 这类对象, 随便多线程共享, 都安全。

原理终于清晰了。SimpleDateFormat 非线程安全的根源,是它有可变的、实例级的共享状态:它(从父类 DateFormat)继承了一个 protected Calendar calendar 成员,以及用于数字解析的可变缓冲;而它的 format()/parse() 方法,在执行时,会读写这个共享的 calendar更要命的是,parse() 的过程是"清空 calendar → 逐步写入解析结果 → 从 calendar 读出最终时间"这样的多步操作,而这几步不是原子的!所以多线程并发时,一个线程刚写到一半,它的中间状态,就可能被另一个线程的"清空"或"写入"给冲掉——这就是竞态条件(race condition)由此,我总结出了一条判断"一个对象能不能被多线程共享"的通用法则:看两点——第一,它有没有"可变的状态"(可变的成员变量)?第二,它的方法在执行时,会不会"读写"这些可变状态?如果两者都是"是",且它内部又没做同步处理,那它就是"非线程安全"的,绝不能裸着拿去多线程共享。反过来,"无状态"的对象(没有可变成员、方法只依赖入参,比如纯工具方法)和"不可变(immutable)"的对象(创建后状态再也不变,比如 String、比如新的 DateTimeFormatter),则天生就是线程安全的——这类对象,你随便拿去多个线程共享,都绝对安全。我那次的错误,正是把一个明明"有可变状态、读写该状态"的 SimpleDateFormat,错当成了可以安全共享的对象——这是我对"线程安全"理解不到位,交的一笔学费。

第二件事:正解——优先用不可变的 DateTimeFormatter

搞懂了根因——"有状态的对象被多线程共享、状态被踩踏"——正解就清晰了:最推荐的,是改用 Java 8 引入的 java.time 包里的 DateTimeFormatter——它是不可变的、线程安全的,可以放心地做成静态共享常量。如果因为历史原因还必须用 SimpleDateFormat,那就要么每次用时新建一个局部实例(别共享),要么用 ThreadLocal每个线程一份独立的实例

// 正解1(最推荐): 用 java.time 的 DateTimeFormatter —— 不可变, 线程安全!
public class DateUtil {
    // ✓ DateTimeFormatter 是不可变的, 可以安全地静态共享
    private static final DateTimeFormatter FMT =
        DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    public static String format(LocalDateTime dt) {
        return dt.format(FMT);                 // 线程安全, 随便并发调用
    }
    public static LocalDateTime parse(String s) {
        return LocalDateTime.parse(s, FMT);    // 线程安全
    }
}

// 正解2: 实在要用 SimpleDateFormat → 每次新建局部实例(别共享)
public static String formatV2(Date date) {
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); // 局部变量
    return sdf.format(date);   // 每个线程各用各的, 互不干扰(略有 new 开销, 但安全)
}

// 正解3: 用 ThreadLocal —— 每个线程一份独立实例, 兼顾性能与安全
public class DateUtil3 {
    private static final ThreadLocal SDF =
        ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

    public static String format(Date date) {
        return SDF.get().format(date);   // 每个线程拿到的是"自己的"那一份, 不共享
    }
    // ⚠️ 用线程池时, 注意 ThreadLocal 的清理, 避免内存泄漏(用完 remove 或注意复用)
}

// 核心: 让"共享的"东西"不可变/线程安全"(正解1),
//   或让"有状态的"东西"不共享"(正解2/3, 局部 或 每线程一份)。

这套正解,核心都围绕一个原则:要么让"被共享的"东西不可变/线程安全,要么让"有状态的"东西不被共享正解1(最推荐):直接拥抱 Java 8 的 java.time 包——用 DateTimeFormatter 替代 SimpleDateFormat,用 LocalDateTime/LocalDate 替代 DateDateTimeFormatter 被设计成不可变的(immutable),它没有可变的内部状态,所以天生线程安全,可以毫无顾虑地做成静态共享常量;这是从根上解决问题、也最现代的方案。正解2(每次新建局部实例):如果项目里因为历史包袱,还得用 SimpleDateFormat,那最简单的安全做法,就是在方法内部,每次用时 new 一个局部变量——局部变量是每个线程的栈上独有的,根本不存在共享,自然就安全了(代价是每次有一点 new 的开销,但对绝大多数场景,这点开销完全可以接受,别过早优化)。正解3(ThreadLocal):如果你确实想复用、又怕频繁 new 的开销,那就用 ThreadLocal<SimpleDateFormat>——它会给每个线程,分配并保存一份独立的 SimpleDateFormat 实例,线程之间互不共享、互不干扰,兼顾了性能和安全(但要注意,在用线程池时,得留意 ThreadLocal 的清理,避免内存泄漏)。我那次的错误,是选了一个最危险的方案——共享一个有状态的实例;而正确的路,无论是哪一种,本质上都是在贯彻那个原则:有状态的东西别共享,要共享的东西得线程安全。

下面这张图,对比了"共享有状态实例"和"几种正解"的路径:

这张图的对比很清楚:左边红色那条,静态共享一个 SimpleDateFormat,多线程同时读写它内部的 Calendar,状态被踩踏,出乱日期或抛异常;右边绿色那几条,要么用不可变的 DateTimeFormatter(无可变状态、天生安全),要么每次 new 局部实例、或用 ThreadLocal(各线程隔离、不共享)。所有正解的共同点,是消除了"多线程共享同一个可变状态"这个根本病灶。

第三件事:Java 里还有哪些"非线程安全"的常见类

填平了 SimpleDateFormat 这个坑,我警觉起来,系统排查了 Java 里其它几个"经常被误当成线程安全、拿去共享"的类——它们都和 SimpleDateFormat 一样,是有状态、非线程安全的:

// Java 里常见的"非线程安全"类(别裸着多线程共享!):

// 1. SimpleDateFormat(本文) → 用 DateTimeFormatter / ThreadLocal / 局部new

// 2. 集合类: HashMap / ArrayList / HashSet 等都非线程安全
//    ✗ 多线程并发写 HashMap → 可能死循环(JDK7)/数据错乱/丢数据
//    ✓ 用 ConcurrentHashMap / Collections.synchronizedXxx / CopyOnWriteArrayList

// 3. StringBuilder → 非线程安全
//    ✓ 多线程共享场景用 StringBuffer(方法加了 synchronized), 或别共享

// 4. java.util.Random → 多线程共享有竞争(性能差且非严格安全)
//    ✓ 用 ThreadLocalRandom.current()

// 5. 普通的成员变量做"计数器" → i++ 不是原子的!
//    ✗ private int count;  count++;  (多线程下会丢更新)
//    ✓ 用 AtomicInteger / synchronized / LongAdder

// 6. 自己写的、含可变成员的工具类/单例
//    → 单例如果有可变状态, 一样非线程安全! 别因为是单例就放心共享。

// 判断口诀: "有可变状态 + 被多线程读写 + 没做同步" = 非线程安全
//   见到这种, 要么改成不可变, 要么别共享, 要么加同步/用并发工具类。

这一排查,让我对 Java 的线程安全,有了全面的警觉。除了 SimpleDateFormat,Java 标准库里,还有一大批"经常被误以为安全、随手拿去多线程共享"的非线程安全类:集合类(HashMap/ArrayList/HashSet 都非线程安全,并发写 HashMap 甚至可能在 JDK7 下导致死循环——要用 ConcurrentHashMap/CopyOnWriteArrayList 等并发容器);StringBuilder(非线程安全,多线程共享要用 StringBuffer 或干脆别共享);java.util.Random(多线程共享有竞争,用 ThreadLocalRandom);普通的 i++ 计数器(i++ 不是原子操作,多线程下会丢更新,要用 AtomicInteger/LongAdder);以及你自己写的、含可变成员的单例/工具类(很多人以为"单例"就安全,殊不知,单例如果持有可变状态,它一样是非线程安全的——单例只保证"实例唯一",不保证"线程安全")。这些类共同指向一个判断口诀:"有可变状态 + 被多线程读写 + 没做同步"= 非线程安全。一旦识别出一个对象符合这个特征,就绝不能裸着多线程共享它——要么把它改造成不可变的,要么干脆别共享(局部变量/ThreadLocal),要么给它加上同步、或换用对应的并发工具类。把这个口诀刻在脑子里,就能在写下"static 共享某个对象"这行代码之前,先警觉地问自己一句:它,线程安全吗?

第四件事:保证线程安全的几种手段,各有取舍

借着这次复盘,我把"如何保证一段代码/一个对象线程安全"的几种通用手段,系统地梳理了一遍,并比较了它们的取舍:

// 保证线程安全的几种通用手段:

// 手段1: 不可变(Immutable) —— 最优雅
//   对象一旦创建, 状态就不能再改 → 多线程只读, 天然安全。
//   例: String, Integer, DateTimeFormatter, 用 final + 不提供 setter
//   优点: 无锁、无开销、最安全; 缺点: 需要"改"时得新建对象。

// 手段2: 不共享(线程封闭) —— 最简单
//   让有状态的对象, 不被多线程共享:
//   - 局部变量(方法内 new): 天然线程封闭
//   - ThreadLocal: 每线程一份
//   优点: 思路简单、无锁竞争; 缺点: ThreadLocal 要注意清理(内存泄漏)。

// 手段3: 加锁(同步) —— 最通用但有代价
//   用 synchronized / ReentrantLock, 让同一时刻只有一个线程访问共享状态。
//   synchronized (lock) { /* 读写共享状态 */ }
//   优点: 通用, 什么都能保护; 缺点: 有性能开销、可能死锁、降低并发度。

// 手段4: 用并发工具类 —— 最实用
//   直接用 JDK 写好的、线程安全的并发容器/原子类:
//   - ConcurrentHashMap, CopyOnWriteArrayList
//   - AtomicInteger / AtomicLong / LongAdder(原子操作)
//   - 各种 BlockingQueue
//   优点: 久经考验、高性能; 缺点: 要选对适用场景。

// 选择优先级(经验):
//   能用"不可变"就用不可变 > 能"不共享"就不共享 >
//   > 用现成的"并发工具类" > 最后才考虑自己"加锁"
//   (越往后, 越容易出错、代价越大)

这一梳理,让我对"线程安全"的实现手段,有了体系化的认识。保证线程安全,本质上有四类通用手段,各有取舍:手段1(不可变,最优雅):让对象一旦创建,状态就不可改变,多线程只读自然安全(如 StringDateTimeFormatter)——无锁、无开销、最安全,代价是需要"改"时得新建对象。手段2(不共享/线程封闭,最简单):让有状态的对象压根不被多线程共享,用局部变量(天然封闭)或 ThreadLocal(每线程一份)——思路简单、无锁竞争,代价是 ThreadLocal 要注意清理。手段3(加锁/同步,最通用但有代价):用 synchronized/ReentrantLock 保证同一时刻只有一个线程访问共享状态——什么都能保护,但有性能开销、可能死锁、降低并发度。手段4(并发工具类,最实用):直接用 JDK 写好的线程安全容器和原子类(ConcurrentHashMapAtomicIntegerLongAdder 等)——久经考验、高性能。而我从中总结出一条选择的优先级:能用"不可变"就用不可变 > 能"不共享"就不共享 > 用现成的"并发工具类" > 最后才考虑自己"加锁"——越往后,越容易出错、代价越大。把这几种手段的取舍,整理成一张对比表:

手段 代表 优点 代价
不可变 DateTimeFormatter、String 无锁、最安全 改需新建对象
不共享 局部变量、ThreadLocal 简单、无竞争 ThreadLocal 要清理
加锁 synchronized、Lock 通用、什么都能护 开销、可能死锁
并发工具类 ConcurrentHashMap、Atomic 高性能、可靠 要选对场景

第五件事:单测过了,不代表并发安全

这次踩坑,在认知层面给了我最大的纠偏——它打破了我"代码跑通了、单测过了就万事大吉"的天真。我把这层反思,沉淀了下来:

认知纠偏: "单测过了"和"并发安全"是两码事

# 我的误解(错误的):
#   "本地跑通了、单元测试全绿了, 这代码就没问题了。"
#   → 我的测试, 都是"单线程"的, 它压根测不出"并发"下才暴露的问题。

# 真相: 并发 bug 有它独特的、可怕的性质:
#   1. 隐蔽: 单线程下永远不出现, 只在多线程并发时才暴露。
#   2. 随机: 出不出错, 取决于线程调度的"时序", 是随机的、偶发的。
#   3. 难复现: 本地、测试环境流量小, 很难触发; 一上生产高并发就爆。
#   4. 后果重: 数据错乱、脏数据落库, 可能造成难以挽回的损失。
#   → "测试通过"给了我虚假的安全感, 而并发的雷, 还埋在那里。

# 更深的根源:"共享可变状态"是并发问题的万恶之源
#   - 几乎所有并发 bug, 根子都在"多个线程, 共享并修改同一份可变数据"。
#   - 解药也就那几条: 不可变(不让改)、不共享(不让多线程碰同一份)、
#     加锁/原子(让修改有序进行)。

# 正确的习惯:
#   1. 写下"共享变量"(尤其 static)时, 警觉地问: 它会被多线程访问吗? 它线程安全吗?
#   2. 别迷信"单测通过"——并发安全, 要专门用并发测试/压测/代码审查来保障。
#   3. 默认假设你的服务是多线程的(Web 服务每个请求一个线程!), 共享的东西都要线程安全。

核心: 单线程的测试, 测不出并发的雷。对每一个"共享可变状态",
  都要专门审视它的线程安全性——这是写并发服务的基本功。

这层反思,是这次踩坑给我最高维度的收获。复盘我的误解,根源是我天真地以为"本地跑通了、单元测试全绿了,代码就没问题了"——可我的测试,清一色都是单线程的,它压根测不出那些只在"多线程并发"下才会暴露的问题。而并发 bug,有着它独特而可怕的性质:它隐蔽(单线程永远不出现)、随机(出错与否取决于线程调度的时序,偶发难测)、难复现(本地和测试环境流量小,很难触发,一上生产高并发就爆)、后果重(数据错乱、脏数据落库,可能造成难以挽回的损失)。我那句"测试通过了",给了我一种虚假的安全感,而并发的雷,其实一直安静地埋在那里,只等高并发来引爆。而这件事更深的根源,我也想明白了:"共享可变状态",是几乎一切并发问题的万恶之源——你回想任何一个并发 bug,根子,几乎都在"多个线程,共享并修改同一份可变数据";而它的解药,也就那么几条:不可变(不让改)、不共享(不让多个线程碰同一份)、加锁/原子(让修改有序进行)。由此,我给自己立下了几条对治的习惯:第一,每当我写下一个"共享变量"(尤其是 static 的)时,都要警觉地问自己:它会被多线程访问吗?它线程安全吗?第二,别再迷信"单测通过"——并发安全,要专门靠并发测试、压测、和代码审查来保障。第三,要默认假设你的服务就是多线程的(别忘了,Web 服务里,几乎每个请求都跑在一个独立的线程上!),所以,凡是被共享的东西,都必须是线程安全的。归根结底:单线程的测试,测不出并发的雷;对每一个"共享可变状态",都专门审视一遍它的线程安全性——这,是写好并发服务,绕不开的基本功。把"天真"和"清醒"两种心态对比成一张表:

维度 天真(踩坑) 清醒(稳)
对"单测通过" 等于没问题 只证明单线程逻辑对
对共享变量 随手 static 共享 先问它线程安全吗
对服务模型 没意识到是多线程 默认每请求一线程
验证方式 只跑单元测试 加压测/并发测试
并发 bug 上线被随机引爆 提前规避

一套"该不该共享这个对象"的决策流程

把这次踩坑的全部教训,我浓缩成了一张"想把一个对象设成共享(尤其 static)时,该怎么判断"的决策图,贴在了团队的并发规范里:

这张图,把我"血泪换来"的整套方法论,串成了一条可执行的路径:想把一个对象设成共享(尤其 static)时,先问它会不会被多线程访问(Web 服务里几乎一定会);若会,再看它有没有可变状态、方法会不会读写这状态——若无状态/不可变,放心共享;若有可变状态且方法会读写它,那它就是非线程安全的,绝不能裸着共享,要按"改用不可变替代品 > 别共享(局部/ThreadLocal) > 加锁/并发工具类"的优先级来处理。这条决策链,现在是我们团队每个人,在敲下 static 关键字之前,都会条件反射般过一遍的"安全检查"。

我立下的几条并发与线程安全规矩

这次"共享 SimpleDateFormat 被高并发引爆"的踩坑,让我把并发编程的注意事项,认真地立成了几条规矩:

  1. 日期格式化,优先用 java.timeDateTimeFormatter它不可变、线程安全,可放心静态共享;别再用 SimpleDateFormat 做共享变量。
  2. 非要用 SimpleDateFormat,就别共享。每次局部 new,或用 ThreadLocal 每线程一份。
  3. 敲下 static 共享前,先问它线程安全吗。有可变状态 + 多线程读写 + 没同步 = 非线程安全。
  4. 记牢常见的非线程安全类。HashMap/ArrayList/StringBuilder/Random/SimpleDateFormat——多线程共享要换并发版本或别共享。
  5. 单例不等于线程安全。单例若持有可变状态,一样不安全;单例只保证实例唯一。
  6. 保证线程安全按优先级选手段。不可变 > 不共享 > 并发工具类 > 自己加锁。
  7. 别迷信单测通过。并发 bug 隐蔽、随机、难复现,要靠压测、并发测试、代码审查专门保障;默认你的服务是多线程的。

写在最后

这次"我把 SimpleDateFormat 设成静态共享、被高并发随机引爆"的经历,是我在后端并发路上,一次很惊险、却也很受用的成长。它教给我的,远不止"SimpleDateFormat 不能共享"这一条具体的技术经验,更是一种对并发的敬畏之心——我们写的后端服务,几乎都跑在多线程的环境里(每一个请求,都是一个线程);而并发的世界,有它自己一套残酷的规则:共享可变状态是万恶之源,单线程的测试给不了你并发安全的保证,那些埋下的雷,会在你最意想不到的高并发时刻,随机引爆。

所以,当你写下任何一个"被多线程共享"的东西时——尤其是那个看似人畜无害的 static 关键字——请一定停下来,郑重地问自己一句:"它,线程安全吗?它有没有可变状态?会不会被多个线程同时读写?"就像 SimpleDateFormat,你只要有过这一念之间的警觉,就绝不会把它做成共享单例,也就不会经历我那些对着乱日期和随机异常、彻夜排查的痛苦。对并发多一分敬畏、对共享状态多一分审视,是从一个"能写功能"的程序员,走向一个"能写可靠的高并发服务"的工程师,必经的修炼。愿你写的每一段并发代码,都坚如磐石;也愿你我,在敲下每一个共享变量时,都心怀那份对线程安全的敬畏。共勉。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

我的 Go 函数明明在成功时返回了 nil,调用方的 if err != nil 却死活为真,把成功请求全当失败处理,我对着这个见鬼的判断查了大半天的深度复盘

2026-6-1 21:43:10

技术教程

我的查询明明在手机号字段上建了索引,却慢得像在全表扫描,EXPLAIN 一看果然没走索引,折腾半天发现罪魁祸首竟是一个隐式类型转换的深度复盘

2026-6-1 21:53:33

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索