ThreadLocal 介绍:
变量值的共享可以使用public static变量的形式使用,所有的线程都可以使用同一个public static变量,那么有没有一个东西能够使每一个线程都拥有自己的变量呢?ThreadLocal就是解决这样的问题的。
ThreadLocal 类提供线程局部变量,它通常是私有类中希望将状态与线程关联的静态字段。
简而言之,就是 ThreadLocal 提供了线程间数据隔离的功能,从它的命名上也能知道这是属于一个线程的本地变量。也就是说,每个线程都会在 ThreadLocal 中保存一份该线程独有的数据,所以它是线程安全的。
我们可以通过一个简单的例子来看看threadlocal的作用和使用方法
public class Demo1 {
public static void main(String[] args) {
ThreadLocal threadLocal=new ThreadLocal();
new Thread(()->{
for (int i = 1; i <= 3; i++) {
threadLocal.set("A:"+i);
System.out.println("A get"+threadLocal.get());
}
},"A").start();
new Thread(()->{
for (int i = 1; i <= 3; i++) {
threadLocal.set("B:"+i);
System.out.println("B get"+threadLocal.get());
}
},"B").start();
for (int i = 1; i <= 3; i++) {
threadLocal.set("main:"+i);
System.out.println("main get"+threadLocal.get());
}
}
}
测试结果:
B getB:1
A getA:1
A getA:2
A getA:3
B getB:2
B getB:3
main getmain:1
main getmain:2
main getmain:3
从运行结果可以看出不同的线程通过threadlocal可以向每个线程储存自己的私有数据。
ThreadLocal原理
threadlocal会将当前数据放入当前线程对象的Map中这个Map是Thread类的实例变量。threadlocal自己不管理,不储存任何数据,他只是数据和Map之间的一个桥梁,用于将数据放入Map中,执行流程如下图:
执行后每个线程的Map中存有自己的数据,map中的key储存的是Threadlocal对象value就是储存的值。每个thread中的map只对当前线程可见,其他线程不可以访问该map中的值。当前线程销毁了,map也随之销毁,当前map中的数据如果没有被引用或者没有被使用则会被GC回收。
ThreadLocal存取数据流程分析
下面我们结合上图和UML类图和jdk源码来分析一下ThreadLocal类执行存取数据的操作流程。
1) 执行threadLocal.set(“A:”+i);其源代码如下
public void set(T value) {
Thread t = Thread.currentThread();//main线程
ThreadLocalMap map = getMap(t);//从main线程中获得threadlocalmap对象
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);//第一次调用set方法时调用执行
}
}
2)ThreadLocalMap map = getMap(t);其源码如下
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
//返回main线程中threadlocals变量对应的threadlocalMap对象
}
该源代码在Thread类中
ThreadLocal.ThreadLocalMap threadLocals = null;
//threadlocals数据类型是ThreadLocal.ThreadLocalMap
3)取得thread中的ThreadLocal.ThreadLocalMap后,第一次向其中存放数据会调用createMap(t, value)方法来创建ThreadLocal.ThreadLocalMap对象,默认值是null创建时key就是当前的ThreadLocal对象值就是传入的value。
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
参数this是当前的threadlocal对象,firstValue就是调用Threadlocal对象set方法传入的参数值。
4) new ThreadLocalMap(this, firstValue)构造方法源码如下:
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
从源代码可以看出threadlocal对象和firstValue被封装进Entry对象并放入table[]数组中。
5)table[]数组的源代码如下:
ThreadLocalMap类中table是一个Entry[]数组类型。
Entry类的源码如下:该类就是用来存放threadlocal对象和value值
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
经过上面几个步骤成功将value通过ThreadLocal放入当前线程中的ThreadLocalMap对象中。
下面我们来看看get()方法的执行过程,其源码如下:
public T get() {
Thread t = Thread.currentThread();//t就是main线程
ThreadLocalMap map = getMap(t);//从main线程中获得map
if (map != null) {
//以this为key获得对应的Entry对象
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;//从Entry对象中获得value值并返回
return result;
}
}
return setInitialValue();
}
看完源代码后你可能会有一个疑问为什么不直接向Thread类中的ThreaLocalMap中直接存取数据?其原因是因为
ThreadLocal.ThreadLocalMap threadLocals = null; 变量threadLocals
是包级访问权限,所以不能直接从外部访问该变量只有通过同包中的类来访问,而恰好ThreadLocal和Thread在同一个包中。
重写initialValue()方法实现隔离性
在第一次调用get方法返回值是null可以通过重写initialValue()方法实现返回默认值。
public class Demo2 {
public static void main(String[] args) throws InterruptedException{
new Thread(()->{
for (int i = 1; i <= 3; i++) {
System.out.println("B get"+Tools.getThreadLocal().get());
}
},"B").start();
TimeUnit.SECONDS.sleep(2);
for (int i = 1; i <= 3; i++) {
System.out.println("main get"+Tools.getThreadLocal().get());
}
}
}
class Tools extends ThreadLocal{
@Override
protected Object initialValue() {
return new Date().getTime();
}
public static Tools getThreadLocal(){
return new Tools();
}
}
测试结果:
B get1619784730996
B get1619784731026
B get1619784731026
main get1619784732996
main get1619784732996
main get1619784732996
InheritableThreadLocal类的使用
类ThreadLocal不能实现值继承
public class Demo3 {
public static void main(String[] args) throws InterruptedException{
ThreadLocal threadLocal = new ThreadLocal();
for (int i = 1; i <= 3; i++) {
threadLocal.set("main set value:"+i);
System.out.println("main get"+threadLocal.get());
}
TimeUnit.SECONDS.sleep(2);
new Thread(()->{
for (int i = 1; i <= 3; i++) {
System.out.println("A get:"+threadLocal.get());
}
},"A").start();
}
}
测试结果:
main getmain set value:1
main getmain set value:2
main getmain set value:3
A get:null
A get:null
A get:null
main线程创建了A线程所以main线程是A线程的父线程。从运行结果来看可以发现main线程中的值并没有继承给A线程,所以ThreadLocal并没有值继承这一特性。
使用InheritableThreadLocal类实现值继承
public class Demo4 {
public static void main(String[] args) throws InterruptedException{
InheritableThreadLocal threadLocal = new InheritableThreadLocal();
for (int i = 1; i <= 3; i++) {
threadLocal.set("main set value");
System.out.println("main get"+threadLocal.get());
}
TimeUnit.SECONDS.sleep(2);
new Thread(()->{
for (int i = 1; i <= 3; i++) {
System.out.println("A get:"+threadLocal.get());
}
},"A").start();
}
}
测试结果:
main getmain set value
main getmain set value
main getmain set value
A get:main set value
A get:main set value
A get:main set value
InheritableThreadLocal存取数据流程分析
使用InheritableThreadLocal类确实可以实现值继承特性下面我们来分析一下在jdk中的源码:
1)InheritableThreadLocal类的源代码如下
这三个方法都是对父类的方法进行重写。
2)执行threadLocal.set(“main set value”)方法其实就是调用父类的set方法,因为InheritableThreadLocal类并没有重写set方法。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
不过getMap和createMap方法都被重写了所以调用的是InheritableThreadLocal类的方法。
3)getMap方法源码如下
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
可以发现InheritableThreadLocal类是向ThreadLocal.ThreadLocalMap inheritableThreadLocals存放数据。
4)子线程如何从父线程中继承到值的呢?源代码如下
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for (Entry e : parentTable) {
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
ThreadLocal应用场景
1)解决SimpleDateFormat多线程下异常问题
正常多线程情况不使用ThreadLocal
public class Demo5 {
public static void main(String[] args) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
String[] dates = new String[]{"2021-01-01", "2021-04-01", "2021-02-01", "2021-03-01", "2021-01-11"};
MyThread[] threads = new MyThread[10];
for (int i = 0; i < 5; i++) {
threads[i] = new MyThread(simpleDateFormat, dates[i]);
threads[i].start();
}
}
}
class MyThread extends Thread{
private SimpleDateFormat simpleDateFormat;
private String date;
public MyThread(SimpleDateFormat simpleDateFormat, String date) {
this.simpleDateFormat = simpleDateFormat;
this.date = date;
}
@Override
public void run() {
try {
Date time = simpleDateFormat.parse(date);
String newTime = simpleDateFormat.format(time).toString();
if (!newTime.equals(time)) {
System.out.println("threadName:"+this.getName()
+"出错了 日期字符串"+time+"转换后的日期:"+newTime);
}
} catch (ParseException e) {
e.printStackTrace();
}
}
}
测试结果:
Exception in thread "Thread-4" java.lang.NumberFormatException: For input string: "E.21204"
at java.base/jdk.internal.math.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2054)
at java.base/jdk.internal.math.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
threadName:Thread-1出错了 日期字符串Wed Jan 01 00:00:00 CST 2200转换后的日期:2200-01-01
threadName:Thread-0出错了 日期字符串Wed Jan 01 00:00:00 CST 2200转换后的日期:2200-01-01
threadName:Thread-3出错了 日期字符串Mon Mar 01 00:00:00 CST 2021转换后的日期:2200-01-01
threadName:Thread-2出错了 日期字符串Wed Mar 01 00:00:00 CST 2209转换后的日期:2209-03-01
日期转化错误。
下面通过使用ThreadLocal来处理异常,添加工具类
public class Demo5 {
public static void main(String[] args) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
String[] dates = new String[]{"2021-01-01", "2021-04-01", "2021-02-01", "2021-03-01", "2021-01-11"};
MyThread[] threads = new MyThread[10];
for (int i = 0; i < 5; i++) {
threads[i] = new MyThread(simpleDateFormat, dates[i]);
threads[i].start();
}
}
}
class MyThread extends Thread{
private SimpleDateFormat simpleDateFormat;
private String date;
public MyThread(SimpleDateFormat simpleDateFormat, String date) {
this.simpleDateFormat = simpleDateFormat;
this.date = date;
}
@Override
public void run() {
try {
Date time = DateTools.getSimpleDateFormat("yyyy-MM-dd").parse(date);
String newTime = DateTools.getSimpleDateFormat("yyyy-MM-dd").format(time).toString();
if (!newTime.equals(time)) {
System.out.println("threadName:"+this.getName()
+"原来日期字符串"+time+"转换后的日期:"+newTime);
}
} catch (ParseException e) {
e.printStackTrace();
}
}
}
class DateTools{
private static ThreadLocal<SimpleDateFormat> threadLocal=new ThreadLocal<>();
public static SimpleDateFormat getSimpleDateFormat(String datePattern){
SimpleDateFormat simpleDateFormat=null;
simpleDateFormat=threadLocal.get();
if (simpleDateFormat == null) {
simpleDateFormat=new SimpleDateFormat(datePattern);
threadLocal.set(simpleDateFormat);
}
return simpleDateFormat;
}
}
测试结果:
threadName:Thread-1原来日期字符串Thu Apr 01 00:00:00 CST 2021转换后的日期:2021-04-01
threadName:Thread-3原来日期字符串Mon Mar 01 00:00:00 CST 2021转换后的日期:2021-03-01
threadName:Thread-2原来日期字符串Mon Feb 01 00:00:00 CST 2021转换后的日期:2021-02-01
threadName:Thread-0原来日期字符串Fri Jan 01 00:00:00 CST 2021转换后的日期:2021-01-01
threadName:Thread-4原来日期字符串Mon Jan 11 00:00:00 CST 2021转换后的日期:2021-01-11
结果正常。
2)Spring实现事务隔离级别
Spring采用Threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。
Spring框架里面就是用的ThreadLocal来实现这种隔离,主要是在TransactionSynchronizationManager这个类里面,代码如下所示:
private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class);
private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<>("Transactional resources");
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
new NamedThreadLocal<>("Transaction synchronizations");
private static final ThreadLocal<String> currentTransactionName =
new NamedThreadLocal<>("Current transaction name");