更多内容,访问 IT-BLOG

ThreadLocal 并不是一个Thread,而是 ThreadLocalVariable(线程局部变量)。也许把它命名为 ThreadLocalVar更加合适。线程局部变量就是为每一个使用该变量的线程都提供一个变量值的副本,是 Java中一种较为特殊的线程绑定机制,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。ThreadLocal是除了加锁这种同步方式之外的另一种保证多线程访问出现线程不安全的方式。

从线程的角度看,每个线程都保持一个对其线程局部变量副本的隐式引用,只要线程是活动的,ThreadLocal实例就是可访问的;在线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)。

通过ThreadLocal存取的数据,总是与当前线程相关,也就是说,JVM 为每个运行的线程,绑定了私有的本地实例存取空间,从而为多线程环境常出现的并发访问问题提供了一种隔离机制。在 ThreadLocal类中有一个 Map,用于存储每一个线程变量的副本,Map中元素的键为线程对象,而值为对应线程的变量副本。ThreadLocal采用了以空间换时间 的方式。为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

一、API 说明


【1】new ThreadLocal():创建一个线程本地变量。
【2】T get():返回此线程局部变量的当前线程副本中的值,如果这是线程第一次调用该方法,则创建并初始化此副本。
【3】void set(T value):将此线程局部变量的当前线程副本中的值设置为指定值。许多应用程序不需要这项功能,它们只依赖于 initialValue() 方法来设置线程局部变量的值。
【4】void remove():移除此线程局部变量的值。这可能有助于减少线程局部变量的存储需求。如果再次访问此线程局部变量,那么在默认情况下它将拥有其 initialValue。
【5】protected  T initialValue():返回此线程局部变量的当前线程的初始值。最多在每次访问线程来获得每个线程局部变量时调用此方法一次,即线程第一次使用 get() 方法访问变量的时候。如果线程先调用 set(T) 方法再调用 get() 方法,则不会在线程中再调用 initialValue() 方法。若该实现只返回 null;如果程序员希望将线程局部变量初始化为 null 以外的某个值,则必须为 ThreadLocal 创建子类,并重写此方法。通常,将使用匿名内部类。initialValue 的典型实现将调用一个适当的构造方法,并返回新构造的对象。

在程序中一般都重写 initialValue方法,以给定一个特定的初始值。

二、ThreadLocal 简单使用


下面的例子中,开启两个线程,在每个线程内部设置了本地变量的值,然后调用 print方法打印当前本地变量的值。如果在打印之后调用本地变量的 remove方法会删除本地内存中的变量,代码如下所示:

 1 package test;
2
3 public class ThreadLocalTest {
4
5 static ThreadLocal<String> localVar = new ThreadLocal<>();
6
7 static void print(String str) {
8 //打印当前线程中本地内存中本地变量的值
9 System.out.println(str + " :" + localVar.get());
10 //清除本地内存中的本地变量
11 localVar.remove();
12 }
13
14 public static void main(String[] args) {
15 Thread t1 = new Thread(new Runnable() {
16 @Override
17 public void run() {
18 //设置线程1中本地变量的值
19 localVar.set("localVar1");
20 //调用打印方法
21 print("thread1");
22 //打印本地变量
23 System.out.println("after remove : " + localVar.get());
24 }
25 });
26
27 Thread t2 = new Thread(new Runnable() {
28 @Override
29 public void run() {
30 //设置线程1中本地变量的值
31 localVar.set("localVar2");
32 //调用打印方法
33 print("thread2");
34 //打印本地变量
35 System.out.println("after remove : " + localVar.get());
36 }
37 });
38
39 t1.start();
40 t2.start();
41 }
42 }

下面是运行后的结果:
 

三、ThreadLocal 的实现原理


