当前位置: 首页 > news >正文

富利建设集团有限公司网站/软文世界平台

富利建设集团有限公司网站,软文世界平台,angular做的网站大全,建设网站商城需要多少费用文章目录1. 前言2. 什么是ThreadLocal3. ThreadLocal使用实例4. ThreadLocal原理分析4.1 基本流程与源码实现4.2 ThreadLoalMap的数据结构4.3 ThreadLocal 副作用4.3.1 脏数据4.3.2 内存泄露4.3.2.1 Java中的引用4.3.2.2 泄露原因分析5. ThreadLocal应用场景举例6. ThreadLocal…

文章目录

  • 1. 前言
  • 2. 什么是ThreadLocal
  • 3. ThreadLocal使用实例
  • 4. ThreadLocal原理分析
    • 4.1 基本流程与源码实现
    • 4.2 ThreadLoalMap的数据结构
    • 4.3 ThreadLocal 副作用
      • 4.3.1 脏数据
      • 4.3.2 内存泄露
        • 4.3.2.1 Java中的引用
        • 4.3.2.2 泄露原因分析
  • 5. ThreadLocal应用场景举例
  • 6. ThreadLocal的副作用和解决方案
  • 参考

1. 前言

说起ThreadLocal即便你没有直接用到过,它也间接的出现在你使用过的框架里,比如Spring的事物管理Hibernate的Session管理、logback(和log4j)中的MDC功能实现等。而在项目开发中,比如用到的一些分页功能的实现往往也会借助于ThreadLocal。

正是因为ThreadLocal的无处不在,所以在面试的时候也经常会被问到它的实现原理、核心API使用以及内存泄露的问题。

而且基于这些问题还可以拓展到线程安全方面、JVM内存管理与分析、Hash算法等等知识点。可见ThreadLocal对开发人员来说是多么的重要的。如果你还没有全面的了解,那么这篇文章值得你深入学习一下。

2. 什么是ThreadLocal

ThreadLocal是Therad的局部变量的维护类,在Java中是作为一个特殊的变量存储在。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

因为每个Thread内有自己的实例副本,且该副本只能由当前Thread使用,也就不存在多线程间共享的问题

总的来说,ThreadLocal适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。

比如,有一个变量count,在多线程并发时操作count++会出现线程安全问题。但是通过ThreadLocal就可以为每个线程创建只属于当前线程的count副本,各自操作各自的副本,不会影响到其他线程。

从另外一个角度来说,ThreadLocal是一个数据结构,有点像HashMap,可以保存"key:value"键值对,但是一个ThreadLocal只能保存一个键值对,各个线程的数据互不干扰。

用法示例:

@Test
public void test1(){ThreadLocal<String> localName = new ThreadLocal<>();// 只提供了一个set方法;localName.set("程序新视界");// 同时只提供了一个get方法String name = localName.get();System.out.println(name);
}

上述代码中线程A初始化了一个ThreadLocal对象,并调用set方法,保持了一个值。而这个值只能线程A调用get方法才能获取到。如果此时线程B调用get方法是无法获取到的。至于如何实现这一功能的,我们在后面源代码分析中进行讲解,这里知道其功能即可。

3. ThreadLocal使用实例

上面介绍了使用场景和基本的实现理论,下面我们就来通过一个简单的实例看一下如何使用ThreadLocal。

