早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。ThreadLocal是指作用域为Thread的局部变量,也许把它命名为ThreadLocalVariable更容易让人理解一些。此博客很多内容参考了(这篇博客https://www.cnblogs.com/fsmly/p/11020641.html).

介绍

多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。 ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题,如下图所示:

ThreadLocal使用示例

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

package test;

public class ThreadLocalTest {

    static ThreadLocal<String> localVar = new ThreadLocal<>();

    static void print(String str) {
//打印当前线程中本地内存中本地变量的值
System.out.println(str + " :" + localVar.get());
//清除本地内存中的本地变量
localVar.remove();
} public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
//设置线程1中本地变量的值
localVar.set("localVar1");
//调用打印方法
print("thread1");
//打印本地变量
System.out.println("after remove : " + localVar.get());
}
}); Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
//设置线程1中本地变量的值
localVar.set("localVar2");
//调用打印方法
print("thread2");
//打印本地变量
System.out.println("after remove : " + localVar.get());
}
}); t1.start();
t2.start();
}
}

ThreadLocal的实现原理

从上一节中我们可以看出,ThreadLocal主要有set和get方法,用于设置和获取线程中的变量,那么ThreadLocal是怎么实现这个功能的呢?和ThreadLocal实现相关的类主要有三个:ThreadLocal、Thread、ThreadLocalMap,三者之间的关系同样如下图所示:

  1. ThreadLocalMap:名字上看是Map,实际上是一个数组,不过它的功能和Map类似,可以按照key查找数据。
  2. Thread:线程大家应该都知道,那么在ThreadLocal中他起什么作用呢?一个Thread中会包含两个ThreadLocalMap,分别用于存储本线程和父线程的ThreadLocal数据。每一个ThreadLocal变量会在线程中对应一条ThreadLocalMap的key-value,其中key是ThreadLocal的唯一Hash值。
  3. ThreadLocal:每个ThreadLocal都会有一个唯一的Hash值,用于查找这个ThreadLocal在ThreadLocalMap中的值;ThreadLocal提供了方法用于获取当前线程的ThreadLocal数据。

数据存放的位置

ThreadLocal只是一层访问线程数据的壳,ThreadLocal get和set的数据不会在ThreadLocal的实例中存放,而是存放在线程Thread中的ThreadLocalMap,ThreadLocal只是提供了一个访问这些数据的途径。

ThreadLoca的set方法将value添加到调用线程的ThreadLocalMap中,当调用线程调用get方法时候能够从它的ThreadLocalMap中取出变量。如果调用线程一直不终止,那么这个本地变量将会一直存放在他的ThreadLocalMap中,所以不使用本地变量的时候需要调用remove方法将ThreadLocalMap中删除不用的本地变量。

set方法存放数据

ThreadLocal方法的set可以向当前线程的ThreadLocalMap中放入数据,存放数据的源码如下所示,Set过程分为以下步骤:

  1. 获取当前线程。
  2. 从当前线程中获取ThreadLocalMap变量。
  3. 如果当前线程的ThreadLocalMap不为空,用当前的ThreadLocal为Key,需要存放的数据为Value,存放数据。
  4. 如果当前线程的ThreadLocalMap为空,创建ThreadLocalMap并存放数据。
public void set(T value) {
//(1)获取当前线程(调用者线程)
Thread t = Thread.currentThread();
//(2)以当前线程作为key值,去查找对应的线程变量,找到对应的map
ThreadLocalMap map = getMap(t);
//(3)如果map不为null,就直接添加本地变量,key为当前定义的ThreadLocal变量的this引用,值为添加的本地变量值
if (map != null)
map.set(this, value);
//(4)如果map为null,说明首次添加,需要首先创建出对应的map
else
createMap(t, value);
}

get方法获取数据

ThreadLocal方法的get可以获取当前线程ThreadLocalMap中存放的数据,获取存放数据的源码如下所示,get过程分为以下步骤:

  1. 获取当前线程
  2. 从当前线程中获取ThreadLocalMap变量。
  3. 如果ThreadLocalMap变量不为null,就可以在map中查找到本地变量的值。
  4. 如果ThreadLocalMap变量为null,那么就初始化当前线程的ThreadLocalMap。
