前言
ThreadLocal是多线程中一个非常重要的对象,在面试中会被经常问到。所以明白其使用场景,使用方法以及原理和缺点是至关重要的。ThreadLocal实现了线程级别的数据隔离。
一、什么是ThreadLocal
答:每一个Thread对象有一个ThreadLocalMap对象(你可以理解成一个自定义的HashMap对象),ThreadLocalMap的key是ThreadLocal对象,value是一个Integer,String或者Boolean等等的对象。emmmmmm,大家不是说ThreadLocal对象是一个线程局部变量码?怎么到你这就成了一个key了呢?其实ThreadLocal对象并不存储值,只不过ThreadLocal对象的方法使得它看上去像一个存储值的线程局部变量了。然后每一个线程有一个属于自己的变量。算了,你还是讲一个例子让我们看看这对象到底是干啥子的吧!
二、使用场景与使用方法
最典型的是管理数据库的Connection:假设所有的线程通过DBUtil类来获得一个连接到数据库的连接(Connection),那么问题来了:每一个线程拥有一个Connection,那么我们怎么管理这些Connection呢?
- 在DBUtil定义一个Connection池,然后每来一个线程就给他从Connection池中拿一个Connection分配给这个线程
- DBUtil为每一个线程创造一个Connection,每一个线程将自己的Connection存储在自己的ThreadLocalMap中
第一种方式对于Connection的管理会很复杂,因为你需要存储Coonection和线程的对应关系,同时在线程数过大的时候池子可能装不下从而需要复杂的扩容。
第二种方式明显简单很多,当DBUtile想管理某一个线程的Connection的时候只需要通过一个共享的ThreadLocal对象就可以得到这个线程的Connection。
public class ThreadLocalTest {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new ConnectDB(),"1");
Thread t2 = new Thread(new ConnectDB(),"2");
t1.start(); // 线程1连接数据库
t2.start(); // 线程2连接数据库
t1.join(); // 等待线程1结束
t2.join(); // 等待线程2结束
}
}
class ConnectDB implements Runnable{
@Override
public void run() {
System.out.println(DBUtil.getConnection()); // 连接成功时返回Connection信息
}
}
class DBUtil{
/**
* 线程通过此类来连接到数据库
* 每一个线程有一个自己的连接
* */
private static ThreadLocal<String> connect = new ThreadLocal<>(); // key
private static String DBName = "mysql";
public static String getConnection(){
if (connect.get() != null){
return connect.get();
}else {
String c = Thread.currentThread() + "........DB" + " for " + DBName; // 这里用一个字符串表示一个连接
connect.set(c); // 给当前运行线程分配一个连接
return c;
}
}
}
三、ThreadLocal实现原理
打蛇打七寸!我们重点看一下set方法和get方法的实现原理。
- threadLocal.set(“values”)方法:将threadLocal对象做为key,“values”作为value存储到当前线程对象的ThreadLocalMap
- threadLocal.get()方法:返回当前线程对象ThreadLocalMap中key为threadLocal对象的value
public class ThreadLocal<T> {
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocal.ThreadLocalMap map = this.getMap(t);
if (map != null) {
map.set(this, value); // set线程t的ThreadLocalMap
} else {
this.createMap(t, value);
}
}
ThreadLocal.ThreadLocalMap getMap(Thread t) {
return t.threadLocals; // 返回线程t的ThreadLocalMap
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocal.ThreadLocalMap(this, firstValue); // 初始化线程t的ThreadLocalMap
}
static class ThreadLocalMap {
....
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { // ThreadLocalMap你看成一个自定义的HashMap就可以
this.table = new ThreadLocal.ThreadLocalMap.Entry[16];
int i = firstKey.threadLocalHashCode & 15;
this.table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue);
this.size = 1;
this.setThreshold(16);
}
}
private void set(ThreadLocal<?> key, Object value) {
....
}
}
可以看出来ThreadLocal的set方法并不是在操作ThreadLocal对象,而是一直在操作线程t对象的ThreadLocalMap。
public class ThreadLocal<T> {
public T get() {
Thread t = Thread.currentThread();
ThreadLocal.ThreadLocalMap map = this.getMap(t); // 得到线程t的ThreadLocalMap
if (map != null) {
ThreadLocal.ThreadLocalMap.Entry e = map.getEntry(this); // 根据key来得到Entry
if (e != null) {
T result = e.value;
return result; // 返回value
}
}
return this.setInitialValue(); // 返回null
}
}
get方法就比较简单了。
四、ThreadLocal,ThreadLocalMap,Thread关系 && 内存泄漏
我放一张别人的图:
首先我们只看堆:Thread对象中有一个ThreadLocalMap对象,ThreadLocalMap对象中有很多的个Entry,一个Entry由一个ThreadLocal对象做为key,一个Object做为value。
再看一下内存泄漏问题:当一个ThreadLocal对象不再被使用(即不再存在此对象的引用(reference))但是ThreadLocalMap中还有Entry用此对象做为Key(当然,这个Entry已经上永远都不可以使用到了的,因为程序中不存在此对象的引用了)那么此对象就不能被GC所回收从而导致内存泄漏。
解决方法上:threadLocal对象调用自身的remove方法
Conclusion
2020-8-14