ThreadLocal源码解析 1.运行原理

ThreadLocal源码解析—运行原理

简介

ThreadLocal 类用来提供线程内部的局部变量,这种变量在多线程环境下访问(通过 get 和 set 方法访问)时能保证各个线程的变量相对独立于其他线程内的变量,分配在堆内的 TLAB 中。

ThreadLocal 实例通常来说都是 private static 类型的,属于一个线程的本地变量,用于关联线程和线程上下文。每个线程都会在 ThreadLocal 中保存一份该线程独有的数据,所以是线程安全的。

ThreadLocal 作用

  • 线程并发:应用在多线程并发的场景下。

  • 传递数据:通过 ThreadLocal 实现在同一线程不同函数或组件中传递公共变量,减少传递复杂度。

  • 线程隔离:每个线程的变量都是独立的,不会互相影响。

对比 synchronized:

synchronizedThreadLocal
原理同步机制采用以时间换空间的方式,
只提供了一份变量,让不同的线程排队访问
ThreadLocal 采用以空间换时间的方式,
为每个线程都提供了一份变量的副本,从而实现同时访问而相不干扰
侧重点多个线程之间访问资源的同步多线程中让每个线程之间的数据相互隔离

使用场景代码示例

/**
 * ThreadLocal通过为每一个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。
 * 
 * 该类提供了线程局部(thread-local)变量。这些变量不同于它们的普通对应物,
 * 因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量
 * 它独立于变量的初始化副本。ThreadLocal实例通常是类中的 private static 字段
 * 它们希望将状态与某一个线程(例如,用户ID 或 事务ID)相关联。
 * 
 * 例如,以下类生成对每个线程唯一的局部标识符。
 * 
 * 线程ID 是在第一次调用ThreadId.get()时分配的,
 * 在后续调用中不会更改。
 */
public class ThreadId {
    /**
     * 原子整数,一个分配给线程的 Thread ID
     */
    private static final AtomicInteger nextId = new AtomicInteger(0);

    /**
     * 每一个线程对应一个 Thread ID
     */
    private static final ThreadLocal<Integer> threadId = new ThreadLocal<Integer>() {
        /**
         * 初始化一个nextId的值
         */
        @Override
        protected Integer initialValue() {
            // nextId自增
            return nextId.getAndIncrement();
        }
    };

    /**
     * 返回当前线程对应的Thread ID,必要时会进行分配
     */
    public static int get() {
        return threadId.get();
    }

    // ------------------------测试程序------------------------
    public static void main(String[] args) throws InterruptedException {
        RunnableTask task = new RunnableTask();

        Thread t1 = new Thread(task, "线程1");
        t1.start();
        TimeUnit.MILLISECONDS.sleep(100);

        Thread t2 = new Thread(task, "线程2");
        t2.start();
        TimeUnit.MILLISECONDS.sleep(100);

        Thread t3 = new Thread(task, "线程3");
        t3.start();
        TimeUnit.MILLISECONDS.sleep(100);

        Thread t4 = new Thread(task, "线程4");
        t4.start();
        TimeUnit.MILLISECONDS.sleep(100);

        Thread t5 = new Thread(task, "线程5");
        t5.start();
        TimeUnit.MILLISECONDS.sleep(100);
    }
    
    static class RunnableTask implements Runnable {
        @Override
        public void run() {
            try {
                System.out.println("当前线程名称为:" + Thread.currentThread().getName() + ", 分配的id为:" + get());
            } finally {
                // 使用完毕之后,必须释放掉
                threadId.remove();
            }
        }
    }
}

执行结果

当前线程名称为:线程1,分配的Id为:0
当前线程名称为:线程2,分配的Id为:1
当前线程名称为:线程3,分配的Id为:2
当前线程名称为:线程4,分配的Id为:3
当前线程名称为:线程5,分配的Id为:4

从运行结果可以发现,不同的线程的数据是隔离的,一个线程只能获取本线程设置到 ThreadLocal 中的数据。

ThreadLocal 每个线程的变量 (nextId) 都是独立的,不会相互影响。

实现原理

底层结构

JDK8 以前:每个 ThreadLocal 都创建一个 Map,然后用线程作为 Map 的 key,要存储的局部变量作为 Map 的 value,达到各个线程的局部变量隔离的效果。这种结构会造成 Map 结构过大和内存泄露,因为 Thread 停止后无法通过 key 删除对应的数据。

image-20221105223520836

JDK8 以后:每个 Thread 维护一个 ThreadLocalMap,这个 Map 的 key 是 ThreadLocal 实例本身,value 是真正要存储的值。

