(总结)Java多线程_ThreadLocal
使用方式
使用ThreadLocal类来定义线程内部的共享变量,在多线程环境下,可以保证各个线程之间的变量互相隔离、相互独立。ThreadLocal实例通常来说都是private static类型的,它们希望将状态与线程进行关联。这种变量在线程的生命周期内起作用,可以减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。
通过set()/get()方法来进行变量值的设定和取出;
线程中可以创建多个ThreadLocal变量,存储在ThreadLocalMap中;
使用示例如下
public class Test1 {
public static void main(String[] args) {
ThreadLocal<Integer> threadLocal1 = new ThreadLocal<>();
ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>();
Thread threadA = new Thread(new MyThread(),"ThreadA");
Thread threadB = new Thread(new MyThread(),"ThreadB");
threadA.start();
threadB.start();
}
}
class MyThread implements Runnable {
ThreadLocal<Integer> threadLocal1 = new ThreadLocal<>();
ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>();
@Override
public void run() {
for(int i =0 ;i<3;i++) {
threadLocal1.set(i);
threadLocal2.set(i);
System.out.println(Thread.currentThread().getName()+" - threadLocal1.get() - "+
threadLocal1.get());
System.out.println(Thread.currentThread().getName()+" - threadLocal2.get() - "+
threadLocal2.get());
}
}
}
result:
ThreadB - threadLocal1.get() - 0
ThreadA - threadLocal1.get() - 0
ThreadB - threadLocal2.get() - 0
ThreadA - threadLocal2.get() - 0
ThreadB - threadLocal1.get() - 1
ThreadA - threadLocal1.get() - 1
ThreadB - threadLocal2.get() - 1
ThreadA - threadLocal2.get() - 1
ThreadB - threadLocal1.get() - 2
ThreadA - threadLocal1.get() - 2
ThreadB - threadLocal2.get() - 2
ThreadA - threadLocal2.get() - 2
ThreadLocal源码
1.构造方法:
public ThreadLocal() {
}
只有无参构造
2.set():
public void set(T value) {
Thread t = Thread.currentThread(); //获取当前线程
ThreadLocalMap map = getMap(t); //获取当前线程中的ThreadLocalMap
/**
* 当前线程中的ThreadLocalMap不为空时,
* 存入this(当前的ThreadLocal对象),如上:因为调用时使用threadLocal1.set(i)
*/
if (map != null)
map.set(this, value);
/**
* 当前线程中的ThreadLocalMap为空时,
* 创建ThreadLocalMap
*/
else
createMap(t, value);
}
//发现调用了getMap()方法,我们来看一下:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
//看看threadLocals是什么,发现是Thread类下的变量,属于ThreadLocal下的静态类ThreadLocalMap
class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
}
3.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;
}
}
return setInitialValue();
}
4.ThreadLocalMap:
- ThreadLocalMap是用来存储与线程关联的value的哈希表,它具有HashMap的部分特性,比如容量、扩容阈值等,它内部通过Entry类来存储key和value;
- Entry继承自WeakReference,通过上述源码super(k);可以知道,ThreadLocalMap是使用ThreadLocal的弱引用作为Key的;
- 分析到这里,我们可以得到下面这个对象之间的引用结构图(其中,实线为强引用,虚线为弱引用):
- ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部关联的强引用,那么在虚拟机进行垃圾回收时,这个ThreadLocal会被回收,这样,ThreadLocalMap中就会出现key为null的Entry,这些key对应的value也就再无妨访问,但是value却存在一条从Current Thread过来的强引用链。因此只有当Current Thread销毁时,value才能得到释放。
- 因此,只要这个线程对象被gc回收,那些key为null对应的value也会被回收,这样也没什么问题,但在线程对象不被回收的情况下,比如使用线程池的时候,核心线程是一直在运行的,线程对象不会回收,若是在这样的线程中存在上述现象,就可能出现内存泄露的问题。
- ThreadLocalMap中是如何解决方法:
- 获取key对应的value时,会调用ThreadLocalMap的getEntry(ThreadLocal<?> key)方法
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.threadLocalHashCode & (table.length - 1)来计算存储key的Entry的索引位置,然后判断对应的key是否存在,若存在,则返回其对应的value,否则,调用getEntryAfterMiss(ThreadLocal<?>, int, Entry)方法;
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; }
- ThreadLocalMap采用线性探查的方式来处理哈希冲突,所以会有一个while循环去查找对应的key,在查找过程中,若发现key为null,即通过弱引用的key被回收了,会调用expungeStaleEntry(int)方法,清理与key对应的value以及Entry;
- 此时,CurrentThread Ref不存在一条到Entry对象的强引用链,Entry到value对象也不存在强引用,那在程序运行期间,它们自然也就会被回收。expungeStaleEntry(int)方法的后续代码就是以线性探查的方式,调整后续Entry的位置,同时检查key的有效性。
- 但是如果我们既不需要添加value,也不需要获取value,那还是有可能产生内存泄漏的。所以很多情况下需要使用者手动调用ThreadLocal的remove()函数,手动删除不再需要的ThreadLocal,防止内存泄露。若对应的key存在,remove()方法也会调用expungeStaleEntry(int)方法,来删除对应的Entry和value。