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

15.ThreadLocal的作用

1 什么是ThreadLocal

在多线程环境中,如果多个线程同时访问某个变量,如果希望每个线程对共享变量的相关操作仅对自己可见 ,该如何做呢?对应的现实场景就是各有各家的炉灶,相互之间生火做饭互不影响,这就是ThreadLocal要干的事。

ThreadLocal为每个线程提供一个独立的空间,用来存储共享变量的副本,每个副本只会对共享变量的副本进行操作,线程之间互不影响。

我们先看一个例子:

public class ThreadLocalExample {
    public final static ThreadLocal<String> string = ThreadLocal.withInitial(() -> "DEFAULT VALUE");

    public static void main(String[] args) throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + ":INITIAL_VALUE->" + string.get());
        string.set("Main Thread Value");
        System.out.println(Thread.currentThread().getName() + ":BEFORE->" + string.get());
        Thread t1 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + ":T1->" + string.get());
            string.set("T1 Thread Value");
            System.out.println(Thread.currentThread().getName() + ":T1->" + string.get());
        }, "t1");
        Thread t2 = new Thread(() -> {
        //第一个关注点
            System.out.println(Thread.currentThread().getName() + ":T2->" + string.get());
            string.set("T2 Thread Value");
            System.out.println(Thread.currentThread().getName() + ":T2->" + string.get());
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        //第二个关注点
        System.out.println(Thread.currentThread().getName() + ":AFTER->" + string.get());
    }
}

这段代码虽然比较长,但是功能很简单, 首先定义了一个全局变量string,并初始化为"DEFAULT VALUE"。

在main()方法中,主要是main()线程和t1、t2两个子线程,分别获得和修改string变量的值。我们重点关注上面注释标注的两个位置,输出对应的分别是下面的①和②:

main:INITIAL_VALUE->DEFAULT VALUE
main:BEFORE->Main Thread Value
t1:T1->DEFAULT VALUE
t2:T2->DEFAULT VALUE//①第一个关注点输出结果,为什么是DEFAULT VALUE?
t1:T1->T1 Thread Value
t2:T2->T2 Thread Value
main:AFTER->Main Thread Value//②第二个关注点输出结果,为什么是Main Thread Value

我们发现,不同线程通过 string.set()方法设置的值,仅对当前线程可见,各个线程之间不会相互影响,这就是ThreadLocal的作用,它实现了不同线程之间的隔离,从而保证多线程对于共享变量操作的安全性。

2 ThreadLocal的应用

ThreadLocal最经典的应用是在日期方法里,SimpleDateFormat是非线程安全的,例如:

public class SimpleDateFormatExample {
    private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static Date parse(String strDate) throws ParseException {
        return sdf.parse(strDate);
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 9; i++) {
            executorService.execute(() -> {
                try {
                    System.out.println(parse("2022-08-18 16:35:20"));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            });
        }
    }
}

这里构建了一个线程池,通过9次循环让这个线程池去执行一个解析字符串的任务,运行上面的程序,会抛出multiple points的异常,为什么会这样呢?就是因为SimpleDateFormat是非线性安全的,SimpleDateFormat在使用的时候,需要为每个线程单独创建示例,如果有多个线程同时访问,则必须通过外部的同步机制来保护。

SimpleDateFormat继承了DateFormat类,而在DateFormat中定义了两个全局成员变量Calendar和NumberFormat,分别用来进行日期和数字的转化,而DateFormat本身也不是线程安全的。

public class SimpleDateFormat extends DateFormat {
  protected Calendar calendar;
  protected NumberFormat numberFormat;
 }

在SimpleDateFormat类的subParse中 ,会用到numberFormat进行分析操作

if (obeyCount) {
      ...
    number = numberFormat.parse(text.substring(0, start+count), pos);
} else {
    number = numberFormat.parse(text, pos);
}

再看numberFormat.parse的实现类的部分代码:

//DecimalFormat类中
public Number parse(String text, ParsePosition pos) {
 if (!subparse(text, pos, positivePrefix, negativePrefix, digitList, false, status)) {
     return null;
   }
   ...
 if (digitList.fitsIntoLong(status[STATUS_POSITIVE], isParseIntegerOnly())) {
    gotDouble = false;
    longResult = digitList.getLong();
   } else {
      doubleResult = digitList.getDouble();
    }  

注意上面的digitList是一个全局变量。

再看上面的subparse()方法,该方法非常长,我们看几个关键位置:

private final boolean subparse(...){
    ...
    digits.decimalAt = digits.count = 0;
    ....
    backup = -1;
    ....
     if (!sawDecimal) {
         digits.decimalAt = digitCount; // Not digits.count!
    }
    digits.decimalAt += exponent;
}

导致报错的原因就是这里的subparse()方法,对全局变量digits的更新操作没有加锁,不满足原子性。假设ThreadA和B同时进入该方法,就会导致处理冲突。

为了解决该问题,我们可以通过ThreadLocal来保护该值。代码如下:

public class SimpleDateFormatSafetyExample {
    private static final String DATEFORMAT = "yyyy-MM-dd HH:mm:ss";
    private static ThreadLocal<DateFormat> dateFormatThreadLocal = new ThreadLocal<>();

    private static DateFormat getDateFormat() { //每次从threadlocal中获取SimpleDateFormat实例
        DateFormat df = dateFormatThreadLocal.get();
        if (df == null) {
            df = new SimpleDateFormat(DATEFORMAT);
            dateFormatThreadLocal.set(df);
        }
        return df;
    }

    public static Date parse(String strDate) throws ParseException {
        return getDateFormat().parse(strDate);
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 9; i++) {
            executorService.execute(() -> {
                try {
                    System.out.println(parse("2021-06-16 16:35:20"));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            });
        }
    }
}

这里,每个线程通过parse()方法做格式转换时,都可以获得一个完全独立的SimpleDateFormat实例,由于线程不存在对于同一个共享实例的竞争,也就不存在线程安全的问题了。

        

相关文章:

  • Android 11 adb无线调试使用方法
  • 【安卓学习之常见问题】自定义组件-刷新后跳到第一行
  • 为农田解渴为人民群众解忧 国稻种芯-桃源:湖南坚守抗旱一线
  • 【SSM框架】Mybatis详解11(源码自取)之表关联关系
  • docker安装ES(elasticsearch7.4.2)
  • C++—— pass by value and use std::move
  • STM32CubeIDE更新ST LINK驱动失败解决方法
  • PHP cURL 函数
  • 可编程 USB 转串口适配器开发板 SHT3x-DIS 温湿度传感器芯片
  • Git - branch name
  • Vue+element 商品参数业务实现
  • Java项目:SSM博物馆售票管理系统
  • 资料美国二手APX582音频分析仪APX582
  • 流水线:如何做到应用分钟级上线交付?
  • 《算法图解》阅读笔记
  • Computed property XXX was assigned to but it has no setter
  • Git同步原始仓库到Fork仓库中
  • Javascript基础之Array数组API
  • Javascript设计模式学习之Observer(观察者)模式
  • Otto开发初探——微服务依赖管理新利器
  • Python代码面试必读 - Data Structures and Algorithms in Python
  • QQ浏览器x5内核的兼容性问题
  • react-core-image-upload 一款轻量级图片上传裁剪插件
  • RxJS 实现摩斯密码(Morse) 【内附脑图】
  • Spring思维导图,让Spring不再难懂(mvc篇)
  • vue-loader 源码解析系列之 selector
  • VuePress 静态网站生成
  • Webpack 4x 之路 ( 四 )
  • 从零开始的无人驾驶 1
  • 互联网大裁员:Java程序员失工作,焉知不能进ali?
  • 什么是Javascript函数节流?
  • 译有关态射的一切
  • 走向全栈之MongoDB的使用
  • [Shell 脚本] 备份网站文件至OSS服务(纯shell脚本无sdk) ...
  • 宾利慕尚创始人典藏版国内首秀,2025年前实现全系车型电动化 | 2019上海车展 ...
  • 湖北分布式智能数据采集方法有哪些?
  • 蚂蚁金服CTO程立:真正的技术革命才刚刚开始
  • ​香农与信息论三大定律
  • # 学号 2017-2018-20172309 《程序设计与数据结构》实验三报告
  • (1)Map集合 (2)异常机制 (3)File类 (4)I/O流
  • (14)目标检测_SSD训练代码基于pytorch搭建代码
  • (6)添加vue-cookie
  • (java)关于Thread的挂起和恢复
  • (NO.00004)iOS实现打砖块游戏(九):游戏中小球与反弹棒的碰撞
  • (pt可视化)利用torch的make_grid进行张量可视化
  • (第二周)效能测试
  • (附源码)计算机毕业设计ssm高校《大学语文》课程作业在线管理系统
  • (附源码)计算机毕业设计SSM疫情社区管理系统
  • (已更新)关于Visual Studio 2019安装时VS installer无法下载文件,进度条为0,显示网络有问题的解决办法
  • .net oracle 连接超时_Mysql连接数据库异常汇总【必收藏】
  • .net php 通信,flash与asp/php/asp.net通信的方法
  • .NET/C# 的字符串暂存池
  • .NET/C# 中你可以在代码中写多个 Main 函数,然后按需要随时切换
  • .NET/MSBuild 中的发布路径在哪里呢?如何在扩展编译的时候修改发布路径中的文件呢?
  • .Net+SQL Server企业应用性能优化笔记4——精确查找瓶颈