多线程访问同一个共享变量时特别容易出现并发问题,特别是在多个线程需要对一个 共享变量进行写入时。为了保证线程安全,一般使用者在访问共享变量时需要进行适当的同步同步的措施一般是加锁,这就需要使用者对锁有一定的了解,这显然加重了使用者的 负担。那么有没有一种方式可以做到,当创建一个变量后,每个线程对其进行访问的时候访问的是自己线程的变量呢?其实 ThreadLocal就可以做这件事情,虽然 ThreadLocal并不是为了解决这个问题而出现的。
ThreadLocal是JDK lang包提供的,它提供了线程本地变量,也就是如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量时,实际操作的是自己本地内存里面的变量,从而避免了线程安全问题。创建一个ThreadLocal变量后,每个线程都会复制一个变量到自己的本地内存。
首先看一下ThreadLocal的类图,如下:
从该类图可以看到,在Thread类中有一个theradLocals和inheritableThreadLocals的map结构属性,都是ThreadLocalMap类型,可以理解为一个定制化的HashMao。
默认情况下,每个线程的这俩属性都为null。只有当线程第一次调用ThreadLocal中的set或者get方法时才会创建他们。
这里有个重点就是每个线程的本地变量不是存放在ThreadLocal实例里面的,而是存放在调用线程的threadLocals变量里面。所以可以把ThreadLocal理解成一个工具类。
ThreadLocal工具类在调用set的时候将value存放在调用线程的threadLocals里面。当调用线程调用ThreadLocal的get 方法是,则用threadLocals里面将其拿出来。如果线程一直不终止,这个本地变量也会一直存放在调用线程的threadLocals里面,所以不需要的时候要记得remove掉。
threadLocals和inheritableThreadLocals都是是Map结构说明每个线程可以存放多个本地变量。
ThreadLocalMap
ThreadLocal的内部类,其中的内部类Entry继承了WeakReference(弱引用)
弱引用相关和Threadlocal中Entry为什么使用弱引用的相关链接:
https://www.cnblogs.com/gudi/p/6403953.html
https://www.jianshu.com/p/964fbc30151a
https://www.cnblogs.com/zjj1996/p/9140385.html
https://blog.csdn.net/qq_42862882/article/details/89820017?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.control&dist_request_id=1328602.9405.16149115671133691&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.control
首先弱引用就是在gc的时候去回收。如上图,在栈中存在ThreadLocal的引用,图中的虚线部分为弱引用,
反证一下:如果虚线部分也是实线,也就是强引用,当栈中的ThreadLocalRef = null后,ThreadLocal 当前线程的threadLocalMap中还是有强引用,就回收不了了,导致内存泄露,所以将key设置为了弱引用。
假如每个key都强引用指向threadlocal,也就是上图虚线那里是个强引用,那么这个threadlocal就会因为和entry存在强引用无法被回收!造成内存泄漏 ,除非线程结束,线程被回收了,map也跟着回收。
虽然上述的弱引用解决了key,也就是线程的ThreadLocal能及时被回收,但是value却依然存在内存泄漏的问题。
当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收.
map里面的value却没有被回收.而这块value永远不会被访问到了. 所以存在着内存泄露,因为存在一条从current thread连接过来的强引用.
只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收。所以当线程的某个localThread使用完了,马上调用threadlocal的remove方法,那就啥事没有了!
另外其实只要这个线程对象及时被gc回收,这个内存泄露问题影响不大,但在threadLocal设为null到线程结束中间这段时间不会被回收的,就发生了我们认为的内存泄露。
最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的。就可能出现内存泄露。
最后补充一点:Java为了最小化减少内存泄露的可能性和影响,在ThreadLocal的get,set的时候都会清除线程Map里所有key为null的value。
set
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 将当前线程传入获取threadLocals,获取的是当前线程自身的map
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
// 第一次条用就床架您当前线程对应的threadLocals
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
getMap(t)的作用是获取线程自己的变量threadLocals, ThreadLocal变量被绑定到了线程的成员变量上。
如果getMap()的返回值不为空,则把 value值设置到 threadLocals中,也就是把当前变量值放入当前线程的内存变量threadLocals中。
threadLocals是一个 Hashmap结构,其中key就是当前ThreadLocal的实例对象引用, value是通过set方法传递的值。
如果getMap(t)返回空值则说明是第一次调用set方法,这时创建当前线程的threadlocals变量。
createMap(t,firstValue)创建了当前线程的ThreadLocals变量。
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程的threadLocals map
ThreadLocalMap map = getMap(t);
if (map != null) {
// 如果map已经初始化了,传入Threadlocal这个本地线程变量(set的时候this为key) 取出value
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 如果threadLocals为null,则初始化
return setInitialValue();
}
private T setInitialValue() {
T value = initialValue(); // null
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
如果threadLocals不为null,删除ThradLocal实例的本地变量。
在每个线程内部都有一个名为threadLocals的成员变量,该变 量的类型为Hashmap,其中key为我们定义的ThreadLocal变量的this引用, value则为我 们使用set方法设置的值。每个线程的本地变量存放在线程自己的内存变量threadLocals中,如果当前线程一直不消亡,那么这些本地变量会一直存在,所以可能会造成内存溢出,因此使用完毕后要记得调用ThreadLocal的 remove方法删除对应线程的threadlocals中的本地变量。
ThreadLocal不支持继承性,比如说父线程中的ThreadLcoal在其子线程中时不可见的。因为在子线程里面调用get方法时是获取当前线程的threadLocals对象,所以获取不到父线程的本地变量是正常的。
为了解决上述问题,出现了InheritableThreadLocal这个类。
InheritableThreadLocal继承了ThreadLocal类,并提供了一个特性,就是让子线程可以访问在父线程中设置的本地变量。
protected T childValue(T parentValue) {
return parentValue;
}
ThreadLocalMap getMap(Thread t) {
// 返回的是inheritableThreadLocals
return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
// 创建的是inheritableThreadLocals而不是threadLocals
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
InheritableThreadLocal继承了ThreadLocal ,并重写了三个方法。
在Thread的构造器中有这样一段代码:
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
...
// 获取当前线程,也就是父线程
Thread parent = currentThread();
...
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
// 如果父线程的inheritableThreadLocals 不为null,则将父线程的inheritableThreadLocals也赋值为子线程
this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}
追踪createInheritedMap可以看到这个方法:
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
// 这里调用了重写的方法,获取父线程中的value
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
所以将ThreadLocal实例初始化为InheritableThreadLocal就拥有了继承性,在子类连就可以调用父类的本地变量了
那么在什么情况下需要子线程可以获取父线程的 threadlocal变量呢?
情况还是蛮多的,比如子线程需要使用存放在 threadlocal变量中的用户登录信息,再比如一些中间件需要把统一的id追踪的整个调用链路记录下来。
其实子线程使用父线程中的 threadlocal方法有多种方式,比如创建线程时传入父线程中的变量,并将其复制到子线程中,或者在父线程中构造一个map作为参数传递给子线程,但是这些都改变了我们的使用习惯,所以在这些情况下InheritableThreadlocal就显得比较有用。
本文学习了总结了ThreadLocal的设计初衷和实现原理,并介绍了InheritableThreadLocal实现了本地变量的可继承性,也简单介绍了弱引用等知识点。
参考资料:《java并发编程之美》
by – 俩只猴