java线程本地变量ThreadLocal详解

 更新时间:2019年06月06日 09:55:09   作者:mayoi7  
ThreadLocal则为每一个线程提供了一个变量副本,从而隔离了多个线程访问数据的冲突,ThreadLocal提供了线程安全的对象封装,下面我们就来详细了解一下吧

介绍

ThreadLocal作为JDK1.2以来的一个java.lang包下的一个类,在面试和工程中都非常重要,这个类的主要目的是提供线程本地的变量,所以也有很多地方把这个类叫做线程本地变量

从字面理解,这个类为每个线程都创建了一个本地变量,实际上是ThreadLocal为变量在每个线程中都创建了一个副本,使得每个线程都可以访问自己内部的副本变量

通常提到多线程,都会考虑变量同步的问题,但是ThreadLocal并不是为了解决多线程共享变量同步的问题,而是为了让每个线程的变量不互相影响,相当于线程之间操纵的都是变量的副本,自然就不用考虑多线程竞争的问题,也自然没有性能损耗

使用方式

先来看常用的这几个方法

public T get() { }
public void set(T value) { }
public void remove() { }
protected T initialValue() { }

显而易见,get()方法获取线程拥有的副本值,set()方法进行设值,remove()方法移除,initialValue()进行变量初始化,我们先来看下面这个实例,同时体会一下应用场景

public class Demo {
public static ThreadLocal<Integer> threadLocal = null;
public static void main(String[] args) {
threadLocal = new ThreadLocal<Integer>() {
/**
* 通过重写该方法来初始化ThreadLocal的值
*/
@Override
protected Integer initialValue() {
return 10;
}
};
MyThread t1 = new MyThread(20);
MyThread t2 = new MyThread(30);
t1.start();
// 这里为了描述清晰,省略了try-catch语句块
t1.join();
t2.start();
}
}

在上述方法中,我们定义并初始化一个ThreadLocal类为10(通过重写initialValue()方法实现),然后开启了两个线程,同时我们这里让t2线程等待t1线程执行完再执行

MyThread类详细信息如下

class MyThread extends Thread {
private int val = 0;
MyThread(int val) {
this.val = val;
}
@Override
public void run() {
System.out.println(Thread.currentThread() + "-BEFORE-" + Demo.threadLocal.get());
Demo.threadLocal.set(val);
System.out.println(Thread.currentThread() + "-AFTER-" + Demo.threadLocal.get());
}
}

我们通过调用ThreadLocal对象的get()方法来获取当前的值,然后通过set()方法设置一个新值(每个线程我们设置不同的值),然后再通过get()方法来获取设置后的值

运行结果如下

重点是图中标注的t2线程变量的初始值,虽然我们在t1线程中修改了变量的值,但是在t2线程中变量值并没有被改变,这样就实现了每个线程独有的变量

同时,如果一个ThreadLocal对象要在很多地方进行复用时,需要在使用前通过调用**remove()**方法来将本地变量恢复到默认值

也许有人会问了,我们给每个线程定义自己的私有变量不是也可以实现同样的操作吗,理论上当然是可行的,但是ThreadLocal远比私有变量的形式方便,不仅可以在线程外部进行统一的初始化,而且避免在线程内部额外设置变量

原理

点进ThreadLocal的源码中,发现并没有存储变量的字段值,那看来ThreadLocal并不负责保存变量,我们只能从方法下手

先看initial()方法,毕竟我们的变量默认初始值就是在这个方法中设置,如下

protected T initialValue() {
return null;
}

我们在每次创建ThreadLocal都要重写这个方法,那么这个方法到底在哪调用呢,我们点进get()方法源码中,如下

public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程的map
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
T result = (T)e.value;
return result;
}
}
// 如果map为空则进行创建
return setInitialValue();
}

有点眉头了,我们发现这里获取了一个ThreadLocalMap对象,所以会想到有可能是通过让线程与变量作一个KV表,来实现每个线程拥有自己独有的变量

我们点进getMap(t)方法中,发现返回了线程t的一个threadLocals属性,这是Thread类的一个字段:

ThreadLocal.ThreadLocalMap threadLocals = null;

这是一个由ThreadLocal类维护的属性,Thread的任何方法都没有对这个字段进行修改操作,而这个ThreadLocalMap本身又是ThreadLocal的一个内部类,可以把它理解成一个Map(虽然这个类没有继承Map接口)

