从源码分析ThreadLocal的原理

本文参考并摘抄自java基础教程由浅入深全面解析threadlocal

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

基本使用

package com.fxsh.ThreadLocal;

import lombok.extern.slf4j.Slf4j;

/*
 *       需求:线程隔离
 *           在多线程并发的场景下,每个线程中的变量都是相互独立的
 *           线程A:设置(变量1) 获取(变量1)
 *           线程B: 设置(变量2) 获取(变量2)
 *           ThreadLocal:
 *               1. set():将变量绑定到当前线程
 *               2. get():获取当前线程绑定的变量
 * */
@Slf4j(topic = "tl.d1")
public class MyDemo01 {
    private String content;

    ThreadLocal<String> threadLocal = new ThreadLocal<>();
    public String getContent() {
        //return content;
        return threadLocal.get();
    }

    public void setContent(String content) {
//        this.content = content;
        //变量content绑定到当前变量
        threadLocal.set(content);
    }

    public static void main(String[] args){
        MyDemo01 threadLocalTest = new MyDemo01();

        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                /*
                 *   每个线程绑定一个变量,等一会儿获取这个变量
                 * */
                threadLocalTest.setContent(Thread.currentThread().getName()+"的数据");
                log.debug("--------------");
                log.debug(Thread.currentThread().getName()+"-----" + threadLocalTest.getContent());
            },"thread"+i).start();
        }
    }
}


ThreadLocal类和synchronized

@Slf4j(topic = "tl.d2")
public class MyDemo02 {
    private String content;

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public static void main(String[] args){
        MyDemo01 threadLocalTest = new MyDemo01();

        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                /*
                 *   每个线程绑定一个变量,等一会儿获取这个变量
                 * */
                synchronized (MyDemo02.class){
                    threadLocalTest.setContent(Thread.currentThread().getName()+"的数据");
                    log.debug("--------------");
                    log.debug(Thread.currentThread().getName()+"-----" + threadLocalTest.getContent());
                }

            },"thread"+i).start();
        }
    }
}

能够解决问题,但是加锁,导致并发性能降低

ThreadLocal类和synchronized的区别

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

ThreadLocal的内部结构

在JDK8之前:

每个ThreadLocal都创建一个Map,然后用线程作为Mapkey,要存储的局部变量作为Mapvalue,这样就能达到各个线程的局部变量的隔离效果。

从源码分析ThreadLocal的原理_第1张图片

JDK8开始:

每个Thread维护一个ThreadLocalMap,这个Map的keyThreadLocal实例本身,value才是真正要存储的值Object

具体的过程是这样的:

​ 1) 每个Thread线程内部都有一个Map(ThreadLocalMap)

​ 2) Map里面存储ThreadLocal对象(key)和线程的变量副本(value)

​ 3) Thread内部的Map是有ThreadLocal维护的,由ThreadLocal负责向Map中获取和设置线程的变量值

​ 4) 对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本隔离,互不干扰。

从源码分析ThreadLocal的原理_第2张图片

JDK8设计方案好处:

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

ThreadLocal的核心方法源码

除了构造方法,ThreadLocal对外暴露了一下4个方法:

方法声明 描述
protected T initialValue() 返回当前线程局部变量的初始值
public void set(T value) 设置当前线程绑定的局部变量
public T get() 获取当前线程绑定的局部变量
public void remove() 移除当前线程绑定的局部变量

set方法

public void set(T value) {
    //获取当前线程对象
    Thread t = Thread.currentThread();
    //获取此线程对象中维护的ThreadLocalMap
    ThreadLocal.ThreadLocalMap map = this.getMap(t);
    //判断Map是否存在
    if (map != null) {
        //Map存在,则设置此实体entry
        map.set(this, value);
    } else {
        //1)当前线程Thread不存在ThreadLocalMap对象
        //2) 调用createMap进行ThreadLocalMap对象的初始化
        //3) 并将当前线程t和value作为第一个entry存放至ThreadLocalMap中
        this.createMap(t, value);
    }

}
ThreadLocal.ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocal.ThreadLocalMap(this, firstValue);
}

执行流程:

1)首先获取当前线程,并根据当前线程获取ThreadLocalMap

2)如果map不为空,则将参数设置到Map中(当前ThreadLocal为key)

3)如果Map为空,那么给该线程创建Map,并设置参数

get方法

