一文彻底搞懂ThreadLocal

ThreadLocal

前言:在处理多线程并发安全的方法中,我们最常用的是通过锁来控制不同线程对临界区的访问,但即使加入乐观锁或悲观锁来控制多线程对于共享变量的控制,也会存在并发冲突,同时造成性能的影响。目前我们想彻底避免竞争,那threadLocal则是一个很好的选择了。

ThreadLocal是什么

定义:threadLocal顾名思义作为的是本地线程变量,其作用主要就是做数据隔离,保证在多线程环境下每个线程都独立包含一个变量,防止其他线程篡改。它提供了一种将可变数据通过每个线程有自己的独立副本从而实现线程封闭的机制。

private ThreadLocal<Integer> threadLocal = new ThreadLocal<>();	//创建一个ThreadLocal对象

ThreadLocal的作用

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

  • 传递数据: 我们可以通过ThreadLocal在同一线程,不同组件中传递公共变量

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

ThreadLocal的常用方法

方法声明描述
ThreadLocal()提供无参构造用于创建ThreadLocal对象
public void set( T value)设置当前线程的局部变量值
public T get()获取当前线程的局部变量值
public void remove()删除当前线程绑定的局部变量(避免内存泄漏)

ThreadLocal与synchronized

  • 多线程场景下无锁无本地线程变量:
public class ThreadLocalDemo1 {
    private String content;
    public String getContent() {
        return content;
    }
    public void setContent(String content) {
        this.content = content;
    }
    public static void main(String[] args) {
        ThreadLocalDemo1 threadLocalDemo1 = new ThreadLocalDemo1();
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(() -> {
                threadLocalDemo1.setContent(Thread.currentThread().getName() + "的数据");
                System.out.println(Thread.currentThread().getName() + "->" + threadLocalDemo1.getContent());
            });
            thread.setName("线程" + i);
            thread.start();
        }
    }
}
//输出结果(每次对应结果不相同,顺序不同)
线程0->线程2的数据
线程3->线程2的数据
线程2->线程2的数据
线程4->线程2的数据
线程1->线程2的数据
  • 多线程场景下使用synchronized关键字(以时间换空间)
public class ThreadLocalDemo2 {
    private String content;
    public String getContent() {
        return content;
    }
    public void setContent(String content) {
        this.content = content;
    }
    public static void main(String[] args) {
        ThreadLocalDemo2 threadLocalDemo2 = new ThreadLocalDemo2();
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(() -> {
                synchronized (ThreadLocalDemo2.class) {
                    threadLocalDemo2.setContent(Thread.currentThread().getName() + "的数据");
                    System.out.println(Thread.currentThread().getName() + "->" + threadLocalDemo2.getContent());
                }
            });
            thread.setName("线程" + i);
            thread.start();
        }
    }
}
//输出结果(通过synchronized关键字加锁,保证其代码串行执行。每次对应结果相同,顺序不同)
线程0->线程0的数据
线程4->线程4的数据
线程3->线程3的数据
线程2->线程2的数据
线程1->线程1的数据
  • 多线程场景下使用threadLocal(以空间换时间)
public class ThreadLocalDemo3 {
    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
    private String content;
    public String getContent() {
        return threadLocal.get();
    }
    public void setContent(String content) {
        threadLocal.set(content);
    }
    public static void main(String[] args) {
        ThreadLocalDemo3 threadLocalDemo3 = new ThreadLocalDemo3();
        for (int i = 0; i < 3; i++) {
            Thread thread = new Thread(() -> {
                threadLocalDemo3.setContent(Thread.currentThread().getName() + "的数据");
                System.out.println(Thread.currentThread().getName() + "->" + threadLocalDemo3.getContent());
            });
            thread.setName("线程" + i);
            thread.start();
        }
    }
}

//输出结果(通过threadLocal定义本地线程变量,在多线程场景下,每个线程都包含一个threadLocal,实现线程隔离。每次对应结果相同,顺序不同)
线程1->线程1的数据
线程0->线程0的数据
线程2->线程2的数据

ThreadLocal的应用场景

  • Spring实现事务隔离级别源码,Spring通过ThreadLocal来保证单个线程中的数据库操作是同一个数据库连接,使业务层无感知并管理connection对象,通过传播级别来管理多线程时事务之间的切换、挂起、恢复。
