当前位置:首页 > 财经

threadlocal 面试必会必知:深入分析 ThreadLocal

作者@陈明兄弟

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

上一篇

经常鼻塞怎么回事 老是鼻塞该怎么办?

下一篇

北京到兰州 定了!北京到兰州高铁7月1日开通!9小时直达兰州,丝绸之路就在沿途!

复方玄驹胶囊的作用与功效 用药须知 :复方玄驹胶囊用于壮阳的方法是怎样呢?

复方玄驹胶囊的作用与功效 用药须知 :复方玄驹胶囊用于壮阳的方法是怎样呢?

复方玄驹胶囊的主要成分是黑蚂蚁、淫羊藿、枸杞和蛇床子。那么,复方玄驹胶囊壮阳的方法是什么呢?  复方玄驹胶囊是一种苦、微麻、味甘的胶囊。复方玄驹胶囊中的淫羊藿对垂体后叶素诱导的大鼠心肌缺血有一定的保护作用。复方玄驹胶囊里的黑蚂蚁,咸扁有毒。复方...

复方玄驹胶囊怎么样 用药须知 :复方玄驹胶囊用于壮阳的方法是怎样呢?

复方玄驹胶囊怎么样 用药须知 :复方玄驹胶囊用于壮阳的方法是怎样呢?

复方玄驹胶囊的主要成分是黑蚂蚁、淫羊藿、枸杞和蛇床子。那么,复方玄驹胶囊壮阳的方法是什么呢?  复方玄驹胶囊是一种苦、微麻、味甘的胶囊。复方玄驹胶囊中的淫羊藿对垂体后叶素诱导的大鼠心肌缺血有一定的保护作用。复方玄驹胶囊里的黑蚂蚁,咸扁有毒。复方...

复方玄驹胶囊治阳痿 用药须知 :复方玄驹胶囊用于壮阳的方法是怎样呢?

复方玄驹胶囊治阳痿 用药须知 :复方玄驹胶囊用于壮阳的方法是怎样呢?

复方玄驹胶囊的主要成分是黑蚂蚁、淫羊藿、枸杞和蛇床子。那么,复方玄驹胶囊壮阳的方法是什么呢?  复方玄驹胶囊是一种苦、微麻、味甘的胶囊。复方玄驹胶囊中的淫羊藿对垂体后叶素诱导的大鼠心肌缺血有一定的保护作用。复方玄驹胶囊里的黑蚂蚁,咸扁有毒。复方...

复方玄驹胶囊效果怎么样 用药须知 :复方玄驹胶囊用于壮阳的方法是怎样呢?

复方玄驹胶囊效果怎么样 用药须知 :复方玄驹胶囊用于壮阳的方法是怎样呢?

复方玄驹胶囊的主要成分是黑蚂蚁、淫羊藿、枸杞和蛇床子。那么,复方玄驹胶囊壮阳的方法是什么呢?  复方玄驹胶囊是一种苦、微麻、味甘的胶囊。复方玄驹胶囊中的淫羊藿对垂体后叶素诱导的大鼠心肌缺血有一定的保护作用。复方玄驹胶囊里的黑蚂蚁,咸扁有毒。复方...

处方药招商 处方药药店销售,最新式的策略方法

在药房会员制营销中,慢性病患者的数据最有价值,处方药占大多数。 如何做好处方药会员营销,O2O营销,建立自己的私域电商流量? 药店、诊所、DTP、O2O、CRM(会员销售)等渠道如何销售和消费? 在此背景下,赛博创计划于6月29日至30日在成都...

康乃馨的养殖方法 康乃馨的养护,还是需要花点心思的哟!

  • 康乃馨的养殖方法 康乃馨的养护,还是需要花点心思的哟!
  • 康乃馨的养殖方法 康乃馨的养护,还是需要花点心思的哟!
  • 康乃馨的养殖方法 康乃馨的养护,还是需要花点心思的哟!

最新手机来电铃声 最新iPhone 更换铃声方法,纯手机更换手机来电铃声

  • 最新手机来电铃声 最新iPhone 更换铃声方法,纯手机更换手机来电铃声
  • 最新手机来电铃声 最新iPhone 更换铃声方法,纯手机更换手机来电铃声
  • 最新手机来电铃声 最新iPhone 更换铃声方法,纯手机更换手机来电铃声

手机来电铃音 最新iPhone 更换铃声方法,纯手机更换手机来电铃声

  • 手机来电铃音 最新iPhone 更换铃声方法,纯手机更换手机来电铃声
  • 手机来电铃音 最新iPhone 更换铃声方法,纯手机更换手机来电铃声
  • 手机来电铃音 最新iPhone 更换铃声方法,纯手机更换手机来电铃声