public T get() {
    //获取当前线程对象
    Thread t = Thread.currentThread();
    //获取当前线程维护的ThreadLocalMap对象
    ThreadLocal.ThreadLocalMap map = this.getMap(t);
    //如果ThreadLocalMap对象不为空
    if (map != null) {
        //获取以当前ThreadLocal为Key的实体e
        ThreadLocal.ThreadLocalMap.Entry e = map.getEntry(this);
        //如果e不为空
        if (e != null) {
            //获取存储实体e对应的value值
            //即为我们想要的当前线程对应此ThreadLocal的值
            T result = e.value;
            return result;
        }
    }
	/*
		初始化:有两种情况会执行当前代码
		1:当前线程的ThreadLocalMap对象不存在
		2:当前线程的ThreadLocalMap对象存在,但是没有与当前ThreadLocal对象管理的Entry
	*/
    return this.setInitialValue();
}
private T setInitialValue() {
    //initialValue()方法可以被子类重写,如果不重写,默认返回null
    T value = this.initialValue();
    //获取当前线程
    Thread t = Thread.currentThread();
    //获取当前线程维护的ThreadLocalMap对象
    ThreadLocal.ThreadLocalMap map = this.getMap(t);
    //对map判空
    if (map != null) {
        //存在则设置此实体entry
        map.set(this, value);
    } else {
        //1)当前线程Thread不存在ThreadLocalMap对象
        //2) 调用createMap进行ThreadLocalMap对象的初始化
        //3) 并将当前线程t和value作为第一个entry存放至ThreadLocalMap中
        this.createMap(t, value);
    }

    if (this instanceof TerminatingThreadLocal) {
        TerminatingThreadLocal.register((TerminatingThreadLocal)this);
    }
	//返回设置的value
    return value;
}

执行流程:

1)首先获取当前线程,并根据当前线程获取ThreadLocalMap

2)如果map不为空,则根据当前ThreadLocal获取对应的实体e;如果map为空,则跳转到第4步

3)如果实体e不为空,那么返回e对应的value值,否则跳转到第4步

4)调用initialValue()方法,获取初始值value,然后使用当前ThreadLocal和初始值value初始化当前线程维护的ThreadLocalMap。然后返回初始值value。

总结:先获取当前线程的ThreadLocalMap变量,如果存在则返回值,不存在则创建并返回初始值。

remove方法

public void remove() {
    //获取当前线程对象中维护的ThreadLocalMap对象
    ThreadLocal.ThreadLocalMap m = this.getMap(Thread.currentThread());
    //如果map存在
    if (m != null) {
        //调用map.remove,删除key为当前ThreadLocal的记录
        m.remove(this);
    }

}

执行流程:

1)首先获取当前线程,并根据当前线程获取ThreadLocalMap

2)如果map不为空,则移除当前ThreadLocal对象对应的entry

initialValue方法

/*
	返回当前线程对应ThreadLocal的初始值
	
	此方法的第一次调用发生在:当前线程通过get方法调用此线程的ThreadLocal值时
	如果先调用了set方法,那么该方法将不会被调用
	通常情况下,每个线程最多调用一次这个方法

*/
protected T initialValue() {
    return null;
}
  • 这个方法是一个延迟调用方法,只有在还未调用set方法就调用get方法时,该方法才会被调用,且只被调用一次
  • 这个方法缺省实现直接返回null
  • 如果想要一个除null之外的初始值,可以重写这个方法。

ThreadLocalMap源码分析

基本结构

ThreadLocalMapThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也是独立实现的,它们的类图如图所示:

从源码分析ThreadLocal的原理_第3张图片

(1)成员变量

//初始容量 -- 必须是2的整次幂
private static final int INITIAL_CAPACITY = 16;
//存放数据的table
private ThreadLocal.ThreadLocalMap.Entry[] table;
//数组里面entrys的个数,用于判断是否超过阈值
private int size = 0;
//进行扩容的阈值
private int threshold;//Default to 0

(2) 存储结构 - Entry