public T get() {
//(1)获取当前线程
Thread t = Thread.currentThread();
//(2)获取当前线程的threadLocals变量
ThreadLocalMap map = getMap(t);
//(3)如果threadLocals变量不为null,就可以在map中查找到本地变量的值
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//(4)执行到此处,threadLocals为null,调用该更改初始化当前线程的threadLocals变量
return setInitialValue();
} private T setInitialValue() {
//protected T initialValue() {return null;}
T value = initialValue();
//获取当前线程
Thread t = Thread.currentThread();
//以当前线程作为key值,去查找对应的线程变量,找到对应的map
ThreadLocalMap map = getMap(t);
//如果map不为null,就直接添加本地变量,key为当前线程,值为添加的本地变量值
if (map != null)
map.set(this, value);
//如果map为null,说明首次添加,需要首先创建出对应的map
else
createMap(t, value);
return value;
}

ThreadLocal不支持继承性

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

package test;

public class ThreadLocalTest2 {

    //(1)创建ThreadLocal变量
public static ThreadLocal<String> threadLocal = new ThreadLocal<>(); public static void main(String[] args) {
//在main线程中添加main线程的本地变量
threadLocal.set("mainVal");
//新创建一个子线程
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("子线程中的本地变量值:"+threadLocal.get());
}
});
thread.start();
//输出main线程中的本地变量值
System.out.println("mainx线程中的本地变量值:"+threadLocal.get());
}
}

InheritableThreadLocal类

在上面说到的ThreadLocal类是不能提供子线程访问父线程的本地变量的,而InheritableThreadLocal类则可以做到这个功能,下面是该类的源码,InheritableThreadLocal类继承了ThreadLocal类,并重写了childValue、getMap、createMap三个方法。我们接下来分别介绍一下三种方法的用处。

  1. createMap:当线程中不存在ThreadLocalMap变量,但是调用set或者get方法设置值的时候,需要初始化ThreadLocalMap变量时调用该方法。
  2. getMap:需要获取线程的ThreadLocalMap时调用该方法,这里返回的ThreadLocalMap始终为InheritableThreadLocalMap。
  3. childValue:在创建新线程的时候,如果父线程有ThreadLocalMap变量并且允许inherite ThreadLocalMap,那么程序会复制父线程的InheritableThreadLocal到子线程中,childValue表示在复制过程中如何根据父线程中得到数据生成线程中的数据。


public class InheritableThreadLocal<T> extends ThreadLocal<T> {
/**
* Creates an inheritable thread local variable.
*/
public InheritableThreadLocal() {} /**
* Computes the child's initial value for this inheritable thread-local
* variable as a function of the parent's value at the time the child
* thread is created. This method is called from within the parent
* thread before the child is started.
* <p>
* This method merely returns its input argument, and should be overridden
* if a different behavior is desired.
*/
protected T childValue(T parentValue) {
return parentValue;
} /**
* Get the map associated with a ThreadLocal.
*/
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
} /**
* Create the map associated with a ThreadLocal.
*/
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}

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

ThreadLocal内存泄漏

通过前面的分析我们知道,ThreadLocal的线程数据是存放在ThreadLocalMap中的,所以如果ThreadLocal出现内存泄漏,那么肯定是ThreadLocalMap中存储的数据出现了泄露,我们需要看看ThreadLocalMap中的数据结构。ThreadLocalMap的数据结构如下所示,ThreadLocalMap中的数据存储在一个Entry数组中,Entry中有对ThreadLocal的WeakReference。

