作者@陈明兄弟
ThreadLoacal是什么?ThreadLocal是什么?以前面试别人的时候会问这个问题。有些伙伴喜欢把它和线程同步机制混为一谈。其实ThreadLocal和线程同步没有任何关系。ThreadLocal虽然为多线程环境下的成员变量问题提供了解决方案,但并不是多线程共享变量问题的解决方案。那么ThreadLocal到底是什么呢?
API是这样介绍的:
这个类提供线程局部变量。这些变量不同于正常的对应变量,因为每个访问一个变量的线程(通过其{@code get}或{@code set}方法)都有自己独立初始化的变量副本。{@code ThreadLocal}实例通常是类中的私有静态字段,希望将状态与线程相关联(例如,用户标识或事务标识)。
这个类提供线程局部变量。这些变量不同于普通的对应变量,因为每个访问变量的线程(通过其get或set方法)都有自己的局部变量,它独立于变量的初始化副本。线程本地实例通常是类中的私有静态字段,用于将状态与线程相关联(例如,用户标识或事务标识)。
所以ThreadLocal与线程同步机制不同,线程同步机制是多个线程共享同一个变量,而ThreadLocal为每个线程创建一个单独的变量副本,这样每个线程就可以独立地更改自己的变量副本,而不影响其他线程对应的副本。可以说ThreadLocal为多线程环境下的变量问题提供了另一种解决方式。
ThreadLocal定义了四种方法:
get():返回此线程局部变量的当前线程副本中的值。initialValue():返回此线程局部变量的当前线程的“初始值”。remove():移除此线程局部变量当前线程的值。set(T value):将此线程局部变量的当前线程副本中的值设置为指定值。除了这四种方法,threadlocal内部还有一个静态的内部类ThreadLocalmap,这是实现线程隔离机制的关键。get(),set()和remove()都是基于这个内部类操作的。ThreadLocalMap提供了一种方法,将每个线程的变量副本存储在键值对中,其中key是当前ThreadLocal对象,value是相应线程的变量副本。
关于ThreadLocal有两点需要注意:
ThreadLocal实例本身是不存储值,它只是提供了一个在当前线程中找到副本值得key。是ThreadLocal包含在Thread中,而不是Thread包含在ThreadLocal中,有些小伙伴会弄错他们的关系。下图是Thread、ThreadLocal、ThreadLocalMap之间的关系(http://blog . xiaohansong . com/2016/08/06/Thread local-memory-leak/)
ThreadLocal用法示例
例子如下:
public class SeqCount {private static ThreadLocal<Integer> seqCount = new ThreadLocal<Integer>(){// 实现initialValue()public Integer initialValue() {return 0;}};public int nextSeq(){seqCount.set(seqCount.get() + 1);return seqCount.get();}public static void main(String[] args){SeqCount seqCount = new SeqCount();SeqThread thread1 = new SeqThread(seqCount);SeqThread thread2 = new SeqThread(seqCount);SeqThread thread3 = new SeqThread(seqCount);SeqThread thread4 = new SeqThread(seqCount);thread1.start();thread2.start();thread3.start();thread4.start();}private static class SeqThread extends Thread{private SeqCount seqCount;SeqThread(SeqCount seqCount){this.seqCount = seqCount;}public void run() {for(int i = 0 ; i < 3 ; i++){System.out.println(Thread.currentThread().getName() + " seqCount :" + seqCount.nextSeq());}}}}运行结果:
从运行结果可以看出,ThreadLocal确实可以实现线程隔离机制,保证变量的安全性。在这里,我们想问一个问题。在上面的代码中,ThreadLocal的initialValue()方法返回0。如果添加了此方法,它将返回一个对象。会怎么样?例如:
A a = new A();private static ThreadLocal<A> seqCount = new ThreadLocal<A>(){// 实现initialValue()public A initialValue() {return a;}};class A{// ....}具体流程请参考:关于ThreadLocal实现原理的一些思考
线程本地源代码分析
虽然ThreadLocal解决了这个多线程变量的复杂问题,但是它的源代码实现相对简单。ThreadLocalMap是实现线程本地的关键,就从它开始吧。
ThreadLocalMap
线程本地映射使用条目来存储键值,如下所示:
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,value就是值。同时Entry也继承了WeakReference,所以Entry对应的key(ThreadLocal实例)的引用是弱引用(弱引用我这里就不多说了,有兴趣的可以关注一下这个博客:Java理论与实践:用弱引用阻断内存泄漏)
ThreadLocalMap的源代码稍微多一点,所以我们来看两个核心方法:getEntry()和set(ThreadLocal key,Object value)。
设置(线程本地键,对象值)
private void set(ThreadLocal<?> key, Object value) {ThreadLocal.ThreadLocalMap.Entry[] tab = table;int len = tab.length;// 根据 ThreadLocal 的散列值,查找对应元素在数组中的位置int i = key.threadLocalHashCode & (len-1);// 采用“线性探测法”,寻找合适位置for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {ThreadLocal<?> k = e.get();// key 存在,直接覆盖if (k == key) {e.value = value;return;}// key == null,但是存在值(因为此处的e != null),说明之前的ThreadLocal对象已经被回收了if (k == null) {// 用新元素替换陈旧的元素replaceStaleEntry(key, value, i);return;}}// ThreadLocal对应的key实例不存在也没有陈旧元素,new 一个tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value);int sz = ++size;// cleanSomeSlots 清楚陈旧的Entry(key == null)// 如果没有清理陈旧的 Entry 并且数组中的元素大于了阈值,则进行 rehashif (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();}这个set()操作和我们在集合中知道的put()方式有点不一样,虽然都是键值结构,不同的是它们解决哈希冲突的方式不同。Set Map的put()采用拉链方式,ThreadLocalMap的Set()采用开放寻址方式(详见哈希冲突处理博客)。掌握开放地址法,一目了然。
set()操作除了存储元素外,还有一个非常重要的功能,就是replaceStaleEntry()和cleanSomeSlots()。这两种方法可以清除key == null的实例,防止内存泄漏。set()方法中还有一个重要的变量:threadLocalHashCode,定义如下:
private final int threadLocalHashCode = nextHashCode();从名字可以看出,threadLocalHashCode应该是ThreadLocal的哈希值,定义为final,也就是说ThreadLocal的哈希值一旦创建就已经确定了,生成过程是调用nextHashCode():
private static AtomicInteger nextHashCode = new AtomicInteger();private static final int HASH_INCREMENT = 0x61c88647;private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);}NextHashCode表示分配下一个ThreadLocal实例的ThreadLocal hashcode的值,HASH _ INDENTATION表示分配两个threadlocal实例的threadLocalHashCode的增量。他们的定义可以从nextHashCode看到。
getEntry()
private Entry getEntry(ThreadLocal<?> key) {int i = key.threadLocalHashCode & (table.length - 1);Entry e = table[i];if (e != null && e.get() == key)return e;elsereturn getEntryAfterMiss(key, i, e);}由于采用开放寻址方式,当前键的哈希值与数组中元素的索引不完全对应。首先取一个探测号(key的哈希值),如果对应的key就是我们要找的元素,就返回;否则,调用getEntryAfterMiss(),如下所示:
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);elsei = nextIndex(i, len);e = tab[i];}return null;}这里有一点很重要。当key == null时,调用expungeStaleEntry()方法。该方法用于处理key == null,有利于GC恢复,可以有效避免内存泄漏。
get()
返回当前线程对应的线程变量
public T get() {// 获取当前线程Thread t = Thread.currentThread();// 获取当前线程的成员变量 threadLocalThreadLocalMap map = getMap(t);if (map != null) {// 从当前线程的ThreadLocalMap获取相对应的EntryThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")// 获取目标值T result = (T)e.value;return result;}}return setInitialValue();}首先通过当前线程获取对应的成员变量ThreadLocalMap,然后通过ThreadLocalMap获取当前线程本地的Entry,最后通过获取的Entry获取目标值结果。
getMap()方法可以获取当前线程对应的ThreadLocalMap,如下所示:
ThreadLocalMap getMap(Thread t) {return t.threadLocals;} set(T value)设置当前线程的线程局部变量的值。
public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);}获取当前线程对应的ThreadLocalMap。如果不是空,则调用ThreadLocalMap的set()方法,键为当前ThreadLocal。如果它不存在,请调用createMap()方法创建一个新的,如下所示:
void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);} initialValue()返回线程局部变量的初始值。
protected T initialValue() {return null;}该方法被定义为受保护级别并返回null,这显然是由子类实现的,所以我们在使用ThreadLocal时一般应该覆盖这个方法。方法不能显示调用,只在第一次调用get()或set()方法时执行,只执行一次。
移除()
删除当前线程的局部变量值。
public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null)m.remove(this);}这种方法的目的是减少内存占用。当然,我们不需要调用这个方法,因为一个线程结束后,它对应的局部变量会被垃圾收集。
为什么ThreadLocal会泄漏内存
如上所述,每个线程都有一个线程本地映射。ThreadLocalMap,这个映射的键是ThreadLocal instance,是弱引用。我们知道弱参考有利于GC回收。当ThreadLocal的key == null时,GC将回收此部分空,但值可能不会被回收,因为它与Current Thread也有很强的引用关系,如下(图片来自http://www . jiansu . com/p/ee8 c 9 dccc 953):
由于这种强引用关系,该值无法回收。如果线程对象没有被破坏,强引用关系就会一直存在,就会出现内存泄漏。所以只要这个线程对象能被GC及时回收,就不会有内存泄漏。如果碰到线程池,那就更惨了。
那么如何才能避免这个问题呢?
如前所述,如果在ThreadLocalMap中的setEntry()和getEntry()中遇到key == null,则该值将被设置为null。当然,我们也可以调用ThreadLocal的remove()方法进行处理。
以下是ThreadLocal的简要概述:
ThreadLocal 不是用于解决共享变量的问题的,也不是为了协调线程同步而存在,而是为了方便每个线程处理自己的状态而引入的一个机制。这点至关重要。 每个Thread内部都有一个ThreadLocal.ThreadLocalMap类型的成员变量,该成员变量用来存储实际的ThreadLocal变量副本。 ThreadLocal并不是为线程保存对象的副本,它仅仅只起到一个索引的作用。它的主要木得视为每一个线程隔离一个类的实例,这个实例的作用范围仅限于线程内部。小游戏推荐:
如果你喜欢这篇文章,请分享和赞美它
点“好看”
↓↓↓↓
1.《threadlocal 面试必会必知:深入分析 ThreadLocal》援引自互联网,旨在传递更多网络信息知识,仅代表作者本人观点,与本网站无关,侵删请联系页脚下方联系方式。
2.《threadlocal 面试必会必知:深入分析 ThreadLocal》仅供读者参考,本网站未对该内容进行证实,对其原创性、真实性、完整性、及时性不作任何保证。
3.文章转载时请保留本站内容来源地址,https://www.lu-xu.com/caijing/1217221.html