【JDK源码】线程系列之ThreadLocal

简介

ThreadLocal
这个类提供线程局部变量,这些变量与其他正常的变量的不同之处在于,每一个访问该变量的线程在其内部都有一个独立的初始化的变量副本
ThreadLocal实例变量通常采用 private static 在类中修饰。

只要 ThreadLocal 的变量能被访问,并且线程存活,那每个线程都会持有 ThreadLocal
变量的副本。当一个线程结束时,它所持有的所有 ThreadLocal 相对的实例副本都可被回收。

一句话说就是 ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用(相同线程数据共享),也就是变量在线程间隔离(不同的线程数据隔离)而在方法或类间共享的场景。

  • ThreadLocal 提供了一种访问某个变量的特殊方式:访问到的变量属于当前线程,即保证每个线程的变量不一样,而同一个线程在任何地方拿到的变量都是当前这个线程私有的,这就是所谓的线程隔离

ThreadLocal 的使用场景:

  • 对同一个线程调用的多个方法中,共享了某一个变量,这个变量需要传递到多个方法中,这样传来传去太麻烦了,这时就可以采用 ThreadLocal 了。

  • 存储单个线程上下文信息。比如存储id等;

  • 使变量线程安全。变量既然成为了每个线程内部的局部变量,自然就不会存在并发问题了;

原理

对象实例与 ThreadLocal 变量的映射关系是由线程 Thread 来维护的

其实就是对象实例与 ThreadLocal 变量的映射关系是存放的一个 Map 里面(这个 Map 是个抽象的 Map 并不是 java.util 中的 Map ),而这个 MapThread 类的一个字段!而真正存放映射关系的 Map 就是 ThreadLocalMap。ThreadLocalMap保存的是Entry结点,Entry结点中保存了ThreadLocal对象和threadLocal对象相关联的线程局部变量

入门案例

  • 多线程操作对象
public class Demo01 {
    private String string;

    private String getString() {
        return string;
    }

    private void setString(String string) {
        this.string = string;
    }

    public static void main(String[] args) {
        int threads = 9;
        final Demo01 demo = new Demo01();
        // CountDownLatch countDownLatch = new CountDownLatch(threads);
        for (int i = 0; i < threads; i++) {
            Thread thread = new Thread(() -> {
                if (demo.getString() == null){
                    demo.setString(Thread.currentThread().getName());
                }
                System.out.println("demo.getString()================>"+ demo.getString());
                //countDownLatch.countDown();
            }, "执行线程 - " + i);
            thread.start();
        }
    }
}
  • 结果发现多个线程共用了一个demo
demo.getString()================>执行线程 - 0
demo.getString()================>执行线程 - 0
demo.getString()================>执行线程 - 0
demo.getString()================>执行线程 - 0
demo.getString()================>执行线程 - 0
demo.getString()================>执行线程 - 0
demo.getString()================>执行线程 - 0
demo.getString()================>执行线程 - 0
demo.getString()================>执行线程 - 0
  • 还是多线程,采用ThreadLocal来封装对象
public class Demo01 {
    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    private String getString() {
        return threadLocal.get();
    }

    private void setString(String string) {
        threadLocal.set(string);
    }

    public static void main(String[] args) {
        int threads = 9;
        Demo01 demo = new Demo01();
        CountDownLatch countDownLatch = new CountDownLatch(threads);
        for (int i = 0; i < threads; i++) {
            Thread thread = new Thread(() -> {
                if (demo.getString() == null){
                    demo.setString(Thread.currentThread().getName());
                }
                //demo.setString(Thread.currentThread().getName());
                System.out.println("demo.getString()================>" + demo.getString());
                countDownLatch.countDown();
            }, "执行线程 - " + i);
            thread.start();
        }
    }
}
  • 发现多线程互不干扰,资源隔离
demo.getString()================>执行线程 - 0
demo.getString()================>执行线程 - 1
demo.getString()================>执行线程 - 2
demo.getString()================>执行线程 - 3
demo.getString()================>执行线程 - 4
demo.getString()================>执行线程 - 5
demo.getString()================>执行线程 - 6
demo.getString()================>执行线程 - 7
demo.getString()================>执行线程 - 8