public class ThreadLocalMain {/*** ThreadLocal变量,每个线程都有一个副本,互不干扰*/public static final ThreadLocal<String> HOLDER = new ThreadLocal<>();public static void main(String[] args) throws Exception {new ThreadLocalMain().execute();}public void execute() throws Exception {// 主线程设置值HOLDER.set("程序新视界");System.out.println(Thread.currentThread().getName() + "线程ThreadLocal中的值:" + HOLDER.get());new Thread(() -> {System.out.println(Thread.currentThread().getName() + "线程ThreadLocal中的值:" + HOLDER.get());// 设置当前线程中的值HOLDER.set("《程序新视界》");System.out.println("重新设置之后," + Thread.currentThread().getName() + "线程ThreadLocal中的值:" + HOLDER.get());System.out.println(Thread.currentThread().getName() + "线程执行结束");}).start();// 等待所有线程执行结束Thread.sleep(1000L);System.out.println(Thread.currentThread().getName() + "线程ThreadLocal中的值:" + HOLDER.get());}}

示例中定义了一个static final的ThreadLocal变量HOLDER,在main方法中模拟通过两个线程来操作HOLDER中存储的值。先对HOLDER设置一个值,然后打印获取得到的值,然后新起一个线程去修改HOLDER中的值,然后分别在新线程和主线程两处获取对应的值。

注意:我们使用ThreadLocal时,一般都是public static的变量,为什么呢?因为我们知道 public static变量是全局的,多个线程都可以访问,一般情况下变量此时是有状态的,会导致线程非安全的。如果是私有的,别人也不会共享,也就不会产生线程安全问题了,而恰恰 ThreadLocal类型的变量却是又共享又安全!

执行程序,打印结果如下:

main线程ThreadLocal中的值:程序新视界
Thread-0线程ThreadLocal中的值:null
重新设置之后,Thread-0线程ThreadLocal中的值:《程序新视界》
Thread-0线程执行结束
main线程ThreadLocal中的值:程序新视界

对照程序和输出结果,你会发现,主线程和Thread-0各自独享自己的变量存储。主线程并没有因为Thread-0调用了HOLDER的set方法而被改变

之所以能达到这个效果,正是因为在ThreadLocal中,每个线程Thread拥有一份自己的副本变量,多个线程互不干扰。那么,你会疑惑,ThreadLocal是如何实现这一功能的呢?

4. ThreadLocal原理分析

在学习ThreadLocal的原理之前,我们先来看一些相关的理论知识和数据结构。

4.1 基本流程与源码实现

结构如下所示:
在这里插入图片描述

HOLDER和HOLDER2是作为当前线程threadLocalMap的key的,如果存在线程t1和t2,t1的threadLocalMap内有2个元素,为HOLDER和HOLDER2,t1的threadLocalMap内也有2个元素,为HOLDER和HOLDER2。也就是说一个HOLDER实例可以对应一个变量副本,2个实例,可以对应2个变量副本。

如果10个变量,需要10个副本,则需要10个 HOLDER变量吗?累死人了。 当然也可以全局就一个ThreadLocal 句柄,可以把句柄映射的value设计为map,这样间接通过一个句柄,可以存储多个值,此时,如果要存一个副本,则通过HOLDER取出value,这个value是个map类型的,然后在put一个值进去,在调用HOLDER的set,把map存进去即可

演示通过把ThreadLocal设置为map类型,可以存储多个变量:

import java.util.HashMap;
import java.util.Map;/*** 演示通过把ThreadLocal设置为map类型,可以存储多个变量*/
public class ThreadLocalMain2 {/*** ThreadLocal变量,每个线程都有一个副本,互不干扰*/public static final ThreadLocal<HashMap> HOLDER = new ThreadLocal<>();  //map类型public static void main(String[] args) throws Exception {new ThreadLocalMain2().execute();}public void execute() throws Exception {HashMap map = new HashMap();map.put("A","程序新视界A");map.put("B","程序新视界B");// 主线程设置值HOLDER.set(map);System.out.println(Thread.currentThread().getName() + "线程ThreadLocal中的值:" + HOLDER.get());new Thread(() -> {System.out.println(Thread.currentThread().getName() + "线程ThreadLocal中的值:" + HOLDER.get());HashMap   map2 =new HashMap() ;map2.put("A0","程序新视界A");map2.put("B0","程序新视界B");// 设置当前线程中的值HOLDER.set(map2);System.out.println("重新设置之后," + Thread.currentThread().getName() + "线程ThreadLocal中的值:" + HOLDER.get());System.out.println(Thread.currentThread().getName() + "线程执行结束");}).start();// 等待所有线程执行结束Thread.sleep(1000L);System.out.println(Thread.currentThread().getName() + "线程ThreadLocal中的值:" + HOLDER.get());}}

一个线程内可以存多个ThreadLocal对象,存储的位置位于ThreadThreadLocal.ThreadLocalMap变量,在Thread中有如下变量:

public class Thread implements Runnable {/* ThreadLocal values pertaining to this thread. This map is maintained* by the ThreadLocal class. */ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocalMap是由ThreadLocal维护的静态内部类,正如代码中注解所说这个变量是由ThreadLocal维护的。

我们在使用ThreadLocal的get()、set()方法时,其实都是调用了ThreadLocalMap类对应的get()、set()方法。

Thread中的这个变量的初始化通常是在首次调用ThreadLocal的get()、set()方法时进行的。

public class ThreadLocal<T> {public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);else//需要初始化mapcreateMap(t, value);}

上述set方法中,首先获取当前线程对象,然后通过getMap方法来获取当前线程中的threadLocals:

public class ThreadLocal<T> {ThreadLocalMap getMap(Thread t) {return t.threadLocals;}

如果Thread中的对应属性为null,则创建一个ThreadLocalMap并赋值给Thread:

public class ThreadLocal<T> {void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);}

如果已经存在,则通过ThreadLocalMap的set方法设置值,这里我们可以看到set中key为this,也就是当前ThreadLocal对象,而value值则是我们要存的值。

对应的get方法源码如下:

public class ThreadLocal<T> {public T get() {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}return setInitialValue();}

可以看到同样通过当前线程,拿到当前线程的threadLocals属性,然后从中获取存储的值并返回。在get的时候,如果Thread中的threadLocals属性未进行初始化,则也会间接调用createMap方法进行初始化操作。

下面我们通过一个流程图来汇总一下上述流程:
在这里插入图片描述
上述流程中给Thread的threadLocals属性初始化的操作,在JDK8和9中通过debug发现,都没有走createMap方法,暂时还不清楚JVM是如何进行初始化赋值的。而在测试JDK13和JDK14的时候,很明显走了createMap方法。

4.2 ThreadLoalMap的数据结构

ThreadLoalMap是ThreadLocal中的一个静态内部类,类似HashMap的数据结构,但并没有实现Map接口。

每个线程内部有个ThreadLoalMap,把ThreadLoalMap看做一个map时,key是ThreadLocal,value是Object。
在这里插入图片描述

ThreadLoalMap中初始化了一个大小16的Entry数组,Entry对象用来保存每一个key-value键值对。通过上面的set方法,我们已经知道其中的key永远都是ThreadLocal对象。

看一下相关的源码:

public class ThreadLocal<T> {static class ThreadLocalMap {static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}private static final int INITIAL_CAPACITY = 16;// ...}

ThreadLoalMap的类图结构如下:
在这里插入图片描述

由于theadLocalMaps是延迟创建的,因此在构造时至少要创建一个Entry对象。这里可以从构造方法中看到:

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {table = new Entry[INITIAL_CAPACITY];int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);table[i] = new Entry(firstKey, firstValue);size = 1;setThreshold(INITIAL_CAPACITY);
}

上述构造方法,创建了一个默认长度为16的Entry数组,通过hashCode与length位运算确定索引值i。而上面也提到,每个Thread都有一个ThreadLocalMap类型的变量。

至此,结合Thread,我们可以看到整个数据模型如下:
在这里插入图片描述

4.3 ThreadLocal 副作用

4.3.1 脏数据

脏数据应该是大家比较好理解的,所以这里呢,先拿出来讲。线程复用会产生脏数据。由于线程池会重用 Thread 对象 ,那么与 Thread 绑定的类的静态属性 ThreadLocal 变量也会被重用。如果在实现的线程 run() 方法体中不显式地调用 remove() 清理与线程相关的 ThreadLocal 信息,那么倘若下一个线程不调用 set() 设置初始值,就可能 get() 到重用的线程信息,包括 ThreadLocal 所关联的线程对象的 value 值。

4.3.2 内存泄露

ThreadLocalMap 的每个 Entry 都是一个对键的弱引用 - WeakReference<ThreadLocal<?>>,这一点从super(k)可看出。另外,每个 Entry都包含了一个对 值 的强引用

引用链如图所示:
在这里插入图片描述
其中虚线表示弱引用,实线表示强引用。下面我们先来了解一下Java中引用的分类。

当在某个方法内执行: ThreadLocal<String> threadLocal = new ThreadLocal<>(); 时:
在这里插入图片描述

4.3.2.1 Java中的引用

Java中通常会存在以下类型的引用:强引用、弱引用、软引用、虚引用。