下面是 ThreadLocal的类图结构,从图中可知:Thread类中有两个变量 threadLocalsinheritableThreadLocals,二者都是 ThreadLocal内部类 ThreadLocalMap类型的变量,我们通过查看内部内 ThreadLocalMap可以发现实际上它类似于一个HashMap。在默认情况下,每个线程中的这两个变量都为null。


只有当线程第一次调用 ThreadLocal的 set或者 get方法的时候才会创建他们(后面我们会查看这两个方法的源码)。除此之外,每个线程的本地变量不是存放在 ThreadLocal实例中,而是放在调用线程的 ThreadLocals变量里面(前面也说过,该变量是Thread类的变量)。也就是说,ThreadLocal类型的本地变量是存放在具体的线程空间上,其本相当于一个装载本地变量的工具壳,通过 set方法将 value添加到调用线程的 threadLocals中,当调用线程调用 get方法时候能够从它的 threadLocals中取出变量。如果调用线程一直不终止,那么这个本地变量将会一直存放在他的 threadLocals中,所以不使用本地变量的时候需要调用 remove方法将 threadLocals中删除不用的本地变量。下面我们通过查看 ThreadLocal的set、get以及remove方法来查看 ThreadLocal具体实怎样工作的。

【1】set方法源码

 1 public void set(T value) {
2 //(1)获取当前线程(调用者线程)
3 Thread t = Thread.currentThread();
4 //(2)以当前线程作为key值,去查找对应的线程变量,找到对应的map
5 ThreadLocalMap map = getMap(t);
6 //(3)如果map不为null,就直接添加本地变量,key为当前线程,值为添加的本地变量值
7 if (map != null)
8 map.set(this, value);
9 //(4)如果map为null,说明首次添加,需要首先创建出对应的map
10 else
11 createMap(t, value);
12 }

在上面的代码中,(2)处调用 getMap方法获得当前线程对应的 threadLocals(参照上面的图示和文字说明),该方法代码如下:

1 ThreadLocalMap getMap(Thread t) {
2 return t.threadLocals; //获取线程自己的变量threadLocals,并绑定到当前调用线程的成员变量threadLocals上
3 }

如果调用 getMap方法返回值不为 null,就直接将 value值设置到 threadLocals中(key为当前线程引用,值为本地变量);如果getMap方法返回 null说明是第一次调用 set方法(前面说到过,threadLocals默认值为null,只有调用 set方法的时候才会创建map),这个时候就需要调用 createMap方法创建 threadLocals,该方法如下所示:createMap方法不仅创建了threadLocals,同时也将要添加的本地变量值添加到了 threadLocals中。

1 void createMap(Thread t, T firstValue) {
2 t.threadLocals = new ThreadLocalMap(this, firstValue);
3 }

【2】get方法源码:在 get方法的实现中,首先获取当前调用者线程,如果当前线程的 threadLocals不为null,就直接返回当前线程绑定的本地变量值,否则执行 setInitialValue方法初始化 threadLocals变量。在 setInitialValue方法中,类似于 set方法的实现,都是判断当前线程的 threadLocals变量是否为null,是则添加本地变量(这个时候由于是初始化,所以添加的值为null),否则创建 threadLocals变量,同样添加的值为null。

 1 public T get() {
2 //(1)获取当前线程
3 Thread t = Thread.currentThread();
4 //(2)获取当前线程的threadLocals变量
5 ThreadLocalMap map = getMap(t);
6 //(3)如果threadLocals变量不为null,就可以在map中查找到本地变量的值
7 if (map != null) {
8 ThreadLocalMap.Entry e = map.getEntry(this);
9 if (e != null) {
10 @SuppressWarnings("unchecked")
11 T result = (T)e.value;
12 return result;
13 }
14 }
15 //(4)执行到此处,threadLocals为null,调用该更改初始化当前线程的threadLocals变量
16 return setInitialValue();
17 }
18
19 private T setInitialValue() {
20 //protected T initialValue() {return null;}
21 T value = initialValue();
22 //获取当前线程
23 Thread t = Thread.currentThread();
24 //以当前线程作为key值,去查找对应的线程变量,找到对应的map
25 ThreadLocalMap map = getMap(t);
26 //如果map不为null,就直接添加本地变量,key为当前线程,值为添加的本地变量值
27 if (map != null)
28 map.set(this, value);
29 //如果map为null,说明首次添加,需要首先创建出对应的map
30 else
31 createMap(t, value);
32 return value;
33 }

