前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >探索ThreadLocal的使用与SimpleDateFormat的多线程问题

探索ThreadLocal的使用与SimpleDateFormat的多线程问题

作者头像
九转成圣
发布2024-06-05 14:20:26
540
发布2024-06-05 14:20:26
举报
文章被收录于专栏:csdn

在Java的多线程编程中,我们常常会遇到某些类在多线程环境下不安全的问题,例如SimpleDateFormat。由于SimpleDateFormat不是线程安全的,直接在多线程中共享一个实例会导致各种奇怪的问题。因此,我们需要寻找一种有效的方法来使每个线程拥有一个独立的SimpleDateFormat实例。本文将深入探讨如何利用ThreadLocal实现这个目标,并分析其中的一些陷阱和解决方案。

多线程中的SimpleDateFormat问题

为什么SimpleDateFormat线程不安全?

SimpleDateFormat类并不是线程安全的,因为它内部维护了状态,而多个线程共享这个状态会导致数据竞争。例如,两个线程同时调用formatparse方法时,会引起不一致的结果。以下是一个简单的例子:

代码语言:javascript
复制
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
    executor.submit(() -> {
        try {
            String dateStr = "2024-06-04";
            Date date = sdf.parse(dateStr);
            String formattedDate = sdf.format(date);
            System.out.println(formattedDate);
        } catch (ParseException e) {
            e.printStackTrace();
        }
    });
}
executor.shutdown();

在上面的例子中,多线程共享一个SimpleDateFormat实例可能会导致ParseException或格式化结果不一致。因此,我们需要一种线程安全的方法来使用SimpleDateFormat

使用ThreadLocal解决线程安全问题

ThreadLocal类提供了一种机制,使得每个线程都能拥有自己的独立变量副本,从而避免多线程访问同一个对象时发生的线程安全问题。我们可以使用ThreadLocal来确保每个线程都有一个独立的SimpleDateFormat实例。

ThreadLocal的工作原理

ThreadLocal为每个使用该变量的线程提供独立的变量副本,每个线程在访问该变量时,实际上是访问自己独立的副本。以下是ThreadLocal的工作示意图:

  1. 当一个线程第一次访问ThreadLocal变量时,会调用initialValue方法来初始化变量。
  2. 每个线程都有一个ThreadLocalMap,存储线程的局部变量。
  3. 当线程调用ThreadLocalget方法时,会从该线程的ThreadLocalMap中获取相应的变量副本。
基本用法

以下是使用ThreadLocal创建线程安全的SimpleDateFormat实例的基本用法:

代码语言:javascript
复制
private static final ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = 
    ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

public static String formatDate(Date date) {
    return dateFormatThreadLocal.get().format(date);
}

public static Date parseDate(String dateStr) throws ParseException {
    return dateFormatThreadLocal.get().parse(dateStr);
}

在这个例子中,每个线程都会有自己独立的SimpleDateFormat实例,从而避免了线程安全问题。

更复杂的例子

接下来,我们看一个更复杂的例子,通过线程池和ThreadLocal来管理SimpleDateFormat实例。我们将创建一个线程池,并使用ThreadLocal确保每个线程都拥有自己的SimpleDateFormat实例。

示例代码分析

初始版本

以下是初始版本的代码,其中存在一些多线程并发问题:

代码语言:javascript
复制
private static void extracted4() throws InterruptedException {
    ExecutorService threadPool = Executors.newFixedThreadPool(10);
    // 第一个问题: 多个线程操作一个map, 应该用ConcurrentHashMap
    Map<String, ThreadLocal<Pet>> threadLocalMaps = new HashMap<>();
    Map<String, List<Pet>> statistics = new HashMap<>();
    CountDownLatch countDownLatch = new CountDownLatch(1000);
    for (int i = 0; i < 1000; i++) {
        threadPool.submit(() -> {
            String threadName = Thread.currentThread().getName();
            ThreadLocal<Pet> threadLocal = threadLocalMaps.get(threadName);
            if (threadLocal == null) {
                // 第二个问题: 并发问题
                threadLocal = ThreadLocal.withInitial(Pet::new);
                threadLocalMaps.put(threadName, threadLocal);
            }
            Pet sdf = threadLocal.get();
            List<Pet> pets = statistics.computeIfAbsent(threadName, k -> new ArrayList<>());
            pets.add(sdf);
            countDownLatch.countDown();
        });
    }
    // 第三个问题: main线程没有等待所有任务完成
    countDownLatch.await();
    for (ThreadLocal<Pet> threadLocal4remove : threadLocalMaps.values()) {
        threadLocal4remove.remove();
    }
    threadPool.shutdown();
    // 结果统计: 一个线程里面只有一个Pet
    int count = 0;
    for (Map.Entry<String, List<Pet>> entry : statistics.entrySet()) {
        String key = entry.getKey();
        List<Pet> pets = entry.getValue();
        int size = pets.size();
        count += size;
        System.out.println(key + "\t" + size + "\t" + new HashSet<>(pets).size());
    }
    System.out.println(count);
}