源码分析

  • 独立

在这里插入图片描述

成员属性

/**
* hreadLocalHashCode ---> 用于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对象时,就会使用 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() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}
/*
* 初始化一个起始value:
* 默认返回null,一般情况下都是需要重写这个方法的
*/
protected T initialValue() {
    return null;
}

构造方法

  • 空构造
public ThreadLocal() {
}

成员方法

get()
  • 返回当前线程与当前ThreadLocal对象相关联的线程局部变量,这个变量只有当前线程能访问,其中涉及getMapgetEntrysetInitialValue
/**
 * 如果当前线程没有分配局部变量,则使用 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对象与当前threadLocal对象相关联的线程局部变量!
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            // 返回value值
            return result;
        }
    }

    // 执行到这里有几种情况?
    // 情况1:当前线程对应的threadLocalMap是空
    // 情况2:当前线程与当前threadLocal对象没有生成过相关联的线程局部变量,也就是当前threadLocal对象没有被初始化到table中

    // setInitialValue方法初始化当前线程与当前threadLocal对象相关联的线程局部变量value值,
    // 且当前线程如果没有threadLocalMap的话,还会初始化创建map!
    return setInitialValue();
}
  • getMap 获取当前线程t的ThreadLocalMap对象
// ThreadLocalMap(位于Thread类中)
ThreadLocalMap getMap(Thread t) {
    // 返回当前线程的 threadLocals
    return t.threadLocals;
}
  • getEntry 得到当前的ThreadLocal实例在table中的ThreadLocal对象(存有当前threadLocal相关的局部变量)
private Entry getEntry(ThreadLocal<?> key) {
    // 传入ThreadLocal引用的key,然后通过按位与找到key在map中的位置
    int i = key.threadLocalHashCode & (table.length - 1);
    // 取出此Entry(里面存着ThreadLocal对象,和threadLocal对象相关联的线程局部变量)
    Entry e = table[i];
    // 非空判断,是否与传入的ThreadLocal对象相同
    if (e != null && e.get() == key)
        return e;
    else
        // 走到这里说明两个不同的ThreadLocal对象hash到了同一个位置,然后就从此位置一次往后找,没找到就返回null
        return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    while (e != null) {
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        // e不等于null,而k等于null 说明当前e已经过期了,就把e清理掉
        if (k == null)
            expungeStaleEntry(i);
        else
            // 往后依次找
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}
private static int nextIndex(int i, int len) {
    // 往后移一位
    return ((i + 1 < len) ? i + 1 : 0);
}
  • setInitialValue 初始化当前线程与当前threadLocal对象相关联的线程局部变量value值
/**
 * setInitialValue方法初始化当前线程与当前threadLocal对象相关联的线程局部变量value值,
 * 且当前线程如果没有threadLocalMap的话,还会初始化创建map!
 * @return the initial value
 */
private T setInitialValue() {
    // 调用的当前ThreadLocal对象的initialValue方法,这个方法大部分情况下咱们都会重写来给当前 ThreadLocal 对象赋初始值。
    // 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;
}
  • createMap 创建当前线程的ThreadLocalMap对象
/**
 * 创建当前线程的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);
}
  • map.set
private void set(ThreadLocal<?> key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    Entry[] tab = table;
    int len = tab.length;
    // 找到key的位置
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
        // 如果该位置原来就有ThreadLocal对象,就直接覆盖
        if (k == key) {
            e.value = value;
            return;
        }
        // 如果该位置没有,说明当前entry是过期数据(因为e!=null,说明已经初始化过),这个时候可以强行占用该桶位
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
	// e=null,就直接初始化一个Entry,里面存有ThreadLocal对象和其threadLocal相关的局部变量
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}
remove方法
/**
 * 移除当前线程与当前threadLocal对象相关联的线程局部变量:
 */
 public void remove() {
     // 获取当前线程的threadLocalMap对象
     ThreadLocalMap m = getMap(Thread.currentThread());
     // 如果条件成立:说明当前线程已经初始化过threadLocalMap对象了
     if (m != null)
         // 调用threadLocalMap.remove( key = 当前threadLocal)移除线程局部变量
         m.remove(this);
 }