【3】remove方法的实现:remove方法判断当前线程对应的 threadLocals变量是否为null,不为 null就直接删除当前线程中指定的 threadLocals变量。

1 public void remove() {
2 //获取当前线程绑定的threadLocals
3 ThreadLocalMap m = getMap(Thread.currentThread());
4 //如果map不为null,就移除当前线程中指定ThreadLocal实例的本地变量
5 if (m != null)
6 m.remove(this);
7 }

【4】如下图所示:每个线程内部有一个名为 threadLocals的成员变量,该变量的类型为 ThreadLocal.ThreadLocalMap类型(类似于一个HashMap),其中的 key为当前定义的 ThreadLocal变量的 this引用,value为我们使用 set方法设置的值。每个线程的本地变量存放在自己的本地内存变量 threadLocals中,如果当前线程一直不消亡,那么这些本地变量就会一直存在(可能会导致内存溢出),因此使用完毕需要将其 remove掉。

四、ThreadLocal 不支持继承性


同一个 ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到的。(threadLocals 中为当前调用线程对应的本地变量,所以二者自然是不能共享的)

 1 package test;
2
3 public class ThreadLocalTest2 {
4
5 //(1)创建ThreadLocal变量
6 public static ThreadLocal<String> threadLocal = new ThreadLocal<>();
7
8 public static void main(String[] args) {
9 //在main线程中添加main线程的本地变量
10 threadLocal.set("mainVal");
11 //新创建一个子线程
12 Thread thread = new Thread(new Runnable() {
13 @Override
14 public void run() {
15 System.out.println("子线程中的本地变量值:"+threadLocal.get());
16 }
17 });
18 thread.start();
19 //输出main线程中的本地变量值
20 System.out.println("mainx线程中的本地变量值:"+threadLocal.get());
21 }
22 }

五、InheritableThreadLocal 类


在上面说到的 ThreadLocal类是不能提供子线程访问父线程的本地变量的,而 InheritableThreadLocal类则可以做到这个功能,下面是该类的源码

 1 public class InheritableThreadLocal<T> extends ThreadLocal<T> {
2
3 protected T childValue(T parentValue) {
4 return parentValue;
5 }
6
7 ThreadLocalMap getMap(Thread t) {
8 return t.inheritableThreadLocals;
9 }
10
11 void createMap(Thread t, T firstValue) {
12 t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
13 }
14 }