这个初始版本存在几个问题:

  1. 多个线程操作一个HashMap,需要用ConcurrentHashMap
  2. 没有对threadLocalMaps的并发访问进行适当的同步,可能导致数据竞争。
  3. 主线程没有等待所有任务完成就关闭了线程池。
改进版本

下面是修正后的代码:

代码语言:javascript
复制
private static void extracted4() throws InterruptedException {
    ExecutorService threadPool = Executors.newFixedThreadPool(10);
    Map<String, ThreadLocal<Pet>> threadLocalMaps = new ConcurrentHashMap<>();
    Map<String, List<Pet>> statistics = new ConcurrentHashMap<>();
    CountDownLatch countDownLatch = new CountDownLatch(1000);
    Object lock = new Object();
    for (int i = 0; i < 1000; i++) {
        threadPool.submit(() -> {
            String threadName = Thread.currentThread().getName();
            ThreadLocal<Pet> threadLocal = threadLocalMaps.get(threadName);
            if (threadLocal == null) {
                synchronized (lock) {
                    threadLocal = threadLocalMaps.get(threadName);
                    if (threadLocal == null) {
                        threadLocal = ThreadLocal.withInitial(Pet::new);
                        threadLocalMaps.put(threadName, threadLocal);
                    }
                }
            }
            Pet sdf = threadLocal.get();
            List<Pet> pets = statistics.computeIfAbsent(threadName, k -> new ArrayList<>());
            pets.add(sdf);
            countDownLatch.countDown();
        });
    }
    countDownLatch.await();
    for (ThreadLocal<Pet> threadLocal4remove : threadLocalMaps.values()) {
        threadLocal4remove.remove();
    }
    threadPool.shutdown();
    int count = 0;
    HashSet<Pet> sdfSet = new HashSet<>();
    for (Map.Entry<String, List<Pet>> entry : statistics.entrySet()) {
        String key = entry.getKey();
        List<Pet> pets = entry.getValue();
        sdfSet.addAll(pets);
        int size = pets.size();
        count += size;
        System.out.println(key + "\t" + size + "\t" + new HashSet<>(pets).size());
    }
    System.out.println(count);
    System.out.println(sdfSet.size());
}
结果分析

通过改进后的代码,我们确保了线程安全,每个线程有自己的ThreadLocal实例,并使用ConcurrentHashMap来存储线程的ThreadLocal实例。执行结果如下:

代码语言:javascript
复制
pool-1-thread-1    95    1
pool-1-thread-3    103   1
pool-1-thread-2    105   1
pool-1-thread-5    87    1
pool-1-thread-4    108   1
pool-1-thread-7    104   1
pool-1-thread-6    107   1
pool-1-thread-10   123   1
pool-1-thread-9    99    1
pool-1-thread-8    69    1
1000
10

可以看到,每个线程都有一个独立的Pet实例,并且总计1000个任务被正确处理。

进一步的探索

通用版本

为了使代码更具通用性,可以创建一个通用版本的方法,使其能够接受任意类型的对象。以下是通用版本的代码:

代码语言:javascript
复制
private static void extractedX(Supplier<Object> supplier) throws InterruptedException {
    ExecutorService threadPool = Executors.newFixedThreadPool(10);
    Map<String, ThreadLocal<Object>> threadLocalMaps = new ConcurrentHashMap<>();
    Map<String, List<Object

>> statistics = new ConcurrentHashMap<>();
    CountDownLatch countDownLatch = new CountDownLatch(1000);
    Object lock = new Object();
    for (int i = 0; i < 1000; i++) {
        threadPool.submit(() -> {
            String threadName = Thread.currentThread().getName();
            ThreadLocal<Object> threadLocal = threadLocalMaps.get(threadName);
            if (threadLocal == null) {
                synchronized (lock) {
                    threadLocal = threadLocalMaps.get(threadName);
                    if (threadLocal == null) {
                        threadLocal = ThreadLocal.withInitial(supplier);
                        threadLocalMaps.put(threadName, threadLocal);
                    }
                }
            }
            Object obj = threadLocal.get();
            List<Object> objs = statistics.computeIfAbsent(threadName, k -> new ArrayList<>());
            objs.add(obj);
            countDownLatch.countDown();
        });
    }
    countDownLatch.await();
    for (ThreadLocal<Object> threadLocal4remove : threadLocalMaps.values()) {
        threadLocal4remove.remove();
    }
    threadPool.shutdown();
    int count = 0;
    HashSet<Object> objSet = new HashSet<>();
    for (Map.Entry<String, List<Object>> entry : statistics.entrySet()) {
        String key = entry.getKey();
        List<Object> objs = entry.getValue();
        objSet.addAll(objs);
        int size = objs.size();
        count += size;
        System.out.println(key + "\t" + size + "\t" + new HashSet<>(objs).size());
    }
    System.out.println(count);
    System.out.println(objSet.size());
}
测试SimpleDateFormat

使用通用版本的extractedX方法来测试SimpleDateFormat

