XiaoLin's Blog

Xiao Lin

ThreadLocal 学习

20
2024-02-23

1. ThreadLocal介绍

1.1 基本介绍

ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是 private static类型的,用于关联线程和线程上下文。

我们可以得知ThreadLocal的作用是:提供线程内的局部变量,不同的线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度。

总结:

  1. 线程并发:在多线程的场景下
  2. 传递数据:我们可以通过ThreadLocal在同一线程,不同组件中传递公共变量
  3. 线程隔离:每个线程的变量都是独立的,不会互相影响

1.2 基本使用

1.2.1 常用方法

方法声明

描述

ThreadLocal()

构造方法,用于创建对象

public void set(T value)

设置当前线程绑定的局部变量

public T get()

获取当前线程绑定的局部变量

public void remove()

删除当前线程绑定的局部变量(注意:该操作特别重要,防止内存溢出)

1.2.2 使用案例

package xyz.xiaolinz.demo1;

/**
* 
* @author huangmuhong
* @version 1.0.0
* @date 2023/1/12
*
 *
 * ThreadLocal:
 *  1. set(): 给当前变量绑定一个值
 *  2. get(): 获取当前变量的值
**/
public class ThreadLocalDemo {

    ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        final ThreadLocalDemo demo = new ThreadLocalDemo();
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                demo.set(Thread.currentThread().getName() + "的数据");
                System.out.println(Thread.currentThread().getName() + "====>" + demo.get());
                // 使用完成时显示调用remove()方法
                demo.remove();
            }).start();
        }
    }

    /**
     * 绑定变量到当前线程
     *
     * @param value 值
     */
    public void set(String value) {
        threadLocal.set(value);
    }

    /**
     * 获取当前线程绑定的变量
     *
     * @return 值
     */
    public String get() {
        return threadLocal.get();
    }

    /**
     * 去除绑定变量
     * 该方法不是必须的,因为ThreadLocalMap中的Entry是使用弱引用的,所以当线程结束时,会自动被回收,不会造成内存泄漏
     * 但是当其他对象持有ThreadLocal的引用时,就需要手动调用该方法去除绑定变量,否则会造成内存泄漏
     */
    public void remove() {
        threadLocal.remove();
    }
}

1.3 ThreadLocal类与synchronized关键字

1.3.1 synchronized同步方式

synchronized简单案例

package xyz.xiaolinz.demo2.demo1;

/**
* 
* @author huangmuhong
* @version 1.0.0
* @date 2023/1/12
*
 *
 * ThreadLocal:
 *  1. set(): 给当前变量绑定一个值
 *  2. get(): 获取当前变量的值
**/
public class SynchronizedDemo {

    private String content;
    public static void main(String[] args) {
        final SynchronizedDemo demo = new SynchronizedDemo();
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                synchronized (demo) {
                    demo.setContent(Thread.currentThread().getName() + "的数据");
                    System.out.println(Thread.currentThread().getName() + "====>" + demo.getContent());
                }

            }).start();
        }
    }

    /**
     *
     *
     * @return 值
     */
    public String getContent() {
            return content;
    }

    /**
     *
     *
     * @param value 值
     */
    public void setContent(String value) {

            content = value;
    }

}

结果打印

![[Untitled 13.png]]

从结果可以发现,加锁的方案同样也可以实现线程隔离的效果,但是在代码中加锁会导致程序失去了并发性,程序只能阻塞的等待前一个线程将demo对象的锁释放后才能继续去操作content的值。所以给该方案降低了程序的性能

1.3.2 ThreadLocal与synchronized的区别

虽然ThreadLocal模式与synchronized关键字都用于处理多线程并发访问变量的问题,不过两者处理的角度和思路不同

synchronized

ThreadLocal

原理

同步机制采用了“以时间换空见”的方式,只提供了一份变量,让不同的线程派对访问

ThreadLocal采用“以空间换时间”的方式,为每个线程都提供了一份变量父本,从而实现同时访问而互不干扰

侧重点

多个线程间访问资源的同步(阻塞)

多线程中让每个线程之间的数据相互隔离

总结:在刚刚的案例中,ThreadLocal 和 synchronized 都能解决问题,但是使用 ThreadLocal 是最佳选择,我们在开发中通长都会选择以“空间换时间”的方案将性能最大化,但是使用 ThreadLocal 需要注意内存泄漏的问题