总结

  • 每个线程都有自己的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对象也有扩容机制,ThreadLocalMap 的扩容阈值为初始容量的 2/3,当数组中,存储 Entry 节点的个数大于等于 2/3 时,会它并不会直接开始扩容。而是先调用 rehash()方法,在该方法中,全面扫描整个数组,并将数组中过期的数据(key == null)给清理掉,重新整理数组。如果重新整理数组,并将过期的数据清理后,再次重新判断数组内的 Entry 节点的个数是否达到扩容阈值的3/4,如果达到再调用真正扩容的方法resize();

resize() 方法内部?

  • resize() 方法在真正执行扩容时,内部逻辑是先创建一个新的数组,新数组长度是原来数组长度的 2 倍。
  • 然后遍历旧数组,将旧数组中的数据重新按照 hash 算法迁移到新数组里面。
  • 接着重新计算出下次扩容的阈值threshold

强引用-软引用-弱引用

  • 强引用:普通的引用,强引用指向的对象不会被回收
  • 软引用:仅有软引用指向的对象,只有发生gc且内存不足,才会被回收;
  • 弱引用:仅有弱引用指向的对象,只要发生gc就会被回收
public static void main(String[] args) {

    Object a = new Object();
    Object b = new Object();
    Object c = new Object();

    Object strongA = a;
    SoftReference<Object> softB = new SoftReference<>(b);
    WeakReference<Object> weakC = new WeakReference<>(c);

    a = null;
    b = null;
    c = null;

    System.out.println("Before gc...");
    System.out.println(String.format("strongA = %s, softB = %s, weakC = %s", strongA, softB.get(), weakC.get()));
    System.out.println("Run GC...");

    System.gc();

    System.out.println("After gc...");
    System.out.println(String.format("strongA = %s, softB = %s, weakC = %s", strongA, softB.get(), weakC.get()));
}
Before gc...
strongA = java.lang.Object@61bbe9ba, softB = java.lang.Object@610455d6, weakC = java.lang.Object@511d50c0
Run GC...
After gc...
strongA = java.lang.Object@61bbe9ba, softB = java.lang.Object@610455d6, weakC = null

问题

ThreadLocalMap的问题

ThreadLocal在ThreadLocalMap中是以一个弱引用身份被Entry中的Key引用的

由于Entry的key是弱引用,而Value是强引用。这就导致了一个问题,ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key会被回收,而Value不会回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露

为什么使用弱引用?

  • key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
  • key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。但是这个key==null,value!=null。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
  • 比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。

如何避免泄漏

既然Key是弱引用,那么我们要做的事,就是在调用ThreadLocal的get()、set()方法时完成后再调用remove方法,将Entry节点和Map的引用关系移除并且还会调用expungeStaleEntry移除key=null的Entry,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。

Hash冲突怎么解决

和HashMap的最大的不同在于,ThreadLocalMap结构非常简单,没有next引用,也就是说ThreadLocalMap中解决Hash冲突的方式并非链表的方式,而是采用线性探测的方式,所谓线性探测,就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。

/**
 * Increment i modulo len.
 */
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}
 
/**
 * Decrement i modulo len.
 */
private static int prevIndex(int i, int len) {
    return ((i - 1 >= 0) ? i - 1 : len - 1);
}

总结

  • ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。
  • ThreadLocal的作用:实现线程范围内的局部变量,即ThreadLocal在一个线程中是共享的,在不同线程之间是隔离的

参考
ThreadLocal源码分析_02 内核(ThreadLocalMap)
ThreadLocal原理详解——终于弄明白了ThreadLocal
深挖ThreadLocal
ThreadLocal原理及内存泄露预防

评论 48
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

早上真起不来!

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

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

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

打赏作者

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

抵扣说明:

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

余额充值