从上面代码可以看出,InheritableThreadLocal类继承了 ThreadLocal类,并重写了childValue、getMap、createMap三个方法。其中 createMap方法在被调用(当前线程调用 set方法时得到的 map为 null的时候需要调用该方法)的时候,创建的是inheritableThreadLocal而不是 threadLocals。同理,getMap方法在当前调用者线程调用 get方法的时候返回的也不是threadLocals 而是 inheritableThreadLocal。下面我们看看重写的 childValue方法在什么时候执行,怎样让子线程访问父线程的本地变量值。我们首先从 Thread类开始说起

 1 private void init(ThreadGroup g, Runnable target, String name,
2 long stackSize) {
3 init(g, target, name, stackSize, null, true);
4 }
5 private void init(ThreadGroup g, Runnable target, String name,
6 long stackSize, AccessControlContext acc,
7 boolean inheritThreadLocals) {
8 //判断名字的合法性
9 if (name == null) {
10 throw new NullPointerException("name cannot be null");
11 }
12
13 this.name = name;
14 //(1)获取当前线程(父线程)
15 Thread parent = currentThread();
16 //安全校验
17 SecurityManager security = System.getSecurityManager();
18 if (g == null) { //g:当前线程组
19 if (security != null) {
20 g = security.getThreadGroup();
21 }
22 if (g == null) {
23 g = parent.getThreadGroup();
24 }
25 }
26 g.checkAccess();
27 if (security != null) {
28 if (isCCLOverridden(getClass())) {
29 security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
30 }
31 }
32
33 g.addUnstarted();
34
35 this.group = g; //设置为当前线程组
36 this.daemon = parent.isDaemon();//守护线程与否(同父线程)
37 this.priority = parent.getPriority();//优先级同父线程
38 if (security == null || isCCLOverridden(parent.getClass()))
39 this.contextClassLoader = parent.getContextClassLoader();
40 else
41 this.contextClassLoader = parent.contextClassLoader;
42 this.inheritedAccessControlContext =
43 acc != null ? acc : AccessController.getContext();
44 this.target = target;
45 setPriority(priority);
46 //(2)如果父线程的inheritableThreadLocal不为null
47 if (inheritThreadLocals && parent.inheritableThreadLocals != null)
48 //(3)设置子线程中的inheritableThreadLocals为父线程的inheritableThreadLocals
49 this.inheritableThreadLocals =
50 ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
51 this.stackSize = stackSize;
52
53 tid = nextThreadID();
54 }

在 init方法中,首先(1)处获取了当前线程(父线程),然后(2)处判断当前父线程的 inheritableThreadLocals是否为null,然后调用createInheritedMap将父线程的 inheritableThreadLocals作为构造函数参数创建了一个新的 ThreadLocalMap变量,然后赋值给子线程。下面是 createInheritedMap方法和 ThreadLocalMap的构造方法

 1 static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
2 return new ThreadLocalMap(parentMap);
3 }
4
5 private ThreadLocalMap(ThreadLocalMap parentMap) {
6 Entry[] parentTable = parentMap.table;
7 int len = parentTable.length;
8 setThreshold(len);
9 table = new Entry[len];
10
11 for (int j = 0; j < len; j++) {
12 Entry e = parentTable[j];
13 if (e != null) {
14 @SuppressWarnings("unchecked")
15 ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
16 if (key != null) {
17 //调用重写的方法
18 Object value = key.childValue(e.value);
19 Entry c = new Entry(key, value);
20 int h = key.threadLocalHashCode & (len - 1);
21 while (table[h] != null)
22 h = nextIndex(h, len);
23 table[h] = c;
24 size++;
25 }
26 }
27 }
28 }

在构造函数中将父线程的 inheritableThreadLocals成员变量的值赋值到新的 ThreadLocalMap对象中。返回之后赋值给子线程的inheritableThreadLocals。总之,InheritableThreadLocals类通过重写getMap和createMap两个方法将本地变量保存到了具体线程的inheritableThreadLocals变量中,当线程通过InheritableThreadLocals实例的set或者get方法设置变量的时候,就会创建当前线程的inheritableThreadLocals变量。而父线程创建子线程的时候,ThreadLocalMap中的构造函数会将父线程的inheritableThreadLocals中的变量复制一份到子线程的inheritableThreadLocals变量中。

六、从 ThreadLocalMap看 ThreadLocal使用不当的内存泄漏问题


【1】基础概念 :首先我们先看看 ThreadLocalMap的类图,在前面的介绍中,我们知道 ThreadLocal只是一个工具类,他为用户提供getsetremove接口操作实际存放本地变量的 threadLocals(调用线程的成员变量),也知道 threadLocals是一个ThreadLocalMap类型的变量,下面我们来看看 ThreadLocalMap这个类。在此之前,我们回忆一下 Java中的四种引用类型链接