什么情况下会出现内存泄露呢?

  1. 当一个线程调用ThreadLocal的set方法设置变量的时候,当前线程的ThreadLocalMap就会存放一个记录,这个记录的key值为ThreadLocal的弱引用,value就是通过set设置的值。
  2. 如果当前线程一直存在且没有调用该ThreadLocal的remove方法,如果这个时候别的地方还有对ThreadLocal的引用,那么当前线程中的ThreadLocalMap中会存在对ThreadLocal变量的引用和value对象的引用,是不会释放的,就会造成内存泄漏。
  3. 考虑这个ThreadLocal变量没有其他强依赖,如果当前线程还存在,由于线程的ThreadLocalMap里面的key是弱引用,所以当前线程的ThreadLocalMap里面的ThreadLocal变量的弱引用在gc的时候就被回收,但是对应的value还是存在的这就可能造成内存泄漏(因为这个时候ThreadLocalMap会存在key为null但是value不为null的entry项)。

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

Java中的四种引用类型

上文中我们说到了WeakReference,大家可能对这个词有点陌生,Java中有四种引用类型:

  1. 强引用:Java中默认的引用类型,一个对象如果具有强引用那么只要这种引用还存在就不会被GC。
  2. 软引用:简言之,如果一个对象具有弱引用,在JVM发生OOM之前(即内存充足够使用),是不会GC这个对象的;只有到JVM内存不足的时候才会GC掉这个对象。软引用和一个引用队列联合使用,如果软引用所引用的对象被回收之后,该引用就会加入到与之关联的引用队列中。
  3. 弱引用(这里讨论ThreadLocalMap中的Entry类的重点):如果一个对象只具有弱引用,那么这个对象就会被垃圾回收器GC掉(被弱引用所引用的对象只能生存到下一次GC之前,当发生GC时候,无论当前内存是否足够,弱引用所引用的对象都会被回收掉)。弱引用也是和一个引用队列联合使用,如果弱引用的对象被垃圾回收期回收掉,JVM会将这个引用加入到与之关联的引用队列中。若引用的对象可以通过弱引用的get方法得到,当引用的对象呗回收掉之后,再调用get方法就会返回null;
  4. 虚引用:虚引用是所有引用中最弱的一种引用,其存在就是为了将关联虚引用的对象在被GC掉之后收到一个通知。(不能通过get方法获得其指向的对象)。

我是御狐神,欢迎大家关注我的微信公众号:wzm2zsd

本文最先发布至微信公众号,版权所有,禁止转载!

最新文章

  1. Bootstrap Modal 垂直方向加滚动条
  2. Mac OS X 背后的故事
  3. delphi 怎么获取工程版本号
  4. centos7 安装 mariadb 的正确命令
  5. objective-c自学总结(二)---init/set/get方法
  6. mac 安装PIL
  7. 输入两棵二叉树A,B,判断B是不是A的子结构。(ps:我们约定空树不是任意一个树的子结构)
  8. OneAPM Cloud Test——系统性能监控神器
  9. Flask源码解读(一)
  10. 64位win8 配置Apache2.4+mod_msgi4.4.21+django1.8.6+python3.4
  11. 2)JS动态生成HTML元素的爬取
  12. 自学python笔记(二)
  13. BST 解析 (二)height and deletion
  14. python基础 列表 的用法及介绍
  15. BZOJ 1485: [HNOI2009]有趣的数列 [Catalan数 质因子分解]
  16. [LeetCode] The Maze III 迷宫之三
  17. java实现小学生四则运算
  18. Java学习笔记(十五):import关键字
  19. vue.js初始学习笔记&amp;vue-cli
  20. Bigining

热门文章

  1. Dubbo 实现一个Route Factory(用于灰度发布)
  2. NOIP&amp;CSP PJ 难度刷题记录
  3. 洛谷P3067题解
  4. kali linux重启网卡失败:Job for networking.service failed because the control process exited with error code. See &quot;systemctl status networking.service&quot; and &quot;journalctl -xe&quot; for details. 问题排查
  5. rsa加密初探
  6. 大龄程序员的出路在哪里?八年老Android的一点心得
  7. 掌握JavaScript中的Promise,实现异步编程
  8. Docker部署netcore web实践
  9. PostgreSQL 时间转换
  10. OSPF的DR与BDR