//代码操作数据库需要getConnection()获取连接并在执行完响应操作之后需要关闭连接,而在多线程场景下,每个线程都能获取连接,且可能多个线程获取的连接是相同的,此时就会造成两个线程同用一个连接且其中一个线程使用完之后执行connection.close(),而另一个线程无法完成后续操作导致报错。
  • 用户登录信息存储,多线程环境下保存不同用户登录信息,在业务层次复杂的情况下,我们无需通过获取用户信息后一层一层传到底层,可直接通过threadLocal获取存储的用户信息。
//很多场景下我们需要统一存入用户信息,当某一个逻辑需要用户信息时,我们无需通过参数传入的方式直接通过ThreadLocal里的get()拿到用户信息,减少代码的强耦合性。

ThreadLocal内部结构

JDK8之前

在这里插入图片描述

​ 每个ThreadLocal都保存了一个ThreadLocalMap,Map里面保存着有个静态内部类Entry作为节点,形成一个Entry数组,数组中元素Entry的key当前线程,value为保存的值。

JDK8及之后

在这里插入图片描述

​ 每个Thread都保存了一个ThreadLocalMap,ThreadLocalMap(并非传统意义上的Map,不属于java.util.Map,只是跟传统Map实现方式很像)里面保存着有个静态内部类Entry作为节点的数组,数组中元素Entry的keyThreadLocal实例本身,value为保存的值。

在这里插入图片描述

好处:

  • 每个Map存储的Entry数量变少,因为之前的存储数量由Thread的数量决定,现在是由ThreadLocal的数量决定。在实际运用当中,往往ThreadLocal的数量要少于Thread的数量。
  • 当Thread销毁时,ThreadLocalMap也会随之销毁,能减少内存的使用。

ThreadLocal方法解析

