使用ThreadLocal——解决线程共享安全问题
使用ThreadLocal——解决线程共享安全问题
ThreadLocal是Java中处理线程安全问题的重要工具,它通过为每个线程提供变量的副本,避免了线程间的共享数据冲突。本文将深入探讨ThreadLocal的工作原理、使用方法以及与Synchronized的区别,帮助读者更好地理解和应用这一技术。
一、什么是ThreadLocal?
ThreadLocal是一个类,为每个线程都提供了变量的副本,使得每个线程在某一时间访问到的并非同一个对象,这样就隔离了多个线程对数据的数据共享。
二、如何使用?
先来看看ThreadLocal的几个核心方法:
方法声明 | 描述 |
---|---|
public void set(T value) | 设置当前线程绑定的局部变量 |
public T get() | 获取当前线程绑定的局部变量 |
Public void remove() | 移除当前线程绑定的局部变量 |
protected Object initialValue() | 初始化值 |
使用时需要注意:initialValue()
这个方法是一个延迟调用方法,在线程第1次调用get()
或set(Object)
时才执行,并且仅执行1次。如果没有实现该方法,则直接赋值为null,后续操作时会报NullPointerException异常。
代码证明,该代码中,我们并没有调用initialValue
方法进行初始化,那么变量的值是不是null,那我们进行取数据然后+1是不是就会报错,null+1能执行吗?对不对:
三、ThreadLocal内部是如何实现的呢?
- set方法——设置当前线程中 ThreadLocal 变量的值:
public void set(T value) {
//1.获取当前线程实例对象
set(Thread.currentThread(), value);
}
private void set(Thread t, T value) {
//2.通过当前线程获取一个ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
if (map != null) {
//3.如果map不为null,则以key为当前ThreadLocal,值为value进行存入
map.set(this, value);
} else {
//4.如果map为null,则创建ThreadLocalMap
createMap(t, value);
}
}
上面方法的逻辑很清晰,具体请看上面的注释,通过源码我们可以知道value是存放在ThreadLocal里的,当前先把它理解为一个普普通通的map即可,也就是说,数据value是存放在ThreadLocalMap这个容器中的,并且是以当前的ThreadLocal实例为key的。
首先 ThreadLocalMap 是怎样来的?源码很清楚,是通过
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
ThreadLocal.ThreadLocalMap threadLocals;
createMap方法,其实就是new了一个ThreadLocalMap对象,然后同样以当前 ThreadLocal 实例作为 key,值为 value 存放到 ThreadLocalMap 中的,然后将当前线程对象的 ThreadLocals 赋值为 ThreadLocalMap 对象。
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
总结一下set方法:通过当前线程对象Thread获取该thread所维护的ThreadLocalMap,如果ThreadLocalMap不为null,则以ThreadLocal实例为key,值为value的键值对存入ThreadLocalMap;若为null,就新建ThreadLocalMap,然后再以ThreadLocal实例为key,值为value的键值对存入即可。
- get方法:
public T get() {
//1.获取当前线程实例
return get(Thread.currentThread());
}
private T get(Thread t) {
//2.通过当前线程获取一个ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
if (map != null) {
if (map == ThreadLocalMap.NOT_SUPPORTED) {
return initialValue();
} else {
//3.获取map中以ThreadLocal实例为key的值Entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
//4.如果e存在,则将返回对应的value
T result = (T) e.value;
return result;
}
}
}
//5.若map为null或者entry为null的话,通过该方法初始化,并返回初始化的值。
return setInitialValue(t);
}
另外,看下 setInitialValue 主要做了些什么事情?该方法中就调用了initialValue()方法
private T setInitialValue(Thread t) {
T value = initialValue();
ThreadLocalMap map = getMap(t);
assert map != ThreadLocalMap.NOT_SUPPORTED;
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
if (this instanceof TerminatingThreadLocal<?> ttl) {
TerminatingThreadLocal.register(ttl);
}
return value;
}
并且你自己没有重写initialValue()方法的话,它默认返回null
protected T initialValue() {
return null;
}
总结一下get方法:通过当前线程 thread 实例获取到它所维护的 ThreadLocalMap,然后以当前 ThreadLocal 实例为 key 获取该 map 中的键值对(Entry),如果 Entry 不为 null 则返回 Entry 的 value。如果获取 ThreadLocalMap 为 null 或者 Entry 为 null 的话,就以当前 ThreadLocal 为 Key,value 为 null 存入 map 后,并返回 null。
- remove方法:
public void remove() {
//1.获取当前线程实例
remove(Thread.currentThread());
}
private void remove(Thread t) {
//2.通过当前线程获取一个ThreadLocalMap对象
ThreadLocalMap m = getMap(t);
if (m != null && m != ThreadLocalMap.NOT_SUPPORTED) {
//3.如果不为null,从map中删除以当前ThreadLocal实例为key的键值对 m.remove(this);
}
}
总结一下remove方法:通过当前线程 thread 实例获取到它所维护的 ThreadLocalMap,如果map不为null,则从 map 中删除该 ThreadLocal 实例为 key 的键值对即可。
四、ThreadLocalMap详解
从上面的分析我们已经知道,数据其实都放在了 ThreadLocalMap 中,ThreadLocal 的 get、set 和 remove 方法实际上都是通过 ThreadLocalMap 的 getEntry、set 和 remove 方法实现的。如果想真正全方位的弄懂 ThreadLocal,就必须搞懂ThreadLocalMap。
ThreadLocalMap 是 ThreadLocal 一个静态内部类,和大多数容器一样,内部维护了一个数组(Entry 类型的 table 数组)。
private Entry[] table;
接下来看下 Entry 是什么:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
Entry 是一个以 ThreadLocal 为 key,Object 为 value 的键值对,另外需要注意的是这里的ThreadLocal 是弱引用,因为 Entry 继承了 WeakReference,在 Entry 的构造方法中,调用了 super(k)方法,会将 ThreadLocal 实例包装成一个 WeakReferenece。
五、为什么使用弱引用,而不用强引用
- 当key为强引用时,引用 ThreadLocal 的对象置为null,但是ThreadLocalMap中的key还强引用着ThreadLocal,那么ThreadLocal就不能被回收,最后导致内存泄漏。
- 当key为软引用时,引用 ThreadLocal 的对象置为null,由于ThreadLocalMap的key持有的是ThreadLocal实例的弱引用,弱引用是可以被gc回收掉的,一旦回收是不是就会发生内存泄漏,但是我们在下一次调用get、set、remove方法时,它内部会调用expungeStaleEntryfan方法的,该方法的主要作用是确保
ThreadLocal
实例中的引用保持最新,防止内存泄漏。注意:set、get方法不一定调用expungeStaleEntryfan方法,但是remove一定会调用
所以可以得出使用弱引用可以多一层保障
六、ThreadLocal会引发内存泄漏
内存泄漏是指对象变成了垃圾,垃圾回收器回收不了。
接下来我们使用一张图来看看Thread、ThreadLocal、ThreadMap、Entry之间的关系
注意上图中的实线表示强引用,虚线表示弱引用。Entry 中的 key 是弱引用,当 ThreadLocal 外部强引用被置为 null( ThreadLocalInstance=null )时,那么系统 GC 的时候,根据可达性分析,这个 ThreadLocal 实例就没有任何一条链路能够引用到它,此时 ThreadLocal 势必会被回收,这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry,就没有办法访问这些 key 为 null 的 Entry 的 value,如果当前线程再迟迟不结束的话,这些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value 永远无法回收,造成内存泄漏。
当然,如果当前 thread 运行结束,ThreadLocal、ThreadLocalMap、Entry 没有引用链可达,在垃圾回收的时候都会被系统回收。
七、如何避免内存泄漏呢?
使用 ThreadLocal 后,及时调用其 remove 方法,手动清理不再需要的 entry。这样可以确保在外部对象不再需要时,ThreadLocalMap 中的引用也被正确释放。
八、ThreadLocal和Synchronized有什么区别
Synchronized是通过锁的机制,来保证线程安全的
ThreadLocal是为每个线程提供了变量副本,这是一种“空间换时间”的方案,虽然会让内存占用大很多,但是由于不需要同步也就减少了线程可能存在的阻塞等待,从而提高时间效率。