java线程本地变量ThreadLocal详解

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

java线程本地变量ThreadLocal详解
介绍
ThreadLocal作为JDK1.2以来的⼀个ng包下的⼀个类,在⾯试和⼯程中都⾮常重要,这个类的主要⽬的是提供线程本地的变量,所以也有很多地⽅把这个类叫做线程本地变量
从字⾯理解,这个类为每个线程都创建了⼀个本地变量,实际上是ThreadLocal为变量在每个线程中都创建了⼀个副本,使得每个线程都可以访问⾃⼰内部的副本变量
通常提到多线程,都会考虑变量同步的问题,但是ThreadLocal并不是为了解决多线程共享变量同步的问题,⽽是为了让每个线程的变量不互相影响,相当于线程之间操纵的都是变量的副本,⾃然就不⽤考虑多线程竞争的问题,也⾃然没有性能损耗
使⽤⽅式
先来看常⽤的这⼏个⽅法
public T get() { }
public void set(T value) { }
public void remove() { }
protected T initialValue() { }
显⽽易见,get()⽅法获取线程拥有的副本值,set()⽅法进⾏设值,remove()⽅法移除,initialValue()进⾏变量初始化,我们先来看下⾯这个实例,同时体会⼀下应⽤场景
public class Demo {
public static ThreadLocal<Integer> threadLocal = null;
public static void main(String[] args) {
threadLocal = new ThreadLocal<Integer>() {
/**
* 通过重写该⽅法来初始化ThreadLocal的值
*/
@Override
protected Integer initialValue() {
return 10;
}
};
MyThread t1 = new MyThread(20);
MyThread t2 = new MyThread(30);
t1.start();
// 这⾥为了描述清晰,省略了try-catch语句块
t1.join();
t2.start();
}
}
在上述⽅法中,我们定义并初始化⼀个ThreadLocal类为10(通过重写initialValue()⽅法实现),然后开启了两个线程,同时我们这⾥让t2线程等待t1线程执⾏完再执⾏
MyThread类详细信息如下
class MyThread extends Thread {
private int val = 0;
MyThread(int val) {
this.val = val;
}
@Override
public void run() {
System.out.println(Thread.currentThread() + "-BEFORE-" + Demo.threadLocal.get());
Demo.threadLocal.set(val);
System.out.println(Thread.currentThread() + "-AFTER-" + Demo.threadLocal.get());
}
}
我们通过调⽤ThreadLocal对象的get()⽅法来获取当前的值,然后通过set()⽅法设置⼀个新值(每个线程我们设置不同的值),然后再通过get()⽅法来获取设置后的值
运⾏结果如下
重点是图中标注的t2线程变量的初始值,虽然我们在t1线程中修改了变量的值,但是在t2线程中变量值并没有被改变,这样就
实现了每个线程独有的变量
同时,如果⼀个ThreadLocal对象要在很多地⽅进⾏复⽤时,需要在使⽤前通过调⽤**remove()**⽅法来将本地变量恢复到默认值
也许有⼈会问了,我们给每个线程定义⾃⼰的私有变量不是也可以实现同样的操作吗,理论上当然是可⾏的,但是ThreadLocal远⽐私有变量的形式⽅便,不仅可以在线程外部进⾏统⼀的初始化,⽽且避免在线程内部额外设置变量
原理
点进ThreadLocal的源码中,发现并没有存储变量的字段值,那看来ThreadLocal并不负责保存变量,我们只能从⽅法下⼿
先看initial()⽅法,毕竟我们的变量默认初始值就是在这个⽅法中设置,如下
protected T initialValue() {
return null;
}
我们在每次创建ThreadLocal都要重写这个⽅法,那么这个⽅法到底在哪调⽤呢,我们点进get()⽅法源码中,如下
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程的map
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
T result = (T)e.value;
return result;
}
}
// 如果map为空则进⾏创建
return setInitialValue();
}
有点眉头了,我们发现这⾥获取了⼀个ThreadLocalMap对象,所以会想到有可能是通过让线程与变量作⼀个KV表,来实现每个线程拥有⾃⼰独有的变量
我们点进getMap(t)⽅法中,发现返回了线程t的⼀个threadLocals属性,这是Thread类的⼀个字段:
ThreadLocal.ThreadLocalMap threadLocals = null;
这是⼀个由ThreadLocal类维护的属性,Thread的任何⽅法都没有对这个字段进⾏修改操作,⽽这个ThreadLocalMap本⾝⼜是ThreadLocal的⼀个内部类,可以把它理解成⼀个Map(虽然这个类没有继承Map接⼝)
同时要注意,在ThreadLocalMap对象中的Entry对象(键值对),继承了⼀个ThreadLocaMap的弱引⽤,如下
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
也就是说,当ThreadLocal被置为空时,Entry中的Key则会在下⼀次YGC中被回收
我们还是没有看到initialValue()⽅法,别急,点进setInitialValue()⽅法,也就是如果在get()⽅法中检测到map为空时调⽤的⽅法,如下
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;
}
现在基本操作我们都清楚了,set()⽅法和initialValue()⼏乎完全⼀致,remove()⽅法则是普通地移除了⼀个KV键值对(K为当前线程),这⾥均不再列出,如果感兴趣可以⾃⾏查看
注意事项
1.脏数据
从上⾯的分析可以看出,ThreadLocal是和Thread绑定的,每⼀个Thread对应⼀个value,如果没有在使⽤结束后调⽤remove()⽅法,就会在下⼀次重⽤时读到脏数据(针对同⼀个线程⽽⾔),尤其是使⽤线程池的场景(线程池中的线程经常会复⽤)
2.内存泄露
⼀般在使⽤时都会将ThreadLocal设置为静态字段,这时候当线程执⾏完成后,KV中的V是不会⾃动回收的,所以要在使⽤完后及时调⽤remove()⽅法清理
以上就是本⽂的全部内容,希望对⼤家的学习有所帮助,也希望⼤家多多⽀持。

相关文档
最新文档