每个线程都会有属于自己的本地内存,在堆中的变量在被线程使用的时候会被复制一个副本线程的本地内存中,当线程修改了共享变量之后就会通过JMM管理控制写会到主内存中。
很明显,在多线程的场景下,当有多个线程对共享变量进行修改的时候,就会出现线程安全问题,即数据不一致问题。常用的解决方法是对访问共享变量的代码加锁(synchronized或者Lock)。但是这种方式对性能的耗费比较大。
ThreadLocal意为线程本地变量,用于解决多线程并发时访问共享变量的问题。在JDK1.2中引入了ThreadLocal类,来修饰共享变量,使每个线程都单独拥有一份共享变量,这样就可以做到线程之间对于共享变量的隔离问题。
ThreadLocal使用
- 一般都会将ThreadLocal声明成一个静态字段,同时初始化如下:
static ThreadLocal<Object> threadLocal = new ThreadLocal<>();
其中Object就是原本堆中共享变量的数据。
例如,有个User对象需要在不同线程之间进行隔离访问,可以定义ThreadLocal如下:
public class Test {
static ThreadLocal<User> threadLocal = new ThreadLocal<>();
}
- 常用的方法
- set(T value):设置线程本地变量的内容。
- get():获取线程本地变量的内容。
- remove():移除线程本地变量。注意在线程池的线程复用场景中在线程执行完毕时一定要调用remove,避免在线程被重新放入线程池中时被本地变量的旧状态仍然被保存。
public class CommisionContext {
private static ThreadLocal<CommisionAggData> commisionAggDataThreadLocal = new ThreadLocal();
public CommisionContext() {
}
public static CommisionAggData getCommisionAggData() {
return commisionAggDataThreadLocal.get();
}
public static void setCommisionAggData(CommisionAggData data) {
commisionAggDataThreadLocal.set(data);
}
public static void remove() {
commisionAggDataThreadLocal.remove();
}
}
原理
那么如何究竟是如何实现在每个线程里面保存一份单独的本地变量呢?首先,在Java中的线程是什么呢?是的,就是一个Thread类的实例对象!而一个实例对象中实例成员字段的内容肯定是这个对象独有的,所以我们也可以将保存ThreadLocal线程本地变量作为一个Thread类的成员字段,这个成员字段就是:threadLocals
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
是一个在ThreadLocal中定义的Map对象,内部使用Entry保存了该线程中的所有本地变量。
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
ThreadLocalMap中的Entry的定义如下:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
// key为一个ThreadLocal对象,v就是我们要在线程之间隔离的对象
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
set
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程的threadLocals字段
ThreadLocalMap map = getMap(t);
// 判断线程的threadLocals是否初始化了
if (map != null) {
map.set(this, value);
} else {
// 没有则创建一个ThreadLocalMap对象进行初始化
createMap(t, value);
}
}
createMap方法的源码如下:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
map.set方法的源码如下:
/**
* 往map中设置ThreadLocal的关联关系
* set中没有使用像get方法中的快速选择的方法,因为在set中创建新条目和替换旧条目的内容一样常见,
* 在替换的情况下快速路径通常会失败(对官方注释的翻译)
*/
private void set(ThreadLocal<?> key, Object value) {
// map中就是使用Entry[]数据保留所有的entry实例
Entry[] tab = table;
int len = tab.length;
// 返回下一个哈希码,哈希码的产生过程与神奇的0x61c88647的数字有关
int i = key.threadLocalHashCode & (len-1);
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();
}
get
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
// 获取ThreadLocal对应保留在Map中的Entry对象
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
// 获取ThreadLocal对象对应的值
T result = (T)e.value;
return result;
}
}
// map还没有初始化时创建map对象,并设置null,同时返回null
return setInitialValue();
}
remove
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
// 键在直接移除
if (m != null) {
m.remove(this);
}
}
ThreadLocal设计
在JDK早期的设计中,每个ThreadLocal都有一个map对象,将线程作为map对象的key,要存储的变量作为map的value,但是现在已经不是这样了。
JDK8之后,每个Thread维护一个ThreadLocalMap对象,这个Map的key是ThreadLocal实例本身,value是存储的值要隔离的变量,是泛型,其具体过程如下:
每个Thread线程内部都有一个Map(ThreadLocalMap::threadlocals);
Map里面存储ThreadLocal对象(key)和线程的变量副本(value);
Thread内部的Map由ThreadLocal维护,由ThreadLocal负责向map获取和设置变量值;
对于不同的线程,每次获取副本值时,别的线程不能获取当前线程的副本值,就形成了数据之间的隔离。
JDK8之后设计的好处在于:
每个Map存储的Entry的数量变少,在实际开发过程中,ThreadLocal的数量往往要少于Thread的数量,Entry的数量减少就可以减少哈希冲突。
当Thread销毁的时候,ThreadLocalMap也会随之销毁,减少内存使用,早期的ThreadLocal并不会自动销毁。
使用ThreadLocal的好处
- 保存每个线程绑定的数据,在需要的地方可以直接获取,避免直接传递参数带来的代码耦合问题;
- 各个线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失。
ThreadLocal内存泄露问题
内存泄露为程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光,
不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露。
**强引用:**使用最普遍的引用(new),一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。
如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样可以使JVM在合适的时
间就会回收该对象。
**弱引用:**JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。可以在缓存中使用弱引用。
ThreadLocal的实现原理,每一个Thread维护一个ThreadLocalMap(线程本地变量,线程私有,K-V型),key为使用弱引用的ThreadLocal实例,value为线程变量的副本。
ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal不存在外部强引用时, Key(ThreadLocal)势必会被GC回收,这样就会导致ThreadLocalMap中key为null, 而value还存在着强
引用,只有thead线程退出以后,value的强引用链条才会断掉,但如果当前线程再迟迟不结束的话,这
些key为null的Entry的value就会一直存在一条强引用链(红色链条)
key 使用强引用
当ThreadLocalMap的key为强引用回收ThreadLocal时,因为ThreadLocalMap还持有ThreadLocal的强
引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
key 使用弱引用
当ThreadLocalMap的key为弱引用回收ThreadLocal时,由于ThreadLocalMap持有ThreadLocal的弱
引用,即使没有手动删除,ThreadLocal也会被回收。当key为null,在下一次ThreadLocalMap调用
set(),get(),remove()方法的时候会被清除value值。
因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
ThreadLocal正确的使用方法
每次使用完ThreadLocal都调用它的remove()方法清除数据。
将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,不会因为GC导致threadLocal被回收,这样ThreadLocalMap就不会清清除这个threadLocal中的数据。也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值。这种情况下ThreadLocalMap可以一直通过threadLocal访问ThreadLocalMap中的数据。
但是这样也会导致 除非线程消亡,ThreadLocalMap中的Entry将一直得不到释放。需要在最终调用remove方法清除数据
为什么要设置成如弱引用
如果将ThreadLocal变量定义成局部变量,那么可能会导致作为 ThreadLocalMap key值的ThreadLocal因为失去栈中的引用,并且由于弱引用被GC,当key为null,在下一次ThreadLocalMap调用
set(),get(),remove()方法的时候会被清除value值。当然我们并不能保证在key为空之后,一定还会调用set(),get(),remove()等方法,因此,还是要在使用之后手动remove