ThreadLocal底层原理实现
概述
ThreadLocal是java.lang包中的一个类,用来实现变量的线程封闭性,即只有当前线程可以操作该变量,通过把一个变量存在当前线程的一个Map容器中来实现。当然,这样解释很抽象,对于一些人来说难以理解,这里我先介绍一个《java并发编程实战》中描述的jdbc应用场景,来让你知道ThreadLocal到底有什么用。
ThreadLocal的意义
一般来讲,在单线程应用程序中可能会维持一个全局的数据库连接,并在程序启动时初始化这个连接对象,从而避免在调用每个方法(save,get等)时都要传递一个Connection对象。由于Jdbc连接对象不一定是线程安全的,因此,当多线程应用程序在没有协同的情况下使用全局变量时,就不是线程安全的,比如线程一刚获取全局的Connection,准备进行数据库操作,但线程二却执行了Connection.close()。
这种情况下我们可能会取消Connection这个全局变量,在每次要进行数据库相关操作时直接new一个Connection对象进行连接,而这又会导致一个线程执行多次数据库操作时要new多个connection对象,加大系统的负担。这时就会想能不能有这样一种方法,既不让Connection成为全局变量来保证线程安全,又可以实现全局Connection带来的“一次连接”式的便利?ThreadLocal就是做这件事的。
简要应用
下面第一行代码new了一个静态的ThreadLocal<Connection>。以后只要让每个线程在进行jdbc操作前都执行第二行代码,就会把一个创建的Connection对象存放到当前线程的map容器中(保留个引用),后面该线程要进行数据库操作时只要执行第三行代码,就能拿到这个引用进行连接,不用每次都new一个新的。
你可能会问只是一句简单的connectionHolder.get()代码,而且ThreadLocal对象只有一个,那怎么能准确的拿到当前线程中的Connection呢?答案就是这个connection是存在当前线程(一个Thread)中的,不是ThreadLocal中。表面上调用的ThreadLocal.get()和get(),实际上是在当前线程对象中进行设置和查找。不过当前线程的map容器中可能会存多个ThreadLocal的值,所以Map中的key就是ThreadLoca对象,值就是connection,来进行区分。还不懂的话见下面的代码解析,很简单。
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<>();
connectionHolder.set(DriverManager.getConnection(DB_URL));
connectionHolder.get();
源代码分析
Thread的map容器
threadLocals是Thread类的一个属性,该字段的类型是ThreadLocalMap。
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
而ThreadLocalMap是ThreadLocal类中的静态内部类,类字段如下:
private static final int INITIAL_CAPACITY = 16;
private int size = 0;
private int threshold;
private Entry[] table; //Thread的map容器
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
大家可能会好奇,table中的Entry只有一个value,并没有像Hashmap中那样是键值对啊,为什么叫它map容器呢?其实Entry是有key的,不过该字段是从Reference类中继承的,从Entry的构造方法中可看到传入了一个ThreadLocal对象,它就是key,我们追踪super(k),最后发现在Entry的父类Reference中:
private T referent;
//ThreadLocal最终传给了下面方法的第一个参数
Reference(T referent, ReferenceQueue<? super T> queue) {
this.referent = referent;
this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}
知道了Thread的map容器的结构,那接下来看看ThreadLocal的其他方法。
ThreadLocal的set方法
先拿到当前线程的map容器,再进行插入或new一个新的。
public void set(T value) {
Thread t = Thread.currentThread(); //获取当前线程对象
ThreadLocalMap map = getMap(t); //拿到t的map容器
if (map != null)
map.set(this, value); //map不空,根据key(调用set的ThreadLocal)设置值
else
createMap(t, value); //map为空则创建一个map(第一次插入,有点怪怪的)
}
ThreadLocalMap的set方法
类似于HashMap的put,但没有对链表的遍历,是对table的遍历。这里我没有对ThreadLocalMap中的replaceStaleEntry()、cleanSomeSlots()进行仔细研究,以后有时间了再介绍吧。
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
//根据ThreadLocal的hash值计算应该从table中哪个位置开始(跟HashMap一样)
int i = key.threadLocalHashCode & (len-1);
//遍历table中的每一个Entry
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//如果找到相同的key,覆盖并返回
if (k == key) {
e.value = value;
return;
}
//如果发现了空key为空,即ThreadLocal对象被GC了,就把数据放到该位置
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//当返现e==null时,执行下面代码,new再检测是否扩容
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
ThreadLocal的get方法
很简单,如果map里面已经存了对应ThreadLocal的值,则返回;否则执行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的setInitialValue方法
会返回一个初始值,由initialValue返回。可以看到initialValue方法只是返回了一个null,如果我们不重写它的话。同时再加一个set中的判断,若map为null,把initialValue返回的初值也存进去;否则创建一个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;
}
protected T initialValue() {
return null;
}
如果大家想要对ThreadLocal有更直观的了解,可以看一下这篇文章:https://mp.weixin.qq.com/s/aM03vvSpDpvwOdaJ8u3Zgw