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 成功上市!