方法声明描述
public void set( T value)设置当前线程绑定的局部变量
public T get()获取当前线程绑定的局部变量
public void remove()移除当前线程绑定的局部变量
protected T initialValue()返回当前线程局部变量的初始值(未重写则直接返回null,需通过子类重写设置初始值)
set方法
  • 执行流程
  1. 首先获取当前线程,并根据当前线程获取一个Map

  2. 如果获取的Map不为空,则将参数设置到Map中(当前ThreadLocal的引用作为key)

  3. 如果Map为空,则给该线程创建 Map,并设置初始值

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

 /**
     * 获取当前线程Thread对应维护的ThreadLocalMap 
     * 
     * @param  t the current thread 当前线程
     * @return the map 对应维护的ThreadLocalMap 
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
	/**
     *创建当前线程Thread对应维护的ThreadLocalMap 
     *
     * @param t 当前线程
     * @param firstValue 存放到map中第一个entry的值
     */
	void createMap(Thread t, T firstValue) {
        //这里的this是调用此方法的threadLocal
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
get方法
  • 执行过程
  1. 首先获取当前线程, 根据当前线程获取一个Map

  2. 如果获取的Map不为空,则在Map中以ThreadLocal的引用作为key来在Map中获取对应的Entry e,否则转到步骤4

  3. 如果e不为null,则返回e.value,否则转到步骤4

  4. Map为空或者e为空,则通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的Map

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

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

/**
 * 初始化
 *
 * @return the initial value 初始化后的值
 */
private T setInitialValue() {
    // 调用initialValue获取初始化的值
    // 此方法可以被子类重写, 如果不重写默认返回null
    T value = initialValue();
    // 获取当前线程对象
    Thread t = Thread.currentThread();
    // 获取此线程对象中维护的ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
    // 判断map是否存在
    if (map != null)
        // 存在则调用map.set设置此实体entry
        map.set(this, value);
    else
        // 1)当前线程Thread 不存在ThreadLocalMap对象
        // 2)则调用createMap进行ThreadLocalMap对象的初始化
        // 3)并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
        createMap(t, value);
    // 返回设置的值value
    return value;
}
remove方法
  • 执行过程
  1. 首先获取当前线程,并根据当前线程获取一个Map

  2. 如果获取的Map不为空,则移除当前ThreadLocal对象对应的entry

  • 代码展示
/**
 * 删除当前线程中保存的ThreadLocal对应的实体entry
 */
public void remove() {
    // 获取当前线程对象中维护的ThreadLocalMap对象
    ThreadLocalMap m = getMap(Thread.currentThread());
    // 如果此map存在
    if (m != null)
    // 存在则调用map.remove
    // 以当前ThreadLocal为key删除对应的实体entry
    m.remove(this);
}
initValue方法
  • 执行过程

此方法的作用是 返回该线程局部变量的初始值。

(1) 这个方法是一个延迟调用方法,从上面的代码我们得知,在set方法还未调用而先调用了get方法时才执行,并且仅执行1次。

(2)这个方法缺省实现直接返回一个null。

(3)如果想要一个除null之外的初始值,可以重写此方法。(备注: 该方法是一个protected的方法,显然是为了让子类覆盖而设计的)

  • 代码展示
/**
  * 返回当前线程对应的ThreadLocal的初始值
  * 此方法的第一次调用发生在,当线程通过get方法访问此线程的ThreadLocal值时
  * 除非线程先调用了set方法,在这种情况下,initialValue 才不会被这个线程调用。
  * 通常情况下,每个线程最多调用一次这个方法。
  *
  * <p>这个方法仅仅简单的返回null {@code null};
  * 如果程序员想ThreadLocal线程局部变量有一个除null以外的初始值,
  * 必须通过子类继承{@code ThreadLocal} 的方式去重写此方法
  * 通常, 可以通过匿名内部类的方式实现
  *
  * @return 当前ThreadLocal的初始值
  */
protected T initialValue() {
    return null;
}

代码解析

​ ThreadLocalMap属于ThreadLocal的静态内部类,Entry是ThreadLocalMap的静态内部类,其中ThreadLocalMap保存着Entry的数组,Entry中限定了ThreadLocal作为key以及设置传入参数值为value。Entry继承WeakReference,也就是key(ThreadLocal)是弱引用(弱引用将在下一次GC时回收),其目的是将ThreadLocal对象的生命周期和线程生命周期解绑。

public class ThreadLocal<T> {
    private final int threadLocalHashCode = nextHashCode();
    private static AtomicInteger nextHashCode = new AtomicInteger();
    private static final int HASH_INCREMENT = 1640531527;
    ...
    static class ThreadLocalMap {
        private static final int INITIAL_CAPACITY = 16;
        private ThreadLocal.ThreadLocalMap.Entry[] table;
        private int size = 0;
        private int threshold;

        private ThreadLocalMap(ThreadLocal.ThreadLocalMap parentMap) {
            ThreadLocal.ThreadLocalMap.Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            this.setThreshold(len);
            this.table = new ThreadLocal.ThreadLocalMap.Entry[len];
            ThreadLocal.ThreadLocalMap.Entry[] var4 = parentTable;
            int var5 = parentTable.length;
            ...
        }
        ...
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                this.value = v;
            }
        }
        ...
    }
}

弱引用和内存泄漏

弱引用和强引用
概念

​ 弱引用:具有弱引用的对象拥有更短暂的生命周期,在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

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

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

​ 假设在业务代码中使用完ThreadLocal ,threadLocal Ref被回收了。但是因为threadLocalMap的Entry强引用了threadLocal,造成threadLocal无法被回收。在没有手动删除这个Entry以及CurrentThread依然运行的前提下,始终有强引用链 threadRef->currentThread->threadLocalMap->entry,Entry就不会被回收(Entry中包括了ThreadLocal实例和value),导致Entry内存泄漏。也就是说,ThreadLocalMap中的key使用了强引用, 是无法完全避免内存泄漏的。

在这里插入图片描述

如果key使用弱引用

​ 同样假设在业务代码中使用完ThreadLocal ,threadLocal Ref被回收了。由于ThreadLocalMap只持有ThreadLocal的弱引用,没有任何强引用指向threadlocal实例, 所以threadlocal就可以顺利被gc回收,此时Entry中的key=null。但是在没有手动删除这个Entry以及CurrentThread依然运行的前提下,也存在有强引用链 threadRef->currentThread->threadLocalMap->entry -> value ,value不会被回收, 而这块value永远不会被访问到了,导致value内存泄漏。也就是说,ThreadLocalMap中的key使用了弱引用, 也有可能内存泄漏。

在这里插入图片描述

问题

​ 目前针对于ThreadLocal中ThreadLocalMap下的Entry就是继承了弱引用,发生GC时ThreadLocal实例会被回收,也就是key将为null,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。就比如线程池里面的线程,线程都是复用的,那么之前的线程实例处理完之后,出于复用的目的线程依然存活,所以,ThreadLocal设定的value值被持有,导致内存泄露。

