原文地址:cmsblogs.com/?p=2442
ThreadLocal介绍
ThreadLocal
提供了一种解决多线程环境下成员变量的问题,但是它并不是解决多线程共享变量的问题。那么ThreadLocal
到底是什么呢?
API是这样介绍的:This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).
该类提供了线程局部(thread-local)变量。这些变量不同于普通对应物,因为访问某个变量(通过其get或set方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal实例通常是类中的private static字段,它们希望将状态与某一个线程(例如,用户ID或事务ID)相关联。
ThreadLocal
与线程同步机制不同,线程同步机制是多个线程共享同一个变量,而ThreadLocal
为了每一个线程创建一个单独的变量副本,故而每个线程都可以独立地改变自己所拥有的变量副本,而不会影响其他线程所对应的副本。
ThreadLocal
使用示例,代码如下:
public class SeqCount {private static ThreadLocal<Integer> seqCount = new ThreadLocal<Integer>() {// 实现initialValue()public Integer initialValue() {return 0;}};public int nextSeq() {seqCount.set(seqCount.get() + 1);return seqCount.get();}public static void main(String[] args) {SeqCount seqCount = new SeqCount();SeqThread thread1 = new SeqThread(seqCount);SeqThread thread2 = new SeqThread(seqCount);SeqThread thread3 = new SeqThread(seqCount);SeqThread thread4 = new SeqThread(seqCount);thread1.start();thread2.start();thread3.start();thread4.start();}private static class SeqThread extends Thread {private SeqCount seqCount;SeqThread(SeqCount seqCount) {this.seqCount = seqCount;}public void run() {for (int i = 0; i < 3; i++) {System.out.println(Thread.currentThread().getName() + " seqCount :"+ seqCount.nextSeq());}}}
}
复制代码
ThreadLocal实现原理
ThreadLocal
的实现是这样的:每个Thread
维护一个ThreadLocalMap
映射表,这个映射表的key
是 ThreadLocal
实例本身,value
是真正需要存储的Object
。
也就是说ThreadLocal
本身并不存储值,它只是作为一个key
来让线程从ThreadLocalMap
获取 value
。值得注意的是图中的虚线,表示ThreadLocalMap
是使用ThreadLocal
的弱引用作为Key
的,弱引用的对象在GC时会被回收。
ThreadLocal源码分析
ThreadLocalMap
ThreadLocalMap的构造函数如下:
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {table = new Entry[INITIAL_CAPACITY];int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);table[i] = new Entry(firstKey, firstValue);size = 1;setThreshold(INITIAL_CAPACITY);
}
复制代码
由上可知,ThreadLocalMap
其内部利用Entry
来实现key-value的存储,如下:
static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}
}
复制代码
可以看出Entry的key就是ThreadLocal,而value就是值。同时,Entry也继承WeakReference,所以说Entry所对应key(ThreadLocal实例)的引用为一个弱引用。
接下来,看看ThreadLocalMap
最核心的方法set(ThreadLocal> key, Object value)、getEntry()方法。
1、set(ThreadLocal<?> key, Object value)
private void set(ThreadLocal<?> key, Object value) {Entry[] tab = table;int len = tab.length;// 根据 ThreadLocal 的散列值,查找对应元素在数组中的位置int i = key.threadLocalHashCode & (len-1);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实例不存在则创建tab[i] = new Entry(key, value);int sz = ++size;// cleanSomeSlots 清楚陈旧的Entry(key == null)// 如果没有清理陈旧的 Entry 并且数组中的元素大于了阈值,则进行 rehashif (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();}复制代码
set()
操作除了存储元素外,还有一个很重要的作用,就是replaceStaleEntry
()和cleanSomeSlots()
,这两个方法可以清除掉key == null
的实例,防止内存泄漏。在set()
方法中还有一个变量很重要:threadLocalHashCode,定义如下:
private final int threadLocalHashCode = nextHashCode();
复制代码
从名字上面我们可以看出threadLocalHashCode应该是ThreadLocal的散列值,定义为final,表示ThreadLocal一旦创建其散列值就已经确定了,生成过程则是调用nextHashCode():
private static AtomicInteger nextHashCode = new AtomicInteger();private static final int HASH_INCREMENT = 0x61c88647;private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);
}
复制代码
nextHashCode
表示分配下一个ThreadLocal
实例的threadLocalHashCode
的值,HASH_INCREMENT
则表示分配两个ThradLocal
实例的threadLocalHashCode
的增量。
2、getEntry()
private Entry getEntry(ThreadLocal<?> key) {int i = key.threadLocalHashCode & (table.length - 1);Entry e = table[i];if (e != null && e.get() == key)return e;elsereturn getEntryAfterMiss(key, i, e);
}
复制代码
采用了开放定址法,所以当前key的散列值和元素在数组的索引并不是完全对应的,首先取一个探测数(key的散列值),如果所对应的key就是我们所要找的元素,则返回,否则调用getEntryAfterMiss()
。
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;// 当key == null时,调用了expungeStaleEntry()方法,该方法用于处理key == null,// 有利于GC回收,能够有效地避免内存泄漏。if (k == null)expungeStaleEntry(i);elsei = nextIndex(i, len);e = tab[i];}return null;}
复制代码
ThreadLocal核心方法
set(T value):设置当前线程的线程局部变量的值
public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);
}void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);
}
复制代码
获取当前线程所对应的ThreadLocalMap
,如果不为空,则调用ThreadLocalMap
的set()方法,key就是当前ThreadLocal
,如果不存在,则调用createMap()
方法创建。
get():返回当前线程所对应的线程变量
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;}}// 如果ThreadLocalMap不存在,返回初始值。return setInitialValue();
}
复制代码
首先通过当前线程获取所对应的成员变量ThreadLocalMap
,然后通过ThreadLocalMap
获取当前ThreadLocal
的Entry
,最后通过所获取的Entry
获取目标值result。
initialValue():返回该线程局部变量的初始值
protected T initialValue() {return null;
}
复制代码
这个方法将在一个线程第一次使用get
方法访问变量时被调用,除非线程先前调用了set
方法,在这种情况下,线程不会调用initialValue
方法。通常情况下,每个线程最多调用一次此方法,但在后续调用remove
和get
时,可能会再次调用此方法。
默认实现返回null,如果程序员希望线程局部变量具有非null的初始值,则必须对ThreadLocal
进行子类化,并重写此方法。
remove():将当前线程局部变量的值删除
public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null)m.remove(this);
}
复制代码
该方法的目的是减少内存的占用。当然,我们不需要显示调用该方法,因为一个线程结束后,它所对应的局部变量就会被垃圾回收。
ThreadLocal为什么会内存泄漏
ThreadLocalMap
使用ThreadLocal
的弱引用作为key,如果一个ThreadLocal
没有外部强引用来引用它,那么系统GC的时候,这个ThreadLocal
势必会被回收,ThreadLocalMap
中就会出现key
为null
的Entry,就没有办法访问这些key为null
的Entry的value
。如果当前线程再迟迟不结束的话,这些key为null
的Entry的value
就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永远无法回收,造成内存泄漏。
其实,ThreadLocalMap
的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal
的get()
,set()
,remove()
的时候都会清除线程ThreadLocalMap
里所有key
为null
的value
。
但是这些被动的预防措施并不能保证不会内存泄漏:
-
使用
static
的ThreadLocal
,延长了ThreadLocal
的生命周期,可能导致的内存泄漏。 -
分配使用了
ThreadLocal
又不再调用get()
,set()
,remove()
方法,那么就会导致内存泄漏。
ThreadLocal
内存泄漏的根源是:由于ThreadLocalMap
的生命周期跟Thread
一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
理解了ThreadLocal
内存泄漏的前因后果,那么怎么避免内存泄漏呢?
- 每次使用完
ThreadLocal
,都调用它的remove()
方法,清除数据。
在使用线程池的情况下,没有及时清理ThreadLocal
,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal
就跟加锁完要解锁一样,用完就清理。
参考资料
-
【死磕Java并发】—–深入分析ThreadLocal
-
深入分析 ThreadLocal 内存泄漏问题
如果读完觉得有收获的话,欢迎点赞、关注、加公众号【牛觅技术】,查阅更多精彩历史!!!: