关于bootstrap:深入浅出ThreadLocal

7次阅读

共计 4362 个字符,预计需要花费 11 分钟才能阅读完成。

ThreadLocal 置信大家都有用过的,个别用作存取一些全局的信息。比方用户信息,流程信息,甚至在 Spring 框架外面通过事务注解 Transactional 去获取数据库连贯的实现上,也有它的一份功绩。
ThreadLocal 作为一个进阶必会知识点,而且还是面试高频考点。网上博客对它的解读也必然不会少,然而网上博客解读程度参差不齐,看多了难免会绕。不如本人亲自再梳理一遍,顺便记录下本人的解读。
ThreadLocal 的线程隔离性 Demo
先来看一个小的 demo
static ThreadLocal<Student> threadLocal = new ThreadLocal<Student>();

public static void main(String[] args) {

threadLocalTest1();

}

private static void threadLocalTest1() {

new Thread(new Runnable() {public void run() {
        try {TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {e.printStackTrace();
        }
    System.out.println(threadLocal.get());
    }
}).start();


new Thread(new Runnable() {public void run() {
        try {TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {e.printStackTrace();
        }
        threadLocal.set(new Student("zhangsan"));
    }
}).start();

}
复制代码
如代码所示,开启两个线程。
第一个线程 3 秒之后去取在动态变量 threadLocal 里的变量。
第二个线程 1 秒之后去设置 threadLocal 里的变量。
这段代码运行的后果就是,第一个线程永远获取不到第二个线程给动态变量 threadLocal 里设置的变量。
论断:不同的线程操作同一个 threadLocal 对象,能够实现线程信息之间的隔离。
猜测:看到 set 办法和 get 办法,大胆猜测 threadLocal 对象外面有个 Map,key 为以后线程,value 为 ThreadLocal 泛型里的对象,这样就实现了在空间上的线程安全性。
但事实并不是这样,答案不在猜测中,而在源码中。
ThreadLocal.set()办法源码解读
public void set(T value) {

// 获取以后线程
Thread t = Thread.currentThread();
// 获取以后线程的 ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
    // 把以后 ThreadLocal 对象作为 key,调用 set 办法的入参对象作为 value,放入以后线程的 ThreadLocalMap
    map.set(this, value);
else
    // 通过以后线程和调用 set 办法的入参对象去结构 Map
    createMap(t, value);

}
复制代码
ThreadLocal.get() 办法源码解读
public T get() {

// 获取以后线程
Thread t = Thread.currentThread();
// 获取以后线程的 ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
// 通过以后 ThreadLocal 对象去取 Entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {@SuppressWarnings("unchecked")
    // 获取 Entry 的 value 返回
    T result = (T)e.value;
    return result;
}
}
return setInitialValue();

}
复制代码
那么 ThreadLocalMap 又是什么呢?
再点进源码,你会发现,ThreadLocalMap 是 ThreadLocal 的一个动态外部类,同时在 Thread 类下有一个成员变量 ThreadLocals
ThreadLocal.ThreadLocalMap threadLocals = null
复制代码
那咱们顺着逻辑再上来看看 ThreadLocalMap 的 set 办法用来干嘛的。
ThreadLocalMap.set()办法源码解读
private void set(ThreadLocal<?> key, Object value) {

Entry[] tab = table;
// 取长度
int len = tab.length;
// 算下标
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;
    }
}
// 设置 Entry
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
    rehash();

}
复制代码
其实咱们在晓得了 HashMap 的底层原理实现的根底下来了解上述代码并不难,取 Entry 数组长度,哈希,与运算取模,设置 Entry 零打碎敲。
然而这里须要留神的是,也是 ThreadLocal 最有特色的一点,是这个 Entry 并不是一般了解里的 Entry,而是 ThreadLocalMap 外面的一个动态外部类并且继承了 WeakReference
static class Entry extends WeakReference<ThreadLocal<?>> {

Object value;

Entry(ThreadLocal<?> k, Object v) {super(k);
    value = v;
}

}
复制代码
到此,是不是对几个带有 Thread 的名词弄的有点晕,劳动一下,咱们先来看张图梳理一下 ThreadLocal,ThreadLocalMap,与 Thread 的关系,如图:

层层深刻,这时候问题又来了,弱援用是啥?啥是弱援用?弱什么?什么援用?
WeakReference 演示
public static void main(String[] args) {

wrTest();

}

private static void wrTest() {

WeakReference<Student> weakReference = new WeakReference<Student>(new Student("aaaa"));
System.out.println(weakReference.get());
System.gc();
System.out.println(weakReference.get());

}
复制代码
输入的后果为:
Student@1b6d3586
null
复制代码
在这个 demo 外面,咱们能够很清晰的看到弱援用的个性:当 JVM 进行垃圾回收时,无论内存是否短缺,都会回收被弱援用关联的对象。
当垃圾回收器执行一次之后,原来的弱援用关联的对象就为 null 了,它领有这样的特质,又有什么用呢?
这就得引出内存透露的问题了。
内存透露
应用 ThreadLocal 哪些状况会产生内存透露?
调用 ThreadLocal 的 set 办法设置某个对象进去,起初这个对象回收不了。长此以往,影响程序运行速度,最终造成 OOM。
为什么会回收不了?是因为垃圾回收器执行之后,CurrentThread 仍然运行的前提下,Entry[]始终存在,然而其中有些 key 因为是继承了 WeakReference,在 GC 之后其 get 办法返回值就是 null 了,导致取不到 Entry 外面 key.get()为 null 的 KEY 所对应的 value,而这块 value 永远也拜访不到了。如图:

应用 ThreadLocal 如何防止内存透露?
把 ThreadLocal 定义为 static,放弃单例,不被回收。
用完 ThreadLocal,须要手动擦除对应的 Entry 节点信息,记得调用 ThreadLocal 的 remove 办法。
特地是在理论我的项目的场景下,大多数状况下线程都是交给线程池在治理。一个线程工作跑完,通常不会立刻销毁,而是放在线程池外面期待下次工作的降临(有种说法说是在把线程放回线程池的过程中会擦除 Thread 下的 ThreadLocal.ThreadLocalMap threadLocals 信息,当然,这是线程池帮咱们做的)无论线程池是否帮咱们擦除,咱们用完 ThreadLocal 手动 remove 总是平安的。
附上 remove 的源码。
public void remove() {

// 获取以后线程的 ThreadLocalMap
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null){m.remove(this);
}

}

private void remove(ThreadLocal<?> key) {

Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
     // 匹配 key
     if (e.get() == key) {
     // 革除一个 Entry 元素
     e.clear();
     expungeStaleEntry(i);
     return;
    }
}

}
复制代码
总结
解读完 ThreadLocal 的源码,再回归到它的命名,了解又深了一个档次:
Thread + Local = 线程 + 本地 = 线程本地变量 = 把某个对象放在了线程本地。
文理不分家,无妨借用理科的思维打比方去了解它:
ThreadLocal 对象就像一个具体的主观的对象,能够是某个话题,某部电影,某本书,甚至某个人。
而每个 Thread 就像一个人,读者,旁观者。
Thread 对 ThreadLocal 的 set 操作和 get 操作,就别离对应是一个人对某个主观的对象进行设置主观印象和获取主观印象。即使是同一个对象,不同的人会对其有不同的主观印象并记录在本人的脑海里,在每个人来看这些印象都是正当的,无论你处在哪个上下文,总能疾速获取到这个印象信息,而不会错乱。
在空间上保留对象的正本,通过空间换工夫的思维,也就实现了 ThreadLocal 的线程安全性。

正文完
 0