目录:
- 什么是ThreadLocal
- ThreadLocal使用示例
- 真实案例剖析
- 深入ThreadLocal源码
- ThreadLocal为什么会内存泄漏
- 总结
ThreadLocal是Java里一种特殊的变量。有些伙伴喜欢把它和线程同步机制混为一谈,事实上ThreadLocal与线程同步无关。ThreadLocal虽然提供了一种解决多线程环境下成员变量的问题,但是它并不是解决多线程共享变量的问题。
ThreadLocal与线程同步机制不同,线程同步机制是多个线程共享同一个变量,而ThreadLocal是为每一个线程创建一个单独的变量副本,故而每个线程都可以独立地改变自己所拥有的变量副本,而不会影响其他线程所对应的副本。可以说ThreadLocal为多线程环境下变量问题提供了另外一种解决思路。
注意: ThreadLocal实例本身是不存储值,它只是提供了一个在当前线程中找到副本值得key
ThreadLocal源码中有哪些方法?
除了这四个方法,ThreadLocal内部还有一个静态内部类
ThreadLocalMap提供了一种用键值对方式存储每一个线程的变量副本的方法,key为当前ThreadLocal对象,value则是对应线程的变量副本。
Thread、ThreadLocal、ThreadLocalMap的关系如下;
Thread
--ThreadLocal(属于Thread的成员变量,而不是Thread包含在ThreadLocal中,不要搞错了哦)
--ThreadLocalMap(属于ThreadLocal的静态内部类)
public class ThreadLocalTest {
private static ThreadLocal total = new ThreadLocal(){
// 实现initialValue()
@Override
public Integer initialValue() {
return 0;
}
};
public static int addOne(){
total.set(total.get()+1);
return total.get();
}
public static void main(String[] args) {
Runnable task = () -> {
for(int i=0;i<5;i++){
System.out.println(Thread.currentThread().getName()+" :ThreadLocalTest"+ThreadLocalTest.addOne());
}
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
Thread thread3 = new Thread(task);
thread1.start();
thread2.start();
thread3.start();
}
}
运行结果: 从运行结果可以看出,ThreadLocal确实是可以达到线程隔离机制,确保变量的安全性。
看了这个例子可能很多小伙伴还不知道在实际场景是怎么用的,对吧?那我这里再讲一个实际工作开发中的遇到的一个真实案例;
一天,张大胖上午遇到了一个棘手的问题,他在一个AccountService中写了一段类似这样的代码:
Context ctx = new Context();
ctx.setTrackerID(.....)
然后这个AccountService 调用了其他Java类,不知道经过了多少层调用以后,最终来到了一个叫做AccountUtil的地方,在这个类中需要使用Context中的trackerID来做点儿事情:
很明显,这个AccountUtil没有办法拿到Context对象, 怎么办?
张大胖想到,要不把Context对象一层层地传递下去,这样AccountUtil不就可以得到了吗? 可是这么做改动量太大!涉及到的每一层函数调用都得改动,有很多类都不属于自己的小组管理,还得和别人协调。
更要命的是有些类根本就没有源码,想改都改不了。
这也难不住我,张大胖想:可以把那个set/get TrackerID的方法改成静态(static)的,这样不管跨多少层调用都没有问题!
public class Context{
public static String getTrackerID(){
......
}
public static void setTrackerID(String id){
......
}
}
张大胖得意洋洋地把代码提交给Bill做Review。
Bill看了一眼就指出了致命的问题: 多线程并发的时候出错!
张大胖恨不得找个地缝钻进去:又栽在多线程上面了,这次犯的还是低级错误!
线程1调用了Context.setTrackerID(), 线程2 也调用了Context.setTrackerID(),数据互相覆盖,不出乱子才怪。
张大胖感慨地说:“像我这样中情况,需要在某处设置一个值,然后经过重重方法调用,到了另外一处把这个值取出来,又要线程安全,实在是不好办啊, 对了,我能不能把这个值就放到线程中? 让线程携带着这个值到处跑,这样我无论在任何地方都可以轻松获得了!”
Bill说:“有啊,每个线程都有一个私家领地! 在Thread这个类中有个专门的数据结构,你可以放入你的TrackerID,然后到任何地方都可以把这个TrackerID给取出来。”
“这么好? ”
张大胖打开JDK中的Thread类,仔细查看,果然在其中有个叫做threadLocals的变量,还是个Map类型 , 但是在Thread类中却没有对这个变量操作的方法。
看到张大胖的疑惑,Bill说:“也许你注意到了,这个变量不是通过Thread的访问的,对他的访问委托给了ThreadLocal这个类。”
“那我怎么使用它?”
“非常简单, 你可以轻松创建一个ThreadLocal类的实例:
ThreadLocal threadLocalA= new ThreadLocal();
线程1: threadLocalA.set("1234");
线程2: threadLocalA.set("5678");
像‘1234’, ‘5678’这些值都会放到自己所属的线程对象中。”
“等你使用的时候,可以这么办:” 线程1: threadLocalA.get() --> "1234" 线程2: threadLocalA.get() --> "5678"
“明白了,相当于把各自的数据放入到了各自Thread这个对象中去了,每个线程的值自然就区分开了。 可是我不明白的是为什么那个数据结构是个map 呢?”
“你想想,假设你创建了另外一个threadLocalB:”
ThreadLocal threadLocalB = new ThreadLocal();
线程1: threadLocalB.set(30);
线程2: threadLocalB.set(40);
“明白了,这个私家领地还真是好用,我现在就把我那个Context给改了,让它使用ThreadLocal:”
public class Context {
private static final ThreadLocal mThreadLocal
= new ThreadLocal();
public static void setTrackerID(String id) {
mThreadLocal.set(id);
}
public static String getTrackerID() {
return mThreadLocal.get();
}
}
ThreadLocalMap是ThreadLocal的匿名内部类,是实现ThreadLocal的关键;
ThreadLocalMap其内部利用Entry来实现key-value的存储,如下: 从上面可以看出:
ThreadLocalMap的源码比较多,我们就看两个最核心的方法吧!
set(ThreadLocal key, Object value)
/**
* Set the value associated with key.
*
* @param key the thread local object
* @param value the value to be set
*/
private void set(ThreadLocal> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
// 根据 ThreadLocal 的散列值,查找对应元素在数组中的位置
int i = key.threadLocalHashCode & (len-1);
// 采用“线性探测法”,寻找合适位置(解决hash冲突的一种方法)
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 == null,但是存在值(因为此处的e != null)
//说明之前的ThreadLocal对象已经被回收了
if (k == null) {
// 用新元素替换陈旧的元素
replaceStaleEntry(key, value, i);
return;
}
}
// ThreadLocal对应的key实例不存在也没有陈旧元素,new 一个
tab[i] = new Entry(key, value);
int sz = ++size;
// cleanSomeSlots 清除老旧的Entry(key == null)
// 如果没有清理陈旧的 Entry 并且数组中的元素大于了阈值,则进行 rehash
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
这里的set()操作和我们在集合类的put()方式不一样的,虽然他们都是key-value结构,不同在于他们解决散列冲突的方式不同。
set()操作除了存储元素外,还有一个很重要的作用,就是replaceStaleEntry()和cleanSomeSlots(),这两个方法可以清除掉key == null 的实例,防止内存泄漏。
set()方法中还有一个很重要变量:threadLocalHashCode,定义如下:
private final int threadLocalHashCode = nextHashCode();
从名字上面我们可以看出threadLocalHashCode应该是ThreadLocal的散列值,定义为final,表示ThreadLocal一旦创建其散列值就已经确定了,生成过程则是调用nextHashCode():
private static AtomicInteger nextHashCode =
new AtomicInteger();
/**
* The difference between successively generated hash codes - turns
* implicit sequential thread-local IDs into near-optimally spread
* multiplicative hash values for power-of-two-sized tables.
*/
private static final int HASH_INCREMENT = 0x61c88647;
/**
* Returns the next hash code.
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
nextHashCode表示分配下一个ThreadLocal实例的threadLocalHashCode的值,HASH_INCREMENT则表示分配两个ThradLocal实例的threadLocalHashCode的增量,从nextHashCode就可以看出他们的定义。
getEntry()
/**
* Get the entry associated with key. This method
* itself handles only the fast path: a direct hit of existing
* key. It otherwise relays to getEntryAfterMiss. This is
* designed to maximize performance for direct hits, in part
* by making this method readily inlinable.
*
* @param key the thread local object
* @return the entry associated with key, or null if no such
*/
private Entry getEntry(ThreadLocal> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
由于采用了开放定址法,所以当前key的散列值和元素在数组的索引并不是完全对应的,首先取一个探测数(key的散列值),如果所对应的key就是我们所要找的元素,则返回,否则调用getEntryAfterMiss(),如下:
/**
* Version of getEntry method for use when key is not found in
* its direct hash slot.
*
* @param key the thread local object
* @param i the table index for key's hash code
* @param e the entry at table[i]
* @return the entry associated with key, or null if no such
*/
private Entry getEntryAfterMiss(ThreadLocal> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
这里有一个重要的地方,当key == null时,调用了expungeStaleEntry()方法,该方法用于处理key == null,有利于GC回收,能够有效地避免内存泄漏。
看完了ThreadLocal的ThreadLocalMap,我们再来看看ThreadLocal的set()和get(), initialValue()方法;
返回当前线程所对应的线程变量
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
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,然后通过ThreadLocalMap获取当前ThreadLocal的Entry,最后通过所获取的Entry获取目标值result。
getMap()方法可以获取当前线程所对应的ThreadLocalMap,如下:
/**
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
设置当前线程的线程局部变量的值。
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
获取当前线程所对应的ThreadLocalMap,如果不为空,则调用ThreadLocalMap的set()方法,key就是当前ThreadLocal,如果不存在,则调用createMap()方法新建一个,如下:
/**
* Create the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the map
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
返回该线程局部变量的初始值。
protected T initialValue() {
return null;
}
将当前线程局部变量的值删除。
//该方法的目的是减少内存的占用。当然,我们不需要显示调用该方法
//因为一个线程结束后,它所对应的局部变量就会被垃圾回收。
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
前面提到每个Thread都有一个ThreadLocal.ThreadLocalMap的map,该map的key为ThreadLocal实例,它为一个弱引用,我们知道弱引用有利于GC回收。当ThreadLocal的key == null时,GC就会回收这部分空间,但是value却不一定能够被回收,因为他还与Current Thread存在一个强引用关系,如下(图片来自http://www.jianshu.com/p/ee8c9dccc953): 因为map中存在这个强引用关系,这就会会导致这个value无法被回收。如果这个线程对象不被销毁,那么这个强引用关系会一直存在,然后就会出现内存泄漏情况了。所以说只要这个线程对象能够及时被GC回收,就不会出现内存泄漏。如果碰到线程池,那问题就更大了。
那这样要怎么避免这个问题呢?
在前面提过,在ThreadLocalMap中的setEntry()、getEntry(),如果遇到key == null的情况,会对value设置为null。当然我们也可以显示调用ThreadLocal的remove()方法进行处理。
下面再对ThreadLocal进行简单的总结:
ThreadLocal 不是用于解决共享变量的问题的,也不是为了协调线程同步而存在,而是为了方便每个线程处理自己的状态而引入的一个机制。这点至关重要。
每个Thread内部都有一个ThreadLocal.ThreadLocalMap类型的成员变量,该成员变量用来存储实际的ThreadLocal变量副本。
ThreadLocal并不是为线程保存对象的副本,它仅仅只起到一个索引的作用。它的主要木得视为每一个线程隔离一个类的实例,这个实例的作用范围仅限于线程内部。
参考来源:https://mp.weixin.qq.com/s/L_5WPcX5pzM8GmTpQbytzw
https://mp.weixin.qq.com/s/k4cMqePHagb15-jlYh4PkA
关注微信公众号【悟能之能】了解更多编程技巧。