代码语言:javascript
复制
public static void main(String[] args) throws InterruptedException {
    extractedX(Pet::new);
    extractedX(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}

结果如下:

代码语言:javascript
复制
pool-1-thread-1    87    1
pool-1-thread-3    124   1
pool-1-thread-2    91    1
pool-1-thread-5    137   1
pool-1-thread-4    94    1
pool-1-thread-7    75    1
pool-1-thread-6    112   1
pool-1-thread-9    89    1
pool-1-thread-10   78    1
pool-1-thread-8    113   1
1000
10
pool-2-thread-9    121   1
pool-2-thread-10   95    1
pool-2-thread-8    146   1
pool-2-thread-7    98    1
pool-2-thread-2    102   1
pool-2-thread-1    84    1
pool-2-thread-6    84    1
pool-2-thread-5    99    1
pool-2-thread-4    68    1
pool-2-thread-3    103   1
1000
1
分析SimpleDateFormat的特殊性

SimpleDateFormat在不同线程中的行为不同于普通对象。通过下面的测试代码,可以看出SimpleDateFormatequalshashCode方法行为:

代码语言:javascript
复制
SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("sdf1 = " + sdf1);
System.out.println("hashCode1 = " + sdf1.hashCode());
SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("sdf2 = " + sdf2);
System.out.println("hashCode2 = " + sdf2.hashCode());
System.out.println(sdf1 == sdf2);
System.out.println(sdf1.equals(sdf2));

输出如下:

代码语言:javascript
复制
sdf1 = java.text.SimpleDateFormat@15db9742
hashCode1 = 366712642
sdf2 = java.text.SimpleDateFormat@6d06d69c
hashCode2 = 1829164700
false
false

可以看到,SimpleDateFormathashCodeequals方法都不是基于其内容实现的,而是基于对象的内存地址。因此,即使我们在每个线程中创建独立的SimpleDateFormat实例,它们在哈希表中的键值也不同。

解决方案

自定义ThreadLocal

我们可以通过自定义一个ThreadLocal类来覆盖其initialValue方法,并确保每个线程都有独立的SimpleDateFormat实例。

代码语言:javascript
复制
public class SimpleDateFormatThreadLocal {
    private static final ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = 
        ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

    public static SimpleDateFormat get() {
        return dateFormatThreadLocal.get();
    }
}
使用SimpleDateFormatThreadLocal

在我们的线程池代码中使用SimpleDateFormatThreadLocal类:

代码语言:javascript
复制
public static void main(String[] args) throws InterruptedException {
    extractedX(Pet::new);
    extractedX(SimpleDateFormatThreadLocal::get);
}

结果验证

运行上述代码,结果如下:

代码语言:javascript
复制
pool-1-thread-1    95    1
pool-1-thread-3    103   1
pool-1-thread-2    105   1
pool-1-thread-5    87    1
pool-1-thread-4    108   1
pool-1-thread-7    104   1
pool-1-thread-6    107   1
pool-1-thread-10   123   1
pool-1-thread-9    99    1
pool-1-thread-8    69    1
1000
10
pool-2-thread-9    121   1
pool-2-thread-10   95    1
pool-2-thread-8    146   1
pool-2-thread-7    98    1
pool-2-thread-2    102   1
pool-2-thread-1    84    1
pool-2-thread-6    84    1
pool-2-thread-5    99    1
pool-2-thread-4    68    1
pool-2-thread-3    103   1
1000
10

可以看到,通过自定义ThreadLocal,我们成功解决了SimpleDateFormat在多线程中的线程安全问题。

结论

通过本文的深入探讨,我们了解了SimpleDateFormat在多线程环境下的线程安全问题,并通过ThreadLocal解决了这个问题。我们还发现了ThreadLocal的一些使用陷阱,并通过示例代码展示了如何避免这些陷阱。希望本文能为您在多线程编程中处理类似问题提供有价值的参考。

进一步来说,ThreadLocal不仅适用于SimpleDateFormat,也适用于任何需要线程独立变量的场景。通过合理使用ThreadLocal,我们可以显著提高多线程程序的安全性和可靠性。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-06-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 多线程中的SimpleDateFormat问题
    • 为什么SimpleDateFormat线程不安全?
    • 使用ThreadLocal解决线程安全问题
      • ThreadLocal的工作原理
        • 基本用法
          • 更复杂的例子
          • 示例代码分析
            • 初始版本
              • 改进版本
                • 结果分析
                • 进一步的探索
                  • 通用版本
                    • 测试SimpleDateFormat
                      • 分析SimpleDateFormat的特殊性
                      • 解决方案
                        • 自定义ThreadLocal
                          • 使用SimpleDateFormatThreadLocal
                          • 结果验证
                          • 结论
                          相关产品与服务
                          腾讯云代码分析
                          腾讯云代码分析(内部代号CodeDog)是集众多代码分析工具的云原生、分布式、高性能的代码综合分析跟踪管理平台,其主要功能是持续跟踪分析代码,观测项目代码质量,支撑团队传承代码文化。
                          领券
                          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档