【2】分析 ThreadLocalMap内部实现:上面我们知道 ThreadLocalMap内部实际上是一个 Entry数组​,我们先看看 Entry的这个内部类

 1 /**
2 * 是继承自WeakReference的一个类,该类中实际存放的key是指向ThreadLocal的弱引用和与之对应的value值(该value值
3 * 就是通过ThreadLocal的set方法传递过来的值)由于是弱引用,当get方法返回null的时候意味着回收引用
4 */
5 static class Entry extends WeakReference<ThreadLocal<?>> {
6 /** value就是和ThreadLocal绑定的 */
7 Object value;
8
9 //k:ThreadLocal的引用,被传递给WeakReference的构造方法
10 Entry(ThreadLocal<?> k, Object v) {
11 super(k);
12 value = v;
13 }
14 }
15 //WeakReference构造方法(public class WeakReference<T> extends Reference<T> )
16 public WeakReference(T referent) {
17 super(referent); //referent:ThreadLocal的引用
18 }
19
20 //Reference构造方法
21 Reference(T referent) {
22 this(referent, null);//referent:ThreadLocal的引用
23 }
24
25 Reference(T referent, ReferenceQueue<? super T> queue) {
26 this.referent = referent;
27 this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
28 }

在上面的代码中,我们可以看出,当前 ThreadLocal的引用 k被传递给 WeakReference的构造函数,所以 ThreadLocalMap中的key为 ThreadLocal的弱引用。当一个线程调用ThreadLocal的 set方法设置变量的时候,当前线程的 ThreadLocalMap就会存放一个记录,这个记录的 key值为 ThreadLocal的弱引用,value就是通过 set设置的值。如果当前线程一直存在且没有调用该ThreadLocal的 remove方法,如果这个时候别的地方还有对 ThreadLocal的引用,那么当前线程中的 ThreadLocalMap中会存在对ThreadLocal变量的引用和 value对象的引用,是不会释放的,就会造成内存泄漏。

考虑这个 ThreadLocal变量没有其他强依赖,如果当前线程还存在,由于线程的 ThreadLocalMap里面的 key是弱引用,所以当前线程的 ThreadLocalMap里面的 ThreadLocal变量的弱引用在 gc的时候就被回收,但是对应的 value还是存在的这就可能造成内存泄漏(因为这个时候 ThreadLocalMap会存在key为 null但是 value不为 null的 entry项)。

ThreadLocalMap 中的 Entry的 key使用的是 ThreadLocal对象的弱引用,在没有其他地方对 ThreadLoca依赖,ThreadLocalMap中的 ThreadLocal对象就会被回收掉,但是对应的 value不会被回收,这个时候 Map中就可能存在 key为null但是 value不为null的项,这需要实际使用的时候使用完毕及时调用 remove方法避免内存泄漏。

如果使用线程池,由于线程可能并不是真正的关闭(比如newFixedThreadPool会保持线程一只存活)。因此,如果将一些大对象存放到 ThreadLocalMap中,可能会造成内存泄漏。因为线程没有关闭,无法回收,但是这些对象不会再被使用了。如果希望及时回收对象,则可以使用Thread.remove()方法将变量移除。