在这里插入图片描述

如何解决

​ 内存泄漏的发生跟ThreadLocalMap中的key是否使用弱引用是没有关系的,无论是强引用还是弱引用都存在内存泄漏,问题出现的前提是没有手动删除这个Entry或者当前线程依旧在运行。解决办法如下:

  • 当使用完ThreadLocal后手动进行remove操作,将当前ThreadLocal的Entry直接删除。
  • 存在内存泄露是因为当前key为null,无法再引用而使其生命周期与当前线程生命周期相同,只要当前线程运行结束,内存泄漏也即将不复存在

总结:第二种方法相对于第一种方法更加不容易控制,尤其是使用线程池的时候,线程是可复用的,也就表示即使使用完后线程依旧存活,内存泄漏也就一直存在。

疑问:

那为什么强引用和弱引用都很难避免内存泄漏,但设计却选用了弱引用?

事实上,在ThreadLocalMap中的set/getEntry方法中,会对key为null(也就是ThreadLocal为null)进行判断,如果为null的话,那么是会对value置为null的。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

​ 这就意味着使用完ThreadLocal,CurrentThread依然运行的前提下,就算忘记调用remove方法,弱引用比强引用可以多一层保障:弱引用的ThreadLocal会被回收,对应的value在下一次ThreadLocalMap调用set、get、remove中的任一方法的时候会被清除,从而避免内存泄漏。

hash冲突

什么是hash冲突

​ hash即为散列,hash冲突就是key通过hash算法最终得到的值作为地址存放到当前的键值对(key-value)上,但发现此地址已存有其他的对象,从而引发的冲突。在hashMap中,我们都知道是由数组加链表组成(JDK8后数组+链表+红黑树),其中链表就是为了解决hash冲突的。

问题

​ 由上所知ThreadLocal中维护着一个ThreadLocalMap,ThreadLocalMap不同于HashMap,但实现方式非常相似。但ThreadLocalMap中只包含了Entry数组,为什么只用数组却根本没有链表的形式,这又怎么解决hash冲突这个问题呢?

为什么使用数组

​ 我们开发过程中单个线程是可能会包含多个ThreadLocal的,通过多个ThreadLocal来保存多个对象,ThreadLocalMap保存的正是当前多个ThreadLocal实例的threadLocalHashCode作为key的Entry节点。

怎么解决Hash冲突

源码展示

在这里插入图片描述

所有ThreadLocal进行set值的时候都会通过key.threadLocalHashCode & len - 1算出在Entry数组中所存放的位置,然后根据如下情况赋值:

  • 如果当前位置为空,就初始化一个Entry对象并存入当前set的值。
  • 如果当前位置不为空,并且key和当前Entry的key相同的话,则直接更新value值。
  • 如果当前位置不为空,并且key和当前Entry的key不相同的话,则找下一个空位置,以此类推,直到找到空位置为止。

总结:以上所知,虽然ThreadLocal不存在链表形式,该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。ThreadLocalMap使用线性探测法来解决哈希冲突的。

举个例子,假设当前table长度为16,也就是说如果计算出来key的hash值为14,如果table[14]上已经有值,并且其key与当前key不一致,那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,这个时候如果还是冲突会回到0,取table[0],以此类推,直到可以插入。

eadLocal进行set值的时候都会通过key.threadLocalHashCode & len - 1算出在Entry数组中所存放的位置,然后根据如下情况赋值:

  • 如果当前位置为空,就初始化一个Entry对象并存入当前set的值。
  • 如果当前位置不为空,并且key和当前Entry的key相同的话,则直接更新value值。
  • 如果当前位置不为空,并且key和当前Entry的key不相同的话,则找下一个空位置,以此类推,直到找到空位置为止。

总结:以上所知,虽然ThreadLocal不存在链表形式,该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。ThreadLocalMap使用线性探测法来解决哈希冲突的。

举个例子,假设当前table长度为16,也就是说如果计算出来key的hash值为14,如果table[14]上已经有值,并且其key与当前key不一致,那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,这个时候如果还是冲突会回到0,取table[0],以此类推,直到可以插入。

按照上面的描述,可以把Entry[] table看成一个环形数组。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值