// Thread类中的属性
ThreadLocal.ThreadLocalMap threadLocals = null;
  • 每个 Thread 线程内部都有一个 Map (ThreadLocalMap),是 ThreadLocal 的一个静态内部类。
  • Map 里面存储 ThreadLocal 对象(key)和线程的私有变量(value)。
  • Thread 内部的 Map 是由 ThreadLocal 维护的,由 ThreadLocal 负责向 map 获取和设置线程的变量值。
  • 对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成副本的隔离,互不干扰。

image-20221105223558028

JDK8 设计方案的两个好处:

  • 每个 Map 存储的 Entry 数量会变少,因为之前的存储数量由 Thread 的数量决定,现在由 ThreadLocal 的数量决定,在实际编程当中,往往 ThreadLocal 的数量要少于 Thread 的数量。
  • 当 Thread 销毁之后,对应的 ThreadLocalMap 也会随之销毁,能减少内存的使用,防止内存泄露

内存结构示意图

image-20221105211449556

  • 每个线程持有一个 ThreadLocalMap 对象,是一个 Entry 数组。
  • 其中的 Entry 的 Key 是 ThreadLocal 对象,value 是对应存储的值。
  • 而一个 ThreadLocal 对象可以被多个不同的线程作为 ThreadLocalMap 的 Key。

image-20221105224839583

一句话理解 ThreadLocal:ThreadLocal 是作为当前线程中属性 ThreadLocalMap 集合中的某一个 Entry 的 key 值 Entry<ThreadLocal, value>,虽然不同的线程之间 ThreadLocal 这个 key 值是一样,但是不同的线程所拥有的 ThreadLocalMap 是独一无二的,也就是不同的线程间同一个 ThreadLocal(key)对应存储的值(value)不一样,从而到达了线程间变量隔离的目的,但是在同一个线程中这个 value 变量地址是一样的。

源码解析

属性及构造方法

public class ThreadLocal<T> {
    /*
     * 线程获取threadLocal.get()时,如果是第一次在某个ThreadLocal对象上get时,会给当前线程分配一个value,
     * 这个value 和当前的threadLocal对象被包装成为一个Entry,
     * 其中key是threadLocal对象,value是threadLocal对象给当前线程生成的value,
     *    --- Entry<ThreadLocal, Value> ---
     * 这个Entry存放到当前线程threadLocals这个map的哪个桶位?与当前threadLocal对象的threadLocalHashCode有关,
     * 使用threadLocalHashCode & (table.length - 1)得到的索引位置,就是当前Entry需要存放的位置。
     */
    private final int threadLocalHashCode = nextHashCode();

	/*
	 * 创建ThreadLocal对象时会使用到,每创建一个threadLocal对象,就会使用nextHashCode 分配一个hash值给这个对象。
	 */
    private static AtomicInteger nextHashCode = new AtomicInteger();

    /*
     * 每为线程创建一个ThreadLocalMap对象,这个ThreadLocal.nextHashCode这个值就会增长0x61c88647,这个值很特殊,它是斐波那契数(黄金分割法)。
     * hash增量为这个数字,带来的好处就是hash分布非常均匀。
     */
    private static final int HASH_INCREMENT = 0x61c88647;
    
	/*
     * 调用顺序:
     * threadLocalHashCode => nextHashCode() => nextHashCode => HASH_INCREMENT
     */
    
    /*
     * 创建新的ThreadLocal对象时 会给当前对象分配一个hash,使用这个方法。 
     */
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

	/*
	 * 默认返回null,一般情况下需要重写该方法
	 */
    protected T initialValue() {
        return null;
    }

	// 构造方法
    public ThreadLocal() {
    }   

ThreadLocal.get()

// -----------------------ThreadLocal.get()------------------------------------
	/*
	 * 返回当前线程与当前ThreadLocal对象相关联的 线程局部变量,这个变量只有当前线程能访问到。
     * 如果当前线程 没有分配,则给当前线程去分配(使用initialValue方法)
	 */
    public T get() {
        // 获取当前线程
        Thread t = Thread.currentThread();
        // 获取当前线程内部的threadLocals(getMap(t) => t.threadLocals)也就是threadLocalMap对象
        ThreadLocalMap map = getMap(t);
        // map不为null,说明当前线程已经拥有自己的ThreadLocalMap对象了
        if (map != null) {
            /*
             * 根据key拿到Entry
             * ThreadLocalMap的Entry的key就是ThreadLocal,所以这里传入this(ThreadLocal对象)尝试获取当前ThreadLoacl关联的Entry
             */
            // ThreadLocalMap.getEntry()方法 在下一篇ThreadLocalMap内核分析
            ThreadLocalMap.Entry e = map.getEntry(this);
            // e不为null,说明当前线程初始化过了与当前ThreadLocal对象相关联的线程局部变量
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                // 直接返回对应的value即可
                return result;
            }
        }

        /*
         * 执行到这里有几种情况?
         * 1.当前线程对应的threadLoaclMap为null,
         * 2.当前线程与当前threadLocal对象没有生成过相关联的线程局部变量(Entry为null)
         */

        // setInitialValue():初始化当前线程与当前threadLocal对象相关联的value
        // 且当前线程内部的threadLoaclMap没有创建的话,还会初始化创建Map
        return setInitialValue();
    }    

// ----------------------------ThreadLocal.getMap()----------------------------

    /**
     * threadLocals是Thread类内部的一个属性,
     * (ThreadLocal.ThreadLocalMap threadLocals;)
     * 可以理解为就是一个Map<ThreadLocal, Value> 
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

ThreadLocal.setInitialValue()

// --------------------------ThreadLocal.setInitialValue()-------------------
	/*
     * setInitialValue():初始化当前线程与当前threadLocal对象相关联的value
     * 且当前线程内部的threadLoaclMap没有创建的话,还会初始化创建Map
     */
	private T setInitialValue() {
        /*
         * 调用当前ThreadLocal对象的initialValue()方法,这个方法,大部分情况我们都会重写,
         * value就是当前ThreadLocal对象与当前线程相关联的线程局部变量。
         */
        T value = initialValue();
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取线程内部的threadLocals => threadLocalMap对象
        ThreadLocalMap map = getMap(t);
        // map不为null,说明当前线程已经初始化过threadLocalMap对象了(线程的threadLocals只会初始化一次)
        if (map != null)
            // 保存当前threadLocal与当前线程生成的线程局部变量
			// key -> 当前threadLocal对象,value -> value
            // ThreadLocalMap.set()方法 在下一篇ThreadLocalMap内核分析
            map.set(this, value);
        else
            /*
             * 执行到这里说明当前线程内部的threadLocalMap对象还未初始化,执行createMap()方法创建threadLocalMap。
             * 参数1:当前线程 
             * 参数2:value
             */            
            createMap(t, value);
        // 返回线程与当前threadLocal相关的局部变量
        return value;
    }

// --------------------------ThreadLocal.createMap()--------------------------
	/*
     * 创建threadLocalMap并赋值给当前线程的threadLocals
     */ 
	void createMap(Thread t, T firstValue) {
    	/*
    	 * 传递t的意义就是 要访问当前这个线程的t.threadLocals字段,给这个字段初始化。
    	 * 为线程t内部的 threadLocals赋值 创建一个ThreadLocalMap对象
    	 * this:当前threadLocal对象
    	 * firstValue:value。
    	 */	    	
        // ThreadLoacalMap的构造方法 在下一篇文章ThreadLocalMap内核分析
        t.threadLocals = new ThreadLocalMap(this, firstValue);
	}

get() 方法流程图

  • 由图可知,调用 get 时如果当前线程内部的 map 未被创建或者 map 中没有当前 threadLocal 对象对应的 Entry,那么最终也会创建出 Map 并且插入相应的Entry。

image-20221106192912193

ThreadLocal.set()

    /*
     * 修改当前线程与当前threadLocal对象相关联的 线程局部变量。
     */
	public void set(T value) {
        // 获取当前线程
        Thread t = Thread.currentThread();
        // 获取当前线程内部的threadLocals(threadLocalMap)
        ThreadLocalMap map = getMap(t);
        // 条件成立:说明当前线程的threadLocalMap已经初始化了
        if (map != null)
            // 重写 或者 添加value
            // ThreadLocalMap.set()方法 在下一篇ThreadLocalMap内核分析
            map.set(this, value);
        else
            // 创建线程内部的threadLocalMap并设置value
            createMap(t, value);
    }

ThreadLocal.set() 方法流程图

在这里插入图片描述

ThreadLocal.remove()

  • 将当前线程内部的 ThreadLocalMap 中 key 为当前 threadLocal 对象的 Entry 干掉(具体源码见 ThreadLocalMap 解析)
// --------------------ThreadLocal.remove()-------------------------------
	public void remove() {
    	// 获取当前线程内部的ThreadLocalMap对象
        ThreadLocalMap m = getMap(Thread.currentThread());
        // m不为null,说明说当前线程已经初始化过 threadLocalMap对象了
        if (m != null)
            // 调用remove方法(key = 当前threadLocal对象)
            // ThreadLocalMap.remove()方法 在下一篇ThreadLocalMap内核分析
            m.remove(this);
	}

小结

  • 我们只分析了 ThreadLocal 表层的 get()、set()、remove() 方法,表层的方法实现非常简单,因为复杂的逻辑都在 ThreadLocalMap 内核中。
  • 比如 ThreadLocalMap 中的 getEntry()、set()、remove() 等方法,这些方法才是 ThreadLocal 的真正实现,在下一篇文章分析



参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小成同学_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值