2. ThreadLocal的内部结构

2.1 常见的误解

如果外面恩不去看源代码的话,可能会猜测ThreadLocal是这样子设计的:每个ThreadLocal都创建一个Map,然后用线程作为Map的key,要存储的局部比昂量作为Map的value,这样就能达到各个线程的局部变量隔离的效果。这是最简单的设计方法,JDK最早期的ThreadLocal确实是这样设计的,但现在早已不是了。

![[Untitled 12.png]]

2.2 现在的设计

但是,JDK后面优化了设计方案,在JDK8中 ThreadLocal 的设计是:每个 Thread 维护一个 ThreadLocalMap ,这个Map的keyThreadLocal 本身,value 擦汗is真正要存储的值 Object

具体的过程是这样的:

  1. 每个Thread线程内部都有一个Map(ThreadLocalMap)
  2. Map里面存储ThreadLocal对象(key)和线程的变量副本(value)
  3. Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值
  4. 对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本隔离,互不干扰

![[Untitled 11.png]]

2.3 JDK8后设计的好处

  1. 每个Map存储的Entry数量变少
  2. 当Thread销毁时,ThreadLocalMap也会随之销毁,减少内存的使用

3. ThreadLocal的核心方法源码

基于ThreadLocal的内部结构,我们继续分析核心源码,深入了解原理;

方法:

方法声明

描述

protected T initalValue()

返回当前线程局部变量的初始值

public void set(T set)

设置当前线程绑定的局部变量

public T get()

获取当前线程绑定的局部变量

public void remove()

移除当前线程绑定的局部变量

3.1 set方法

/**
	* 设置当前线程对应的ThreadLocal的值
	* @param value 将要保存在当前线程对应的ThreadLocal的值
	*/
public void set(T value) {
				// 获取当前线程对象
        Thread t = Thread.currentThread();
				// 获取到线程维护的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
				// 往map填充key为ThreadLocal,value为value副本变量的记录
				// 可以看到ThreadLocal本身并不存储任何变量的副本值,其存在更像操作ThreadLocalMap的工具类
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
}

/**
	* 获取当前线程Thread对象维护的ThreadLocalMap
	*/
ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
 }

/**
	* 创建ThreadLocalMap,用于在当前线程(Thread)维护ThreadLocalMap为空的情况下使用
	*/
void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
 }

代码执行流程

  1. 首先获取当前线程,并根据当前线程获取一个ThreadLocalMap
  2. 如果获取的Map不为空,则将参数设置到Map钟(当前的ThreadLocal为key)
  3. 如果Map为空,则给该线程直接new一个ThreadLocalMap,并将值记录

3.2 get方法

/**
	* 返回当前线程中保存着ThreadLocal的值
	* 如果当前线程没有刺ThreadLocal变量
	* 则它会通过调用{@link [[initialvalue]]}方法进行初始化值
	* 
	*/
public T get() {
				// 获取当前线程
        Thread t = Thread.currentThread();
				// 获取当前线程的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
				// 判断map存不存在
        if (map != null) {
						// 以当前ThreadLocal为key,调用getEntry获取对应的存储实体
            ThreadLocalMap.Entry e = map.getEntry(this);
						// 对结果实体进行判空
            if (e != null) {
                @SuppressWarnings("unchecked")
								// 实际结果
                T result = (T)e.value;
                return result;
            }
        }
				/*
						初始化:有两种情况执行当前代码
						第一种情况:map不存在,表示此线程没有维护的ThreadLocalMap对象
						第二种情况:map存在,但是没有与当前ThreadLocal关联的Entry
				*/
        return setInitialValue();
}

/**
	* 当读取不到对应值时使用当前方法初始化
	*/
private T setInitialValue() {
				// 调用initialValue方法获取初始值
				// 此方法可以被子类重写,如果不重写返回null
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
        if (this instanceof TerminatingThreadLocal) {
            TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
        }
        return value;
    }

代码执行流程

  1. 首先获取当前线程,根据当前线程获取一个Map
  2. 如果获取的Map不为空,则在Map中以ThreadLocal的引用作为key来在Map中获取对应的Entry,否则到4
  3. 如果e不为null,则返回e.value 否则转到4
  4. Map为空或者e为空,则通过initialValue函数获取初始值Value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新Map