1、ThreadLocal是什么?
首先,他本质是一个数据结构,有点像HashMap,可以保存 key:value 键值对,但是不同的是,ThreadLocal只能保存一对键值对。
ThreadLocal基本用法:
ThreadLocal<String> localName = new ThreadLocal();
localName.set("小王");
String name = localName.get();
既然他叫ThreadLocal,那肯定跟线程有关,他最重要的特性就是 各个线程的数据互不干扰 !比如上面代码中:线程 1 中初始化了一个ThreadLocal对象localName,并通过set方法,保存了一个值“小王”,同时在线程1中通过localName.get()可以拿到之前设置的值。但是如果在线程2中,拿到的将是一个null。所以很多叫ThreadLocal为线程本地变量。
2、ThreadLocal源码解析
ThreadLocal说白了就是一种数据结构,那么他是怎么做到保证各个线程的数据互不干扰?
ThreadLocal类提供如下几个核心方法:
-
get()方法用于获取当前线程的副本变量值。
-
set()方法用于保存当前线程的副本变量值。
-
initialValue()为当前线程初始副本变量值。
-
remove()方法移除当前线程的副本变量值。
重点看set(T value)和get()方法的源码:
public void set(T value) {
Thread t = Thread.currentThread();//获取当前线程
ThreadLocalMap map = getMap(t);//调用getMap方法,取得当前线程的ThreadLocalMap
if (map != null) // 往当前线程的ThreadLocalMap中set值
map.set(this, value);
else
createMap(t, value);
}
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();
}
ThreadLocalMap getMap(Thread t) {//返回当前线程的ThreadLocalMap
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
上面代码逻辑很简单,每次set的时候,先获取当前线程,然后从当前线程中获取ThreadLocalMap的一个数据结构(每一个线程都有),然后值保存在当前线程的ThreadLocalMap中。
哪个线程进来,我就set 和get 进来的那个线程本身的ThreadLocalMap,所以各个线程可以互不影响。
3、ThreadLoalMap
本文分析的1.8的源码
从名字上看,可以猜到它是一个类似map的结构,但是它没有实现Map接口。他是初始化一个大小为16的Entry数组,Entry是用来保存key-value键值对的数据结构,只不过这里的key在构造的时候就被限定死了,只能是ThreadLocal对象。这里很有意思,饶了一圈又饶了回去。通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己当做key,放进了ThreadLoalMap中。
值得注意的是:ThreadLoalMap的Entry是继承WeakReference,和HashMap很大的区别是,Entry中没有next字段,那么怎么解决冲突?
4、Hash冲突
ThreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1,寻找下一个相邻的位置。显然ThreadLocalMap采用线性探测的方式解决Hash冲突的效率很低,如果有大量不同的ThreadLocal对象放入map中时发送冲突,或者发生二次冲突,则效率很低。
5、内存泄漏
由于ThreadLocalMap的key是弱引用,而Value是强引用。这就导致了一个问题,ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key会被回收,而Value不会回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。
如何避免内存泄露?
既然Key是弱引用,那么我们要做的事,就是在调用ThreadLocal的get()、set()方法时完成后再调用remove方法,将Entry节点和Map的引用关系移除,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。
ThreadLocal<String> localName = new ThreadLocal();
try {
localName.set("小王");
// 其它业务逻辑
} finally {
localName.remove();//很重要!!!
}
6、ThreadLocal的应用。
6.1 管理Connection
当时在学JDBC的时候,为了方便操作写了一个简单数据库连接池,需要数据库连接池的理由也很简单,频繁创建和关闭Connection是一件非常耗费资源的操作,因此需要创建数据库连接池~
那么,数据库连接池的连接怎么管理呢??我们交由ThreadLocal来进行管理。为什么交给它来管理呢??ThreadLocal能够实现当前线程的操作都是用同一个Connection,保证了事务!
6.2 避免一些参数的传递
6.3 context的传递
每个用户请求页面时,我们都会创建一个任务,类似:
public void process(User user) {
checkPermission();
doWork();
saveStatus();
sendResponse();
}
然后,通过线程池去执行这些任务。观察process()方法,它内部需要调用若干其他方法,同时,我们遇到一个问题:如何在一个线程内传递状态?process()方法需要传递的状态就是User实例。有的童鞋会想,简单地传入User就可以了:
public void process(User user) {
checkPermission(user);
doWork(user);
saveStatus(user);
sendResponse(user);
}
但是往往一个方法又会调用其他很多方法,这样会导致User传递到所有地方:
void doWork(User user) {
queryStatus(user);
checkStatus();
setNewStatus(user);
log();
}
这种在一个线程中,横跨若干方法调用,需要传递的对象,我们通常称之为上下文(Context),它是一种状态,可以是用户身份、任务信息等。给每个方法增加一个context参数非常麻烦,而且有些时候,如果调用链有无法修改源码的第三方库,User对象就传不进去了。Java标准库提供了一个特殊的ThreadLocal,它可以在一个线程中传递同一个对象。
典型用法如下:
static ThreadLocal<User> threadLocalUser = new ThreadLocal<>();//ThreadLocal实例通常总是以静态字段初始化
void processUser(user) {
try {
threadLocalUser.set(user);
step1();
step2();
} finally {
threadLocalUser.remove();
}
}
//通过设置一个User实例关联到ThreadLocal中,在移除之前,所有方法都可以随时获取到该User实例:
void step1() {
User u = threadLocalUser.get();//普通的方法调用一定是同一个线程执行的,所以,step1()、step2()以及log()方法内,threadLocalUser.get()获取的User对象是同一个实例。
log();
printUser();
}
void log() {
User u = threadLocalUser.get();
println(u.name);
}
void step2() {
User u = threadLocalUser.get();
checkUser(u.id);
}