多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。
官方注释:
该类提供线程局部变量。这些变量不同于它们的普通副本,因为每个访问一个变量的线程(通过其(ecode get}或(ecode set}方法)都有自己的、独立初始化的变量副本。 ThreadLocal实例通常是希望将状态与线程(例如,用户ID或事务ID)关联的类中的私有静态字段
简单来说:
ThreadLocal为每个线程提供了一个变量副本。每个线程可以使用并修改这个副本,相互之间并不影响。
需要注意: 无论是官方还个人,都建议你使用 private static 来修饰 ThreaLocal 变量
1. 如下代码,注释比较清晰,不再赘述。
/**
* @Data: 2019/11/6
* @Des: ThreadLocal 使用Demo
* 执行结果:
* t1获取到了threadLocal 中的值 : 100
* t1修改到了threadLocal 中的值 : 999
* t2获取到了threadLocal 中的值 : 100
*
*/
public class ThreadLocalDemo {
public static void main(String[] args) {
// 0. 设置ThreadLoca 初始值为100
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 100);
// 1. 创建线程t1
Thread t1 = new Thread(() -> {
String threadName = Thread.currentThread().getName();
// 1.1 获取 ThreadLocal 中的初始值并打印
System.out.println(threadName + "获取到了threadLocal 中的值 : " + threadLocal.get());
// 1.2 重新赋值为999
threadLocal.set(999);
// 1.3 打印赋值后的结果。
System.out.println(threadName + "修改到了threadLocal 中的值 : " + threadLocal.get());
}, "t1");
// 2. 创建线程t2
Thread t2 = new Thread(() -> {
try {
// 1.1 睡眠1s是为了让线程1跑完,设值结束,更好论证
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
String threadName = Thread.currentThread().getName();
// 2.2 打印出当前在线程2中ThreadLocal中的值
System.out.println(threadName + "获取到了threadLocal 中的值 : " + threadLocal.get());
}, "t2");
// 3. 启动两个线程
t1.start();
t2.start();
}
}
可以看到,当线程t1获取到ThreadLocal 中变量值时是100,随机将值修改了999,然后t1再次获取变量值变成了999。证明t1线程的修改是成功的。然后t2线程获取到 ThreadLocal 中的值,还为100。即可证明,ThreadLocal 中的变量是以线程为作用域。
2. 下面的代码可以更加详细的证明这点,代码比较清晰,就不做解释。
/**
* @Data: 2019/11/6
* @Des: ThreadLocal 使用Demo
* 执行结果
* t1获取到了threadLocal 中的值 : 100
* t1修改到了threadLocal 中的值 : 999
* t2获取到了threadLocal 中的值 : 100
* t2又获取到了threadLocal 中的值 : 666
* t1获取到了threadLocal 中的值 : 999
*/
public class ThreadLocalDemo {
public static void main(String[] args) {
// 0. 设置ThreadLoca 初始值为100
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 100);
// 1. 创建线程t1
Thread t1 = new Thread(() -> {
String threadName = Thread.currentThread().getName();
// 1.1 获取 ThreadLocal 中的初始值并打印
System.out.println(threadName + "获取到了threadLocal 中的值 : " + threadLocal.get());
// 1.2 重新赋值为999
threadLocal.set(999);
// 1.3 打印赋值后的结果。
System.out.println(threadName + "修改到了threadLocal 中的值 : " + threadLocal.get());
try {
// 1.4 睡眠2s是为了让线程2跑完,设值结束,更好论证
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 1.5 获取变量值
System.out.println(threadName + "获取到了threadLocal 中的值 : " + threadLocal.get());
}, "t1");
// 2. 创建线程t2
Thread t2 = new Thread(() -> {
try {
// 1.1 睡眠1s是为了让线程1跑完,设值结束,更好论证
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
String threadName = Thread.currentThread().getName();
// 2.2 打印出当前在线程2中ThreadLocal中的值
System.out.println(threadName + "获取到了threadLocal 中的值 : " + threadLocal.get());
// 2.3 设置变量值
threadLocal.set(666);
// 2.4 获取变量值
System.out.println(threadName + "又获取到了threadLocal 中的值 : " + threadLocal.get());
}, "t2");
// 3. 启动两个线程
t1.start();
t2.start();
}
}
ThreadLocal 的 原理其实很简单,关键的就是get和set方法。
首先我们需要知道Thread 类中是有一个 threadLocals
对象,他的类型是 ThreadLocal.ThreadLocalMap
,
与此线程相关的ThreadLocal
值。这个映射由ThreadLocal
类维护。简单来说, Thread类中有一个ThreadLocal.ThreadLocalMap
,它是 ThreadLocal
的一个静态内部类,是一个定制的散列映射(为什么说是定制呢,因为他没有继承或者实现任何Map接口或者其子类),只适用于维护线程本地值,即维护线程独享的本地变量。
Thread 类中, threadLocals 默认为 null
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
我们从 ThreadLocal 的get方法来分析。get方法的代码如下,看起来很少,其实真的很少。
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程的 ThreadLocalMap
ThreadLocalMap map = getMap(t);
// 如果map不为空, 则从线程的ThreadLocalMap 中获取变量
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
....
// 获取线程中的 ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
...
// 给 ThreadLocalMap 设置初始值
private T setInitialValue() {
// 返回我们一开始给ThreadLocal设置的初始值
T value = initialValue();
// 获取当前线程
Thread t = Thread.currentThread();
// 获取线程中的 ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else // 为空则创建一个 ThreadLocalMap。并将value添加到map中,key是当前线程,value是初始值。
createMap(t, value);
return value;
}
...
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
代码注释比较详细,不做过多介绍。
从上面可以看到,当我们调用 get
方法时,ThreadLocal
首先判断当前线程 ThreadLocalMap
是否为空,不为空则从 ThreadLocalMap
获取(由于ThreadLocalMap
是线程私有的,所以各个线程使用各自的ThreadLocalMap
并不会产生冲突)。若为空,则调用 setInitialValued
方法为 ThreadLocalMap
设置初始值。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocal#set(T value) 方法更加简单。和上面的 setInitialValue
方法基本相同,也不做过多介绍。
即,当调用set方法时,会先判断当前线程的ThreadLocalMap 是否为空,不为空,则直接放入,为空则创建一个map,再将值放入map中。
假设有线程A,B。有ThreadLocal L,初始值 i = 100;
首先需要了解Java中对象的四种引用方式,这里不是本文重点,所以就简单说一下。
而在 ThreadLocal.ThreadLocalMap
中,内部类 Entry
继承了 WeakReference
弱引用。而引用的类型是ThreadLocal。这就表明ThreadLocal.ThreadLocalMap.Entry
中的key 是弱引用的,而value是强引用。所以当key被回收时,value还有强引用存在,则value无法回收,会造成内存泄漏。在最新的ThreadLocal中已经做出了修改,即在调用set、get、remove方法时,会清除key为null的Entry,但是如果不调用这些方法,仍然还是会出现内存泄漏 :),所以要养成用完ThreadLocal对象之后及时remove的习惯。
这部分内容是在某一天某一时某一件事引起的某一个想法从而进行某一项验证发现的这一项总结。
话说那一天,那是鞭炮齐鸣,锣鼓喧闹,那家伙是人山人海。。。
事出有因,话不多说。直接看代码
由于包装类型会自动拆箱装箱,所以这里定义了一个Target类,里面只有一个name属性
public class Target {
private String name;
public Target(String name) {
this.name = name;
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
看下面代码, 代码很简单,就启动了两个线程,猜猜输出什么?
public class ThreadLocalDemo {
public static void main(String[] args) throws InterruptedException {
ThreadLocal<Target> threadLocal = ThreadLocal.withInitial(() -> new Target("张三"));
System.out.println(Thread.currentThread().getName() + " ## " + threadLocal.get());
System.out.println(Thread.currentThread().getName() + " ## " + threadLocal.get().getName());
Thread t1 = new Thread(() -> {
threadLocal.get().setName("李四");
System.out.println(Thread.currentThread().getName() + " ## " + threadLocal.get());
System.out.println(Thread.currentThread().getName() + " ## " + threadLocal.get().getName());
}, "t1");
Thread t2 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " ## " + threadLocal.get());
System.out.println(Thread.currentThread().getName() + " ## " + threadLocal.get().getName());
}, "t2");
t1.start();
Thread.sleep(1000); // 睡眠1s是为了让t1执行完再执行t2
t2.start();
}
输入结果如下 :
上面这个结果很简单。可以看到 main线程、t1线程、t2线程所保存的 Target对象并不是同一个(至于为什么不是同一个,下面会讲解)。这时候可以正常使用,在t1线程中修改Target并不影响 t2线程。这符合我们对ThreadLocal 的理解。
那么,下面就是见证奇迹的时刻 。如果我把代码改成下面的样子,就是在ThreadLocal 初始化时值的时候由 匿名类的方式 变成了传递 引用,那么这时候的执行结果是什么?。
public class ThreadLocalDemo {
public static void main(String[] args) throws InterruptedException {
Target target = new Target("张三");
ThreadLocal<Target> threadLocal = ThreadLocal.withInitial(() -> target); // 赋值方式改变
System.out.println(Thread.currentThread().getName() + " ## " + threadLocal.get());
System.out.println(Thread.currentThread().getName() + " ## " + threadLocal.get().getName());
Thread t1 = new Thread(() -> {
threadLocal.get().setName("李四");
System.out.println(Thread.currentThread().getName() + " ## " + threadLocal.get());
System.out.println(Thread.currentThread().getName() + " ## " + threadLocal.get().getName());
}, "t1");
Thread t2 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " ## " + threadLocal.get());
System.out.println(Thread.currentThread().getName() + " ## " + threadLocal.get().getName());
}, "t2");
t1.start();
Thread.sleep(1000);
t2.start();
}
输出结果如下:
有没有发现出乎我们的意料,这里的三个线程 main、t1、t2保存的都是同一个对象,并且我在t1线程中的修改影响到了t2线程的内容。
是不是吓一跳(反正我是吓一跳),这样还怎么使用ThreadLocal。
threadLocal.get()
。这时候追溯源码进入get方法。根据上面的分析, t1线程进来时 ThreadLocalMap
为null,所以会调用 setInitialValue
方法 public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
ThreadLocal
类并没有具体实现,仅仅是返回null。正所谓常言道:事有反常必为妖。 private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
...
protected T initialValue() {
return null;
}
SuppliedThreadLocal
。而我们回头初始化ThreadLocal 值的时候可以发现,我们获取的就是这个子类,那么我们就可以确定。 ThreadLocal<Target> threadLocal = ThreadLocal.withInitial(() -> new Target("张三"));
// 等同于。
ThreadLocal<Target> threadLocal = ThreadLocal.withInitial(new Supplier<Target>() {
@Override
public Target get() {
return new Target("张三");
}
});
ThreadLocal
时因为ThreadLocalMap
为null
,所以会调用 setInitialValue
方法获取初始值,setInitialValue
又会调用 initialValue
方法初始化一个初始值,而我们初始化初始值时又重写了 initialValue
方法。也就是说,当Demo1中 t1 调用ThreadLocal.get
时,也就是又调用了一次 initialValue
方法,也就重新 new Target("张三")
。所以三个线程执行ThreadLocal.get
执行出来的结果并不一样,因为他们每次拿到的对象并不一样。而Demo2中,因为我们传递的是一个 target
引用。导致三个线程获取的都是这一个对象,并且由于ThreadLocal
中并没有深拷贝,所以他们实际操作的是同一个对象,从而造成的Demo2的结果。以上:内容部分参考
https://www.jianshu.com/p/1ff73d2d7520
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正