15.ThreadLocal的作用

Posted 纵横千里,捭阖四方

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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实例,由于线程不存在对于同一个共享实例的竞争,也就不存在线程安全的问题了。

        

以上是关于15.ThreadLocal的作用的主要内容,如果未能解决你的问题,请参考以下文章

13 年,MySQL 之父赌赢了:另起炉灶的 MariaDB 成功上市!

13 年,MySQL 之父赌赢了:另起炉灶的 MariaDB 成功上市!

13 年,MySQL 之父赌赢了:另起炉灶的 MariaDB 成功上市!

FCM的Firebase功能不起作用。提供给sendToDevice()的注册令牌必须是非空字符串或非空数组

GRANT命令的作用及其工作原理

二元logistic回归是非线性回归吗