/*
	Entry继承WeakReference,并且用ThreadLocal作为Key
	如果key为null(entry.get()==null),意味着key不再被引用
	因此这时候entry也可以从table中清除
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        //调用WeakReference的构造函数
        //意味着一个entry对象就是一个ThreadLocal的弱引用
        super(k);
        this.value = v;
    }
}

Entry继承WeakReference,也就是key(ThreadLocal)的一个弱引用,其目的是将ThreadLocal对象的生命周期和线程的生命周期解绑。

弱引用和内存泄露

(1)内存泄露相关概念

  • Memory overflow:内存溢出,没有足够的内存供申请者使用
  • Memory leak:内存泄露是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。内存泄漏的推挤终将导致内存溢出。

(2) 弱引用相关概念

​ Java中的引用有4中类型:强、软、弱、虚。当前这个问题主要涉及到强引用和弱引用:

强引用(Strong Reference),就是我们最常见的普通对象引用,只要还有强引用指向对象,就能表明对象还“活着”,垃圾回收器就不会回收这种对象。

弱引用(WeakReference),垃圾回收器一旦发现只具有弱引用的对象,不管当前内存空间是否足够,都会回收它的内存。

(3)如果key使用强引用

​ 堆栈中的引用关系如图所示:

从源码分析ThreadLocal的原理_第4张图片

假设业务中使用完ThreadLocal,threadLocalRef被回收了(可能是方法调用结束,栈帧出栈),此时堆栈中的引用关系如下图:

从源码分析ThreadLocal的原理_第5张图片

但是如果线程还没有结束,因为ThreadLocalMapEntry强引用了ThreadLocal,所以导致ThreadLocal无法被回收,从而导致内存泄漏。

如果Entry是一个对ThreadLocal的弱引用,那么在threadLocalRef被回收后,堆中的ThreadLocal对象可以被回收掉,因为只有弱引用指向它了。被回收后堆栈中的引用关系如图:

从源码分析ThreadLocal的原理_第6张图片

(4)真实内存泄漏原因

​ 如上图所示,当ThreadLocal对象被回收之后,如果线程还没有结束,那么该线程中ThreadLocalMap中对应于该ThreadLocalEntry的key的值变为null,导致整个value都无法被访问到,因此任然存在内存泄漏(即My Value 内存块无法被回收)。

为了避免这种类型的内存泄露,正确的做法是:

  • 使用完ThreadLocal,调用其remove方法删除对应的Entry

  • 使用完ThreadLocal,当前线程也随之结束。

ThreadLocalMap中hash冲突的解决

ThreadLocalset方法中,如果当前线程的ThreadLocalMap不存在,就会创建当前线程的ThreadLocalMap

(1)构造方法:

static class ThreadLocalMap {
    private static final int INITIAL_CAPACITY = 16;
    private ThreadLocal.ThreadLocalMap.Entry[] table;
    private int size = 0;
    private int threshold;

    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        //初始化table
        this.table = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY];
        //计算索引,重点
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY-1);
        //设置值
        this.table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue);
        this.size = 1;
        //设置阈值,初始容量的2/3
        this.setThreshold(INITIAL_CAPACITY);
    }
    private void setThreshold(int len) {
        this.threshold = len * 2 / 3;
    }
}

构造函数首先创建一个长度为16的Entry数组,然后计算firstKey对应的索引,然后存储到table中,并设置sizethreshold

重点分析:int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY-1);

  • 关于firstKey.threadLocalHashCode:

    public class ThreadLocal<T> {
        private final int threadLocalHashCode = nextHashCode();
        private static AtomicInteger nextHashCode = new AtomicInteger();
        private static final int HASH_INCREMENT = 0x61c88947;
    
        private static int nextHashCode() {
            //获取当前值,并加上HASH_INCREMENT
            //按照ThreadLocalMap的创建顺序,其threadLocalHashCode的值依次为:
            //0,HASH_INCREMENT,2*HASH_INCREMENT,3*HASH_INCREMENT....
            return nextHashCode.getAndAdd(HASH_INCREMENT);
        }
    }
    

    HASH_INCREMENT = 0x61c88947这个数跟*斐波那契数列(黄金分割数)*有关,其主要目的就是为了让哈希码能够均匀的分布在2的n次方的数组里。这样可以尽量避免hash冲突。

  • 关于& (INITIAL_CAPACITY-1)

    计算索引的时候采用hashCode & (size-1)的算法,这相当于取模运算hashCode%size的一个更加高效的实现。正因为这种算法,我们要求size必须是2的整次幂,这也能保证在索引不越界的前提下,是的hash发生冲突的次数减小。

(2)set方法:

private void set(ThreadLocal<?> key, Object value) {
    ThreadLocal.ThreadLocalMap.Entry[] tab = this.table;
    int len = tab.length;
    //计算索引
    int i = key.threadLocalHashCode & len - 1;
	/*
		使用线性探测法解决hash冲突
	*/
    for(ThreadLocal.ThreadLocalMap.Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = (ThreadLocal)e.get();
        //ThreadLocal对应的key存在,直接覆盖之前的值
        if (k == key) {
            e.value = value;
            return;
        }
		//key为null,说明之前的ThreadLocal对象已经被回收了
        //tab[i]指向的Entry是一个陈旧的元素
        if (k == null) {
            //用新元素替换旧的元素,这个方法进行了不少垃圾清理动作,防止内存泄露
            this.replaceStaleEntry(key, value, i);
            return;
        }
    }
	//在线性探测的过程中,如果没有发生hash冲突:
    //则直接在key.threadLocalHashCode & len - 1下标下存放新的Entry
    //在线性探测的过程中,发生hash冲突,但是冲突的位置下存放的entry都是正常的(原key不为空且不等于新key):
    //将会在tab的一个空闲下标下存放新的Entry
    tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value);
    int sz = ++this.size;
    //cleanSomeSlots(i, sz)用于清理那些e.get()==null的元素(非全表清理)
    //如果没有清理任何entry,并且当前tab的使用量达到阈值,那么进行rehash
    //rehash中进行一次全表清理
    //如果清理之后tab中entry个数大于等于threadshold-threshold / 4(即本次清理的个数小于阈值的四分之一)
    //将tab扩容到原来的2倍
    if (!this.cleanSomeSlots(i, sz) && sz >= this.threshold) {
        this.rehash();
    }

}

你可能感兴趣的:(java,#,并发)