当前位置: 首页 > news >正文

SimpleDateFormat线程安全终极方案:ThreadLocal魔法抽屉实践

图片

序章:魔法抽屉的设计图

在作坊的中央,挂着几张由“ThreadLocal大宗师”绘制的魔法抽屉设计图。

设计图一:专属日期刻印章 (dateFormatHolder)

// 代码片段 1: dateFormatHolder 的声明
private static final ThreadLocal<SimpleDateFormat> dateFormatHolder = new ThreadLocal<SimpleDateFormat>() {@Overrideprotected SimpleDateFormat initialValue() {System.out.println(Thread.currentThread().getName() + ": initializing SimpleDateFormat");return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");}
};

故事解读:
这是第一张设计图,名为 dateFormatHolder。它规定了一种特殊的抽屉,用来存放“日期刻印章”(SimpleDateFormat)。日期刻印章这种工具比较娇贵,每个工匠使用时都需要自己独有的一枚,不能混用,否则会出错。

  • • new ThreadLocal<SimpleDateFormat>(): 这声明了我们要设计一种存放“日期刻印章”的抽屉。

  • • initialValue(): 这是设计图上最神奇的部分!它说:“当一位工匠(线程)第一次来到他的工作台,需要使用这种‘日期刻印章抽屉’(第一次调用 get())时,如果抽屉是空的,魔法会自动为他变出一枚全新的、标准格式(yyyy-MM-dd HH:mm:ss)的刻印章放进去。” 每个工匠都会得到属于自己的那一枚。

设计图二:任务身份令牌 (transactionIdHolder)

// 代码片段 2: transactionIdHolder 的声明
private static final ThreadLocal<String> transactionIdHolder = new ThreadLocal<>();

故事解读:
第二张设计图 transactionIdHolder,则规定了一种存放“任务身份令牌”(一个字符串,比如交易ID)的抽屉。这种抽屉在工匠第一次需要时,里面是空的(因为没有 initialValue()),需要工匠自己把令牌放进去。

设计图三:个人计数器 (perThreadCounter)

// 代码片段 3: perThreadCounter 的声明
private static final ThreadLocal<Integer> perThreadCounter = new ThreadLocal<Integer>() {@Overrideprotected Integer initialValue() {System.out.println(Thread.currentThread().getName() + ": initializing counter to 0");return 0;}
};

故事解读:
第三张设计图 perThreadCounter,设计了一种存放“个人计数器”(一个整数)的抽屉。和日期刻印章类似,如果工匠第一次打开这个抽屉,会发现里面已经有一个初始值为 0 的计数器了。


第一卷:刻印章的秘密 - 安全共享娇贵工具

// 代码片段 4: 使用 dateFormatHolder
// ... 在 main 方法的第一个循环中 ...
executor.submit(() -> {// ...SimpleDateFormat sdf = dateFormatHolder.get(); // 获取自己的刻印章String formattedDate = sdf.format(new Date(System.currentTimeMillis() + taskId * 100000));System.out.println(threadName + ": Task " + taskId + " formatted date: " + formattedDate);// ...
});

故事解读:
现在,许多任务被分配下来,工匠们(线程池中的线程)纷纷来到各自的魔法工作台开始工作。

  • • executor.submit(...): 作坊总管(ExecutorService)将一项任务交给一位空闲的工匠。

  • • SimpleDateFormat sdf = dateFormatHolder.get();: 当工匠(比如名为 pool-1-thread-1 的工匠)需要日期刻印章时,他会尝试打开自己工作台上那个依照 dateFormatHolder 设计图打造的专属抽屉。

    • • 如果是第一次打开:抽屉是空的,但 initialValue() 魔法启动,一枚新的刻印章被变出来放进这位工匠的抽屉里。

    • • 如果之前已经用过:他会直接拿出上次放在里面的那枚属于自己的刻印章。

  • • sdf.format(...): 工匠使用自己抽屉里的刻印章来完成工作。其他工匠也在用他们各自抽屉里的刻印章,互不干扰。

核心用途1:为每个线程提供独立的、非线程安全对象的副本,从而实现线程安全。


第二卷:令牌的传递与遗忘 - remove()的重要性

// 代码片段 5: 使用 transactionIdHolder 并演示 remove() 的问题
// ... 在 main 方法的第二个循环中 ...
executor.submit(() -> {// ...String previousTxnId = transactionIdHolder.get(); // 尝试获取令牌if (previousTxnId != null) {System.out.println(threadName + ": Task " + taskId + " WARNING! Found leftover transaction ID: " + previousTxnId);}transactionIdHolder.set(transactionId); // 放入新令牌// ... 工作 ...System.out.println(threadName + ": Task " + taskId + " finished with Transaction ID: " + transactionIdHolder.get());transactionIdHolder.remove(); // 清理抽屉!System.out.println(threadName + ": Task " + taskId + " Transaction ID removed. Current value: " + transactionIdHolder.get());
});

故事解读:
接下来,工匠们开始处理需要“任务身份令牌”的任务。

  • • String previousTxnId = transactionIdHolder.get();: 工匠在开始新任务前,先检查一下自己的“任务身份令牌”抽屉。

  • • if (previousTxnId != null) { ... }警报! 如果工匠发现抽屉里竟然有之前任务留下的令牌,这就意味着上一个使用这张工作台完成任务的工匠(可能是自己,也可能是同一个工匠身份但处理不同任务)忘了清理!这就像你上班时发现办公桌上还留着前一个同事的私人文件,可能会造成混淆。

  • • transactionIdHolder.set(transactionId);: 工匠将当前任务的专属令牌放入自己的抽屉。

  • • transactionIdHolder.remove();这是关键一步! 当工匠完成当前任务后,他非常负责任地将自己抽屉里的令牌取走并销毁(或者说把抽屉清空)。这样,当这个工匠(这个线程)下次被分配一个新任务时,或者当这张工作台(这个线程)被另一个任务复用时,抽屉里就是干净的,不会拿到上一个任务的旧令牌。

核心用途2:在线程的生命周期内携带与该线程相关的上下文信息,如用户身份、事务ID等。务必在不再需要时 remove(),尤其是在使用线程池时,防止内存泄漏或数据串扰。


第三卷:计数器的累加与重置 - initialValue 与 remove 的配合

// 代码片段 6: 使用 perThreadCounter
// ... 在 main 方法的第三个循环中 ...
executor.submit(() -> {// ...Integercount= perThreadCounter.get(); // 获取个人计数器System.out.println(threadName + ": Task " + taskId + " initial counter: " + count);count++;perThreadCounter.set(count); // 更新个人计数器// ...if (taskId % 2 == 0) {System.out.println(threadName + ": Task " + taskId + " (even) REMOVING counter.");perThreadCounter.remove(); // 清理抽屉} else {System.out.println(threadName + ": Task " + taskId + " (odd) NOT removing counter.");}
});

故事解读:
最后,工匠们接到了需要使用“个人计数器”的任务。

  • • Integer count = perThreadCounter.get();: 工匠打开自己的计数器抽屉。

    • • 如果抽屉是空的(比如上个任务完成后调用了 remove(),或者这是他第一次使用这个抽屉),initialValue() 魔法会给他一个值为 0 的新计数器。

    • • 如果抽屉里有上次任务留下的计数器(因为上次没调用 remove()),他会拿出那个值。

  • • count++; perThreadCounter.set(count);: 工匠将计数器加一,然后放回自己的抽屉。

  • • if (taskId % 2 == 0) { perThreadCounter.remove(); }: 对于某些任务(比如任务ID是偶数的),工匠完成任务后会把计数器从抽屉里拿走并清空。这样,当这个工匠(这个线程)下次再做需要计数器的任务时,他又会从 initialValue() 规定的 0 开始。

  • • else { ... NOT removing ... }: 对于另一些任务,工匠保留了计数器。如果这个工匠(这个线程)紧接着又做了一个需要计数器的任务,他会从上一次的计数值继续累加。

核心用途3:维护线程私有的状态,可以有默认初始值,并且可以根据逻辑选择是否在任务结束后重置(通过 remove())或保留(不 remove())该状态供同一线程的后续任务使用。


终章:魔法抽屉的智慧

特性

ThreadLocal (魔法抽屉设计图)

核心作用

为每个工匠(线程)提供一个独立的、私人的储物空间(变量副本)。

set(value)

工匠将自己的私人物品放入自己工作台上的专属抽屉。

get()

工匠从自己工作台上的专属抽屉里取出自己的物品。如果抽屉是空的且有initialValue设计,则先按设计变出初始物品再取出。

remove()

工匠完成当前任务后,清空自己抽屉里的物品。在线程池环境中至关重要!

initialValue()

抽屉设计图上的魔法:若抽屉为空,首次get()时自动放入一个默认物品。

何时使用 ThreadLocal

  1. 1. 线程隔离:当你想为每个线程维护一个独立的变量副本时。例如,SimpleDateFormat 不是线程安全的,用 ThreadLocal 可以让每个线程拥有自己的实例。

  2. 2. 上下文传递:当你想在线程的调用栈中隐式传递数据,而不想在每个方法参数中都显式传递时。例如,保存当前用户的ID、事务ID等。

  3. 3. 避免锁竞争:在某些情况下,如果一个对象的状态可以被分解为每个线程的独立状态,使用 ThreadLocal 可以避免对共享对象进行同步访问,从而提高性能。

重要警示:
在“万能工匠大作坊”(尤其是使用线程池时),工匠们(线程)是会被循环利用的。如果一个工匠完成任务后没有通过 remove() 清理他的“私人魔法抽屉”,那么当这个工匠(同一个线程)被分配下一个任务时,新任务可能会看到上一个任务遗留的物品,导致数据混乱或“内存泄漏”(因为这些旧物品可能永远不会被回收,如果 ThreadLocal 本身的引用还在)。所以,用完 ThreadLocal 变量后,务必调用 remove() 方法清理!

完整代码

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;public class ThreadLocalDemo {// 1. SimpleDateFormat is not thread-safe. Using ThreadLocal to provide each thread its own instance.private static final ThreadLocal<SimpleDateFormat> dateFormatHolder = new ThreadLocal<SimpleDateFormat>() {@Overrideprotected SimpleDateFormat initialValue() {System.out.println(Thread.currentThread().getName() + ": initializing SimpleDateFormat");return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");}};// 2. A ThreadLocal to hold a per-thread transaction ID (as a String)private static final ThreadLocal<String> transactionIdHolder = new ThreadLocal<>();// 3. A ThreadLocal to hold a per-thread counter (as an Integer)//    Demonstrates initialValue for a simple type.private static final ThreadLocal<Integer> perThreadCounter = new ThreadLocal<Integer>() {@Overrideprotected Integer initialValue() {System.out.println(Thread.currentThread().getName() + ": initializing counter to 0");return 0;}};public static void main(String[] args) throws InterruptedException {ExecutorService executor = Executors.newFixedThreadPool(3); // Pool of 3 threadsSystem.out.println("--- Demo: Sharing a non-thread-safe object (SimpleDateFormat) safely ---");for (int i = 0; i < 5; i++) { // More tasks than threads to show reuseint taskId = i;executor.submit(() -> {String threadName = Thread.currentThread().getName();System.out.println(threadName + ": Task " + taskId + " starting.");// Each thread gets its own SimpleDateFormat instanceSimpleDateFormat sdf = dateFormatHolder.get();String formattedDate = sdf.format(new Date(System.currentTimeMillis() + taskId * 100000));System.out.println(threadName + ": Task " + taskId + " formatted date: " + formattedDate);// No need to call remove() for dateFormatHolder if threads are short-lived OR// if the value is meant to be reused by the same thread across tasks.// However, if SimpleDateFormat had task-specific state, remove() would be crucial.});}// Wait for date formatting tasks to complete before next demo// For simplicity, we'll just wait a bit. In real code, use countdown latches or similar.Thread.sleep(1000);System.out.println("\n--- Demo: Per-thread context (Transaction ID) and the importance of remove() ---");Random random = new Random();for (int i = 0; i < 5; i++) {int taskId = i + 10; // Different task IDsexecutor.submit(() -> {String threadName = Thread.currentThread().getName();String transactionId = "TXN-" + threadName.replace("pool-1-thread-","T") + "-" + taskId + "-" + random.nextInt(100);// Check if there's a leftover ID from a previous task in this threadString previousTxnId = transactionIdHolder.get();if (previousTxnId != null) {System.out.println(threadName + ": Task " + taskId + " WARNING! Found leftover transaction ID: " + previousTxnId + ". THIS IS BAD if not intended.");}transactionIdHolder.set(transactionId);System.out.println(threadName + ": Task " + taskId + " started with Transaction ID: " + transactionIdHolder.get());// Simulate worktry {Thread.sleep(50 + random.nextInt(100));} catch (InterruptedException e) {Thread.currentThread().interrupt();}System.out.println(threadName + ": Task " + taskId + " finished with Transaction ID: " + transactionIdHolder.get());// CRUCIAL: Clean up to prevent data from this task leaking to the next task run by this threadtransactionIdHolder.remove();System.out.println(threadName + ": Task " + taskId + " Transaction ID removed. Current value: " + transactionIdHolder.get());});}Thread.sleep(2000); // Wait for transaction tasksSystem.out.println("\n--- Demo: Per-thread counter with initialValue and remove() ---");for (int i = 0; i < 7; i++) {int taskId = i + 20;executor.submit(() -> {String threadName = Thread.currentThread().getName();System.out.println(threadName + ": Task " + taskId + " starting.");Integer count = perThreadCounter.get(); // Gets initial value (0) if not set, or previous value if set by this threadSystem.out.println(threadName + ": Task " + taskId + " initial counter: " + count);count++;perThreadCounter.set(count);System.out.println(threadName + ": Task " + taskId + " incremented counter: " + perThreadCounter.get());// If we don't remove, the next task on THIS thread will see the incremented value.// Depending on the use case, this might be desired or not.// For this demo, let's assume each task should start fresh or with the default.if (taskId % 2 == 0) { // Let's remove for even task IDs to show the differenceSystem.out.println(threadName + ": Task " + taskId + " (even) REMOVING counter. Next task on this thread will get initialValue.");perThreadCounter.remove();} else {System.out.println(threadName + ": Task " + taskId + " (odd) NOT removing counter. Next task on this thread will see: " + perThreadCounter.get());}});}executor.shutdown();try {if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {executor.shutdownNow();}} catch (InterruptedException e) {executor.shutdownNow();}System.out.println("\nAll tasks completed.");}
}

http://www.xdnf.cn/news/1009459.html

相关文章:

  • 天猫代运营哪个公司比较靠谱
  • 黑马教程强化day2-4
  • Python爬虫实战:快手数据采集与舆情分析
  • AIAgent,Prompt,MCP是什么?
  • Eplan2022更改用户界面颜色
  • SAP会计凭证抬头增强
  • 【学习笔记】H264视频编码
  • python虚拟环境
  • JavaScript 中 apply、call 和 bind 方法的手写实现
  • cf1742D
  • <论文>自注意力序列推荐模型SASRec
  • 负氧离子监测站在景区的作用
  • 详解HarmonyOS NEXT系统中ArkTS和仓颉的混合开发
  • sqlmap 的基本用法
  • 树莓派-ubuntu 24.04开启桌面远程访问
  • MD从入门到荒废-Markdown文件插入多个动态徽章
  • linux驱动开发(6)-内核虚拟空间管理
  • python 在基因研究中的应用,博德研究所:基因编辑
  • JDK各个版本新特性
  • 指针01 day13
  • Python 基础语法 (2)【适合 0 基础】
  • SM4 与 AES 在 GPU 上的性能比较
  • 一分钟了解MCP
  • AES加密
  • Huggingface Transformer 使用指南2-开发自定义模型
  • apdl细节
  • TypeReference指定反序列化获取响应对象
  • 小黑享受思考心流躲避迷茫:92. 反转链表 II
  • 2025年度重点专项项目申报指南的通知公布!
  • ADC(模数转换)