  • 强引用:通常new出来的对象就是强引用类型,只要引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足的时候;
  • 软引用:使用SoftReference修饰的对象被称为软引用,软引用指向的对象在内存要溢出的时候被回收。如果回收之后,还没有足够的内存,才会抛出内存溢出异常;
  • 弱引用:使用WeakReference修饰的对象被称为弱引用,只要发生垃圾回收,无论当前内存是否足够,都会回收掉只被弱引用关联的对象实例。
  • 虚引用:虚引用是最弱的引用,在Java中使用PhantomReference进行定义。虚引用中唯一的作用就是用队列接收对象即将死亡的通知。

先通过 一文讲透java弱引用以及使用场景学习 弱引用,否则看不懂下文!
主要结论:2个变量都引用对像A时,其中一个变量P1是弱引用,另一个变量P2是强引用,在gc时,不会断开P1对A的引用;如果仅存在一个变量用P3,对对象B是一个弱引用,在gc时,才会断开P3对B的引用

4.3.2.2 泄露原因分析

正常流程

正常来说,当Thread执行完会被销毁,Thread.threadLocals指向的ThreadLocalMap实例也随之变为垃圾(通过threadLocalMap==null,上图3会断开),它里面存放的Entity也会被回收,5和6都会断开,value被回收,而threadLocal是否回收取决于1是否断开,如果后续1断开,那么threadLocal会回收,如果1不断开,threadLocal不回收(不算泄露,取决用户的逻辑)。这种情况是不会发生内存泄漏的:
在这里插入图片描述


ThreadLocal 泄露

在前面的叙述中,我有提到Entry extends WeakReference<ThreadLocal<?>> 是为了防止内存泄露。实际上,这里说的防止内存泄露是针对ThreadLocal 对象的

怎么说呢?继续往下看。

如果你有学习过Java 中的引用的话,这个WeakReference应该不会陌生,当 JVM 进行垃圾回收时,无论内存是否充足,都会回收只被弱引用关联的对象。

通过这种设计,即使线程正在执行中, 只要 ThreadLocal 对象引用被置成 null(上图中1断开),Entry 的 Key 就会自动在下一次 YGC 时被垃圾回收(因为只剩下ThreadLocalMap 对其的弱引用,参见上图5处,没有强引用了)。

此时上图中6处的value仍然没有释放!

如果这里Entry 的key 值是对 ThreadLocal 对象的强引用的话,即如果上图5处是实线而不是虚线,那么即使ThreadLocal的对象引用被声明成null 时,这些 ThreadLocal 不能被回收,因为还有来自 ThreadLocalMap 的强引用,这样子就会造成内存泄漏。

这类key被回收( key == null)的Entry 在 ThreadLocalMap 源码中被称为 stale entry (翻译过来就是 “过时的条目”),会在下一次执行 ThreadLocalMap 的 getEntry 和 set 方法中,将 这些 stale entry 的value 置为 null,使得原来value 指向的变量可以被垃圾回收。(一种保护机制,这样上图中6处也断开了,保证value可以被回收)

这样子来看,ThreadLocalMap 是通过这种设计,解决了 ThreadLocal 对象可能会存在的内存泄漏的问题,并且对应的value 也会因为上述的 stale entry 机制被垃圾回收。

但是我们为什么还会说使用ThreadLocal 可能存在内存泄露问题呢,在这里呢,指的是还存在那个Value(上图6处)实例无法被回收的情况。


value泄露

请注意,前面 ThreadLocal 泄露中,上述机制的前提是ThreadLocal 的引用被置为null,才会触发弱引用机制,继而回收Entry 的 Value对象实例。我们来看下ThreadLocal 源码中的注释

instances are typically private static fields in classes   //ThreadLocal 对象通常作为私有静态变量使用

作为静态变量使用的话, 那么其生命周期至少不会随着线程结束而结束。也就是说,绝大多数的静态threadLocal对象都不会被置为null。这样子的话,1不断开,通过 stale entry 这种机制来清除Value 对象实例这条路是走不通的。必须要手动remove() 才能保证。

此时,有线程复用的时候,3、4、6一直存在,加上1也存在,value永远不会被回收,导致value泄露。

我们来验证下,改造ThreadLocalMain类,ThreadLocalMain3:

/*** 演示ThreadLocal  执行gc,而数据仍然存在,说明gc没有切断弱引用*/
public class ThreadLocalMain3 {/*** ThreadLocal变量,每个线程都有一个副本,互不干扰*/public static  ThreadLocal<String> HOLDER = new ThreadLocal<>();public static void main(String[] args) throws Exception {new ThreadLocalMain3().execute();}public void execute() throws Exception {// 主线程设置值HOLDER.set("程序新视界");System.out.println(Thread.currentThread().getName() + "线程ThreadLocal中的值:" + HOLDER.get());System.gc();Thread.sleep(1000L);System.out.println(Thread.currentThread().getName() + "线程ThreadLocal中的值:" + HOLDER.get());}}

执行结果:

main线程ThreadLocal中的值:程序新视界
main线程ThreadLocal中的值:程序新视界    //仍然有值,说明弱引用没断开

再次修改ThreadLocalMain3,新增ThreadLocalMain4:

/*** 演示ThreadLocal  执行gc,通过断点发现key=null,说明gc切断弱引用<br>*     原因:HOLDER=null后,此时只有1个变量引用对象,并且是弱引用*/
public class ThreadLocalMain4 extends  Thread{/*** ThreadLocal变量,每个线程都有一个副本,互不干扰*/public static  ThreadLocal<String> HOLDER = new ThreadLocal<>();public static void main(String[] args) throws Exception {new ThreadLocalMain4().execute();}public void execute() throws Exception {// 主线程设置值HOLDER.set("程序新视界");System.out.println(Thread.currentThread().getName() + "线程ThreadLocal中的值:" + HOLDER.get());HOLDER =null;  //手动断开 HOLDER变量的引用System.gc();Thread.sleep(1000L);System.out.println(Thread.currentThread(). getName()+ "断点"); //需要通过断点来查看entry的key属性,此时=null}}

在最后一行打上断点,观察entry的key属性:

在这里插入图片描述
在这里插入图片描述

所以,通常在使用完ThreadLocal后需要调用remove()方法进行内存的清除。

比如在web请求当中,我们可以通过过滤器等进行回收方法的调用:

public void doFilter(ServeletRequest request, ServletResponse){try{//设置ThreadLocal变量localName.set("程序新视界");chain.doFilter(request, response)}finally{//调用remove方法溢出threadLocal中的变量localName.remove();}
}

这样,当请求结束后,该请求内的ThreadLocal会被清除掉。

5. ThreadLocal应用场景举例

最后,我们再来回顾一下ThreadLocal的应用场景:

  • 线程间数据隔离,各线程的ThreadLocal互不影响;

    经典案例 参见 SimpleDateFormat是非线程安全的(可用ThreadLocal解决)

  • 方便同一个线程使用某一对象,避免不必要的参数传递;

  • 全链路追踪中的traceId或者流程引擎中上下文的传递一般采用ThreadLocal;

  • Spring事务管理器采用了ThreadLocal;

  • Spring MVC的RequestContextHolder的实现使用了ThreadLocal;

6. ThreadLocal的副作用和解决方案

  • 内存泄漏
    一般情况下,线程复用才会导致内存泄露,正常的线程销毁后,会清除数据的。
    解决: 每次使用完ThreadLocal都调用它的remove()方法清除数据

  • 脏数据
    线程复用会产生脏数据

    由于结程池会重用 Thread 对象 ,那么与 Thread 绑定的类的静态属性 ThreadLocal 变量也会被重用。如果在实现的线程 run()方法体中不显式地调用 remove() 清理与线程相关的ThreadLocal 信息,那么如果下一个线程不调用set()设置初始值,就可能 get()到重用的线程信息,包括 ThreadLocal 所关联的线程对象的 value 值。

    解决: 每次使用完ThreadLocal都调用它的remove()方法清除数据

  • 将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任
    何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉

参考

ThreadLocal全攻略:使用实战,源码分析,内存泄露分析

ThreadLocal的使用和注意事项
一文讲透java弱引用以及使用场景
Java 并发编程 ③ - ThreadLocal 和 InheritableThreadLocal 详解

相关文章: