ThreadLocal源码分析_01 入门案例以及表层源码分析

Posted 兴趣使然の草帽路飞

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ThreadLocal源码分析_01 入门案例以及表层源码分析相关的知识,希望对你有一定的参考价值。

1、ThreadLocal简介

  • ThreadLocal是一个全局对象,ThreadLocal是线程范围内变量共享的解决方案;ThreadLocal可以看作是一个map集合,key就是当前线程,value就是要存放的变量。eg如下:
// 声明一个ThreadLocal实例
ThreadLocal threadLocal = new ThreadLocal();
// 随机获取一个数字n
int n = new Random().nextInt();
// 存放数据n给threadLocal:
threadLocal.set(data);
// 从threadLocal中取出数据n
threadLocal.get();
  • ThreadLocal的引用变量用完后会自动给你销毁,而不用考虑ThreadLocal中的变量会占用空间。
  • 在ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的key为线程对象,value为对应的线程的变量副本。
  • ThreadLocal的作用:
    • ThreadLocal通过为每一个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。
    • 传递数据:可以通过ThreadLocal在同一线程的不同组件中传递公共变量。
    • 线程隔离:每个线程的变量都是独立的,不会相互影响。

2、入门案例

/**ThreadLocal通过为每一个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。
 * @author csp1999
 * @date 2021/02/21
 */
public class ThreadLocalTest {
    /**
     * 原子整数,一个分配给线程的 Thread ID
     */
    private static final AtomicInteger nextId = new AtomicInteger(0);

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

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

    static class RunnableTask implements Runnable {

        @Override
        public void run() {
            try {
                System.out.println(
                        "当前线程名称为:" + Thread.currentThread().getName() +
                                ",分配的Id为:" + threadId.get()
                );
            } finally {
                threadId.remove();
            }
        }
    }

    // 测试程序
    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);
    }
}

运行结果如下:

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

由上面这个入门案例,可以看出:ThreadLocal每个线程的变量(nextId)都是独立的,不会相互影响。


3、源码分析

一、成员属性

// threadLocalHashCode ---> 用于threadLocals的桶位寻址:
// 1.线程获取threadLocal.get()时:
// 		如果是第一次在某个threadLocal对象上get,那么就会给当前线程分配一个value,
// 		这个value 和 当前的threadLocal对象被包装成为一个 entry 
// 		其中entry的 key 是threadLocal对象,value 是threadLocal对象给当前线程生成的value
// 2.这个entry存放到当前线程 threadLocals 这个map的哪个桶位呢? 
//		桶位寻址与当前 threadLocal对象的 threadLocalHashCode有关系:
// 		使用 threadLocalHashCode & (table.length - 1) 计算结果得到的位置就是当前 entry 需要存放的位置。
private final int threadLocalHashCode = nextHashCode();

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

// HASH_INCREMENT: 表示hash值的增量~
// 每创建一个ThreadLocal对象,ThreadLocal.nextHashCode的值就会增长HASH_INCREMENT(0x61c88647)。
// 这个值很特殊,它是斐波那契数也叫黄金分割数。
// hash增量为这个数字,带来的好处就是hash分布非常均匀。
private static final int HASH_INCREMENT = 0x61c88647;

/**
 * 返回一个nextHashCode的hash值:
 * 创建新的ThreadLocal对象时,使用这个方法,会给当前对象分配一个hash值。
 */
private static int nextHashCode() {
    // 每创建一个对象,nextHashCode计算得到的hash值就增长HASH_INCREMENT(0x61c88647)
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

/*
 * 初始化一个起始value:
 * 默认返回null,一般情况下都是需要重写这个方法的(例如第2小节的入门案例中就重写了该方法)。
 */
protected T initialValue() {
    return null;
}

二、构造方法

  • 一个空参构造,什么也没做,不再分析,继续往下学习!
public ThreadLocal() {
}

三、成员方法

get方法

/**
 * 返回当前线程与当前ThreadLocal对象相关联的线程局部变量,这个变量只有当前线程能访问
 * 如果当前线程没有分配局部变量,则使用 initialValue方法去分配初始局部变量值!
 */
public T get() {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // getMap(t):获取到当前线程Thread对象的ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
    // 如果条件成立:说明当前线程已经拥有自己的ThreadLocalMap对象了
    if (map != null) {
        // key:当前threadLocal对象(this)
        // 根据key调用map.getEntry()方法,获取threadLocalMap中该threadLocal关联的entry
        ThreadLocalMap.Entry e = map.getEntry(this);
        
        // 如果条件成立(当前获取的entry不为空):
        // 说明当前线程初始化过与当前threadLocal对象相关联的线程局部变量!
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            // 返回value值
            return result;
        }
    }

    // 执行到这里有几种情况?
    // 情况1:当前线程对应的threadLocalMap是空
    // 情况2:当前线程与当前threadLocal对象没有生成过相关联的线程局部变量..

    // setInitialValue方法初始化当前线程与当前threadLocal对象相关联的线程局部变量value值,
    // 且当前线程如果没有threadLocalMap的话,还会初始化创建map!
    return setInitialValue();
}

// 获取当前线程t的ThreadLocalMap对象
// ThreadLocalMap(位于Thread类中)
ThreadLocalMap getMap(Thread t) {
    // 返回当前线程的 threadLocals
    return t.threadLocals;
}

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

    // 返回线程与当前threadLocal相关的局部变量
    return value;
}

/**
 * 创建当前线程的ThreadLocalMap对象
 */
void createMap(Thread t, T firstValue) {
    // 传递t的意义就是要访问当前这个线程 t.threadLocals字段,给这个字段初始化:
    // new ThreadLocalMap(this, firstValue):
    // 创建一个ThreadLocalMap对象,初始k-v为: 
    // key:this <当前threadLocal对象> 
    // value:线程与当前threadLocal相关的局部变量
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

set方法

/**
 * 修改当前线程与当前threadLocal对象相关联的线程局部变量:
 */
public void set(T value) {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取当前线程的threadLocalMap对象
    ThreadLocalMap map = getMap(t);
    // 如果条件成立:说明当前线程的threadLocalMap已经初始化过了
    if (map != null)
        // 调用threadLocalMap.set方法进行重写或者添加:
        map.set(this, value);
    
    // 如果执行到else ---> 说明当前线程内部threadLocalMap对象还没有初始化过:
    else
		// 这里调用createMap方法给当前线程创建ThreadLocalMap对象:
        // 参数1:当前线程t
        // 参数2:线程与当前threadLocal相关的局部变量
        createMap(t, value);
}

remove方法

/**
 * 移除当前线程与当前threadLocal对象相关联的线程局部变量:
 */
 public void remove() {
     // 获取当前线程的threadLocalMap对象
     ThreadLocalMap m = getMap(Thread.currentThread());
     // 如果条件成立:说明当前线程已经初始化过threadLocalMap对象了
     if (m != null)
         // 调用threadLocalMap.remove( key = 当前threadLocal)移除线程局部变量
         m.remove(this);
 }

4、小节

  • 如果单看ThreadLocal其内部的相关方法,成员属性等,其实难度并不大,但是ThreadLocal并不止于此,其真正的内核为Thread类中的ThreadLocalMap(比较复杂)。

  • ThreadLocalMap的源码分析留在下一篇文章中介绍!

  • 在学习ThreadLocalMap内核前,先来梳理一下ThreadLocal的执行流程:

在这里插入图片描述

流程总结:

  • 每个线程都有自己的ThreadLocalMap对象;(ThreadLocal 多线程下资源隔离的根本原因)。
  • 各个线程在调用同一个ThreadLocal对象的set(value)设置值的时候,是往各自的ThreadLocalMap对象数组中设置值。
  • 至于当前值放置在数组中的下标位置,则是通过ThreadLocal对象的threadLocalHashCode计算而来。即多线程环境下ThreadLocal对象的threadLocalHashCode是共享的。
  • ThreadLocal对象的threadLocalHashCode是一个原子自增的变量,通过类方法initValue初始化值。
    即:当实例化ThreadLocal对象ThreadLocal local = new ThreadLocal();时,就会初始化threadLocalHashCode的值,这个值不会再变。所以,同一个线程在同一个ThreadLocal对象中set()值,只能保存最后一次set的值。
  • 为什么每个线程都有自己的ThreadLocalMap对象,且是一个数组呢?
    • 答:根据以上的分析,多个线程操作一个ThreadLocal对象就能达到线程之间资源隔离。而采用数组是因为可能一个线程需要通过多个ThreadLocal对象达到多个资源隔离。每个不同的ThreadLocal对象的threadLocalHashCode都不一样,也就映射到ThreadLocalMap对象数组下的不同下标。
  • 每个线程的ThreadLocalMap对象是通过偏移位置的方式解决hash碰撞。
  • 每个线程都有自己的ThreadLocalMap对象也有扩容机制,且是天然线程安全的。

以上是关于ThreadLocal源码分析_01 入门案例以及表层源码分析的主要内容,如果未能解决你的问题,请参考以下文章

ThreadLocal源码分析_02 内核(ThreadLocalMap)

ThreadLocal源码分析_02 内核(ThreadLocalMap)

ThreadLocal源码夺命12问,你能坚持到第几问?

Java 实习生每日面试题打卡——ThreadLocal源码夺命12问,你能坚持到第几问?

ThreadLocal定义使用案例及源码分析

Java Review - 线程池中使用ThreadLocal不当导致的内存泄漏案例&源码分析