ThreadLocal 类的更多相关文章

  1. ThreadLocal<T>类

    ThreadLocal<T>类                                        -------每个线程有仅拥有一个T对象                   ...

  2. 谈话Java在ThreadLocal理解类

    我们必须先了解:ThreadLocal不超过一个线程类,或者它应该被称为线程局部变量.这从ThreadLocal的JDK我们可以看到的定义 public class ThreadLocal<T& ...

  3. 【01-14】java ThreadLocal工具类

    自定义ThreadLocal package concurrent; import java.util.HashMap; import java.util.Map; /** * @author alo ...

  4. ThreadLocal <T>类的说明 转载 原作者 lujh99

    首先,ThreadLocal 不是用来解决共享对象的多线程访问问题的,一般情况下,通过ThreadLocal.set() 到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的.各 ...

  5. ThreadLocal<T> 类

    using System; using System.Threading; using System.Threading.Tasks; class ThreadLocalDemo { // Demon ...

  6. ThreadLocal工具类 隔离思想

    ThreadLocal不是用来解决共享对象的多线程访问问题的, 通过ThreadLocal的set()方法设置到线程的ThreadLocal.ThreadLocalMap里的是是线程自己要存储的对象, ...

  7. ThreadLocal工具类的使用(隔离思想)

    ThreadLocal不是用来解决共享对象的多线程访问问题的, 通过ThreadLocal的set()方法设置到线程的ThreadLocal.ThreadLocalMap里的是是线程自己要存储的对象, ...

  8. java中ThreadLocal类的使用

    ThreadLocal是解决线程安全问题一个很好的思路,ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本,由于Key值不可重复, ...

  9. JavaSe:ThreadLocal

    JDK中有一个ThreadLocal类,使用很方便,但是却很容易出现问题.究其原因, 就是对ThreadLocal理解不到位.最近项目中,出现了内存泄漏的问题.其中就有同事在使用ThreadLocal ...

  10. 切换数据库+ThreadLocal+AbstractRoutingDataSource 一

    最近项目用的数据库要整合成一个,所以把多源数据库切换的写法要清除掉.所以以下记载了多远数据库切换的用法及个人对源码的理解. 框架:Spring+mybatis+vertx,(多源数据库切换的用法不涉及 ...

随机推荐

  1. 如何使用C++代码实现1-100之间的素数

    #include "pch.h" #include <iostream> using namespace std; int main() { cout << ...

  2. bzoj 3669

    思想基本同bzoj 2594,但是多了一步 首先我们发现这时的边有两个属性了,因此我们考虑先去掉其中一者的限制 我们把所有边按$a$大小排序,然后从小到大加入维护的最小生成树 每次加边时都按照$b$的 ...

  3. DAST 代码分析

    DA部分 输入图片大小: images.size: torch.Size([1, 3, 512, 1024])labels.size: torch.Size([1, 512, 1024]) input ...

  4. 项目实训 day15-16

    第一天我与灿哲沟通,我弄明白了真正的网络结构且如何运行的,自己记了下网络草图,开始初步用PlotNN绘制 第二天我发现pycore库表达能力不够,于是参考其他用tex写的例子,写了几个方法,最终能生成 ...

  5. vue组件 子组件没有事件怎么 向父组件传递数据

    通过ref去接收值!!! 需求图片 代码实现 //----------父组件 <div class="fingerprint-bottom"> <el-tabs ...

  6. Python使用双层列表推导式输出九九乘法表

    print('\n'.join([' '.join(['%2d *%2d = %2d' % (col, row, col * row) for col in range(1, row + 1)]) f ...

  7. play() failed because the user didn‘t interact with the document first

    使用js调用音频文件报错,错误信息如下:play() failed because the user didn't interact with the document first该报错是浏览器对于自 ...

  8. SSD目标检测网络解读(含网络结构和内容解读)

    SSD实现思路 SSD具有如下主要特点: 从YOLO中继承了将detection转化为regression的思路,一次完成目标定位与分类 基于Faster RCNN中的Anchor,提出了相似的Pri ...

  9. The first blog

    这是一只爱碎觉的汪的第一篇博客. 下面就来简单介绍一下自己吧,爱好广泛,尤其热爱钢琴和运动,喜欢每个按键在手指间跳动的感觉,喜欢一个个音符连起来奏响的一曲曲优美的音乐,也喜欢运动后大汗淋漓的畅快感.肯 ...

  10. OSIDP-线程-04

    进程和线程 进程具有两个相互独立的特点: 1.资源所有权:进程包括存放进程映像的虚拟地址空间,具有对资源的控制权. 2.调度/执行:进程具有运行状态和优先级,是可被 OS 调度和分派的实体. 通常将分 ...