最通俗易懂的ThreadLocal原理详解

时间:2021-08-05 14:46:43

一、ThreadLocal的用途

 ThreadLocal是线程局部变量,主要用于解决多线程程序的并发问题。产生并发问题的原因是多个线程并发访问共享变量。下面看一段程序。这是一个数据库连接工具类:
public class DBConn {

private static Connection conn;

public static Connection getConn(){
//省略获得connection的过程
return conn;
}
}
当多个线程并发获取Connection时,会出现严重的问题,试想所有线程共享的都是一个Connection,若有一个线程close了Connection,其余线程的连接就会受到影响。
当然有一个最简单的办法,可以在getConn()方法上加上同步锁sychronized,但是这样就回到了串行访问的模式,失去了并发的意义。
这时候我们可以让每个线程都拥有一个Connection的副本,这样线程之间不会互相影响,即使用ThreadLocal。
public class DBConn {

private static final ThreadLocal<Connection> holder = new ThreadLocal<Connection>();

public static Connection getConn(){
//省略获得connection的过程以及往holder中set值的过程
return holder.get();
}
}
这样每个线程得到的Connection都是副本,都是局部变量,不会被其他线程影响。但是这个Demo仅供测试,建议不要这样自己写数据库连接,在工程中使用数据库连接池会更好。

二、ThreadLocal的底层原理

1、核心数据结构及方法

ThreadLocalMap是ThreadLocal的一个静态内部类,是一个Map类型。先看一下源码。
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
注意这是一个Key为ThreadLocal,Value为Object的键值对,关于WeakReference之后提到内存泄漏时再做详细解释。虽然ThreadLocalMap是ThreadLocal的静态内部类,但是每个Thread内部都有一个ThreadLocalMap的成员变量。

ThreadLocal.ThreadLocalMap threadLocals = null;
这就说明了每个线程内部都有一个Map,很多人会想为什么要用一个Map,而不用其他数据结构,因为首先一个线程肯定有不止一个局部变量,每个局部变量都要通过一个索引来获得,索引就是ThreadLocal对象了。
接下来我们来看一下ThreadLocal的最重要的get()和set()方法到底做了什么。
先看ThreadLocal的方法set(T value)方法:
public void set(T value) {
Thread t = Thread.currentThread();//获取当前代码运行的线程
ThreadLocalMap map = getMap(t);//获取Thread对象内部的成员变量ThreadLocalMap,里面有该线程所有的ThreadLocal局部变量
if (map != null)
map.set(this, value);//在map内添加一个键值对
else
createMap(t, value);//若map为空,则初始化map并且添加键值对
}
下面我们来看看getMap(Thread t)方法,非常简单。
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
最重要的是ThreadLocalMap的set(ThreadLocal<?> key, Object value)方法。

private void set(ThreadLocal<?> key, Object value) {

Entry[] tab = table;//获取键值对数组索引
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);//threadLocalHashCode是每个ThreadLocal的hash编码,这种&运算求数组下标的方式与HashMap相同,可以看我的博客,里面有详解
  //找到一个合适的key不为null的位置插入,至于为什么key会为null,后面会说到
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();

if (k == key) {
e.value = value;
return;
}

if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}

tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
看完了set方法,接下来看一下get方法。
ThreadLocal的get方法:
public T get() {        Thread t = Thread.currentThread();//得到当前线程
ThreadLocalMap map = getMap(t);//获得当前线程的ThreadLocalMap
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();//若map为空,其实就是初始化map并返回null值的value
}
ThreadLocal内存模型图:
最通俗易懂的ThreadLocal原理详解
(图片来自网络)


2、ThreadLocal内存泄漏问题

因为ThreadLocalMap中的Entry的键ThreadLocal是WeakReference,即弱引用,在之前的文章中有写到在JVM中弱引用在垃圾回收时,不管内存有没有占满,都会被GC回收。因此很有可能在某次GC之后,某个线程的某个ThreadLocal变量变成了null,那么在Entry中,Key也变成了null,在查找时将永远不会被找到,这个Entry的Value将永远不会被用到,这就是内存泄漏。
当然ThreadLocalMap内部也有解决这个泄漏问题的方法,可以看到ThreadLocalMap在set时,有一个程序段

if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
在选择数组的插入位置时,当发现Key为null时,会调用replaceStaleEntry,即把Key为null的Entry移除,并且做一些相应的处理。
当然,一般我们会把ThreadLocal声明为static final,这样做可以使得ThreadLocal保持强引用。

三、ThreadLocal的使用注意点

如果把同一个对象的索引存储到不同线程的局部变量中,其实他们还是共享了同一个变量,没有解决变量共享带来的并发问题。
ThreadLocal中存储的应该是变量的拷贝或者是重新new出来的对象,这样才算是副本。