同时要注意,在ThreadLocalMap对象中的Entry对象(键值对),继承了一个ThreadLocaMap的弱引用,如下

static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

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

也就是说,当ThreadLocal被置为空时,Entry中的Key则会在下一次YGC中被回收

我们还是没有看到initialValue()方法,别急,点进setInitialValue()方法,也就是如果在get()方法中检测到map为空时调用的方法,如下

private T setInitialValue() {
// 我们设定的初始值
T value = initialValue();
// 当前线程
Thread t = Thread.currentThread();
// 再检查一次是否为空
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}

现在基本操作我们都清楚了,set()方法和initialValue()几乎完全一致,remove()方法则是普通地移除了一个KV键值对(K为当前线程),这里均不再列出,如果感兴趣可以自行查看

注意事项

1.脏数据

从上面的分析可以看出,ThreadLocal是和Thread绑定的,每一个Thread对应一个value,如果没有在使用结束后调用remove()方法,就会在下一次重用时读到脏数据(针对同一个线程而言),尤其是使用线程池的场景(线程池中的线程经常会复用)

2.内存泄露

一般在使用时都会将ThreadLocal设置为静态字段,这时候当线程执行完成后,KV中的V是不会自动回收的,所以要在使用完后及时调用remove()方法清理

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

相关文章

  • Java中的位运算符号解读(&、|、^、~、<<、>>、>>>)

    Java中的位运算符号解读(&、|、^、~、<<、>>、>>>)

    这篇文章主要介绍了Java中的位运算符号(&、|、^、~、<<、>>、>>>),具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-08-08
  • Java日期相关API的基本操作总结

    Java日期相关API的基本操作总结

    大概总结一下日期相关API操作原因是对于日期的操作我们开发中还是很常见的,包括在数据库中保存日期,以及之前String类中对字符串的一些处理开发中都很常见,希望对大家有所帮助
    2022-11-11
  • 解析Java的Jackson库中对象的序列化与数据泛型绑定

    解析Java的Jackson库中对象的序列化与数据泛型绑定

    这篇文章主要介绍了解析Java的Jackson库中对象的序列化与数据泛型绑定,Jackson通常被用来实现Java对象和JSON数据的相互转换功能,需要的朋友可以参考下
    2016-01-01
  • 解决IDEA2020.1.2IDEA打不开的问题(最新分享)

    解决IDEA2020.1.2IDEA打不开的问题(最新分享)

    由于idea安装多了某个jar,点击出现读条后闪退情况,接下来通过本文给大家分享解决IDEA2020.1.2IDEA打不开的问题,非常不错,具有一定的参考借鉴价值,感兴趣的朋友跟随小编一起看看吧
    2020-07-07
  • springboot 使用ThreadLocal的实例代码

    springboot 使用ThreadLocal的实例代码

    这篇文章主要介绍了springboot 使用ThreadLocal的实例代码,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-12-12
  • 举例说明Java中代码块的执行顺序

    举例说明Java中代码块的执行顺序

    这篇文章主要介绍了举例说明Java中代码块的执行顺序,包括静态属性和非静态属性以及构造函数等相关的执行先后,需要的朋友可以参考下
    2015-07-07
  • SpringBoot项目设置断点debug调试无效忽略web.xml问题的解决

    SpringBoot项目设置断点debug调试无效忽略web.xml问题的解决

    这篇文章主要介绍了SpringBoot项目设置断点debug调试无效忽略web.xml问题的解决,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-08-08
  • 最新log4j2远程代码执行漏洞(附解决方法)

    最新log4j2远程代码执行漏洞(附解决方法)

    Apache Log4j2 远程代码执行漏洞攻击代码,该漏洞利用无需特殊配置,经多方验证,Apache Struts2、Apache Solr、Apache Druid、Apache Flink等均受影响,本文就介绍一下解决方法
    2021-12-12
  • 使用MAT进行JVM内存分析实例

    使用MAT进行JVM内存分析实例

    这篇文章主要介绍了使用MAT进行JVM内存分析实例,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-04-04
  • Java中this,static,final,const用法详解

    Java中this,static,final,const用法详解

    这篇文章主要介绍了Java中this,static,final,const用法详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-07-07

最新评论