ThreadLocal,我悟了~

x33g5p2x  于2021-10-20 转载在 其他  
字(4.8k)|赞(0)|评价(0)|浏览(413)

一、ThreadLocal简介

ThreadLocal,直译就是本地线程,是多线程的知识点。线程安全的一个解决方案。

在多线程时,共享一个变量容易出现并发问题,特别是多个线为变量写入值的时候,如果要保证线程的安全性,就需要采取额外的线程安全措施(比如加锁,但是性能很低)。而ThreadLocal就可以更好的解决多线程之间变量的安全问题。

ThreadLocal的作用主要是做数据隔离,填充的数据只属于当前线程,每个线程中存储的变量对别的线程而言是相对隔离的,防止自己线程中的变量被其他线程修改

ThreadLocal由JDK提供,他提供线程本地变量,如果创建一个ThreadLocal变量(ThreadLocal是一个Java类),那么访问这个变量的每一个线程都会有这个ThreadLocal类型变量的副本。之后,当一个线程操作这个变量的时候,操作的是自己本地内存中的ThreadLocal变量,并不会影响到其他线程中的ThradLocal对象,很好的规避了线程安全问题。

如下图:

二、ThreadLocal核心

1.方法

ThreadLocal的方法很简单,就是三个方法就🆗了(可打开ThreadLocal类源码查看详细)

static final ThreadLocal<T> THREAD_LOCAL = new ThreadLocal<T>();
THREAD_LOCAL.set('参数');		// 只能存在一个参数
THREAD_LOCAL.get();			// 获取 无需传递任何参数
THREAD_LOCAL.remove();		// 删除key-value及时释放内存

那么原理呢?请看下面

2.原理

可以从Thread、ThreadLocal二者的源码开始。

ThreadLocal类结构图:

Thread类中有threadLocals和inheritableThreadLocals两个变量,两者都是ThreadLocal内部类ThreadLocalMap类型的变量。可以通过查看ThreadLocalMap源码发现它的结构类似于HashMap(但是它并未实现Map接口)。
结合源码食用更爽~

初始情况下,threadLocals和inheritableThreadLocals都为null,只有当线程第一次调用ThreadLocal的set或者get方法的时候才会创建他们

// Thread类内部
ThreadLocal.ThreadLocalMap threadLocals = null;
// some comments ...
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

并且,每个线程的本地变量不是存放在ThreadLocal实例中,而是放在调用线程Thread的threadLocals变量里面。即 ThreadLocal类型的本地变量是存放在具体的线程空间上,其本身相当于一个装载本地变量的工具壳,通过set方法将value添加到调用当前线程的threadLocals中,当调用ThreadLocal的get方法,就能够从它的threadLocals中取出变量

如果调用线程一直不终止,那么这个本地变量将会一直存放在他的threadLocals中,所以不使用本地变量的时候需要调用remove方法将threadLocals中删除不用的本地变量。

注意,数据存储在Thread类的threadLocals中(k-v)

其中的key为当前定义的ThreadLocal变量的this引用,value为我们使用set方法设置的值。查看ThreadLocal的set方法:

// ThreadLocal的set方法
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        map.set(this, value);
    } else {
        createMap(t, value); // 点击进入查看createMap()源码深入理解
    }
}

每个线程的本地TheadLocalMap类型的变量存放在自己的本地内存变量threadLocals中,如果当前线程一直存在,那么这些本地变量就会一直存在(所以可能会导致内存溢出),因此使用完毕需要将其remove掉。

3.ThradLocalMap底层

每个线程都持有一个ThradLocalMap类。ThreadLocalMap作为ThreadLocal的静态内部类,它为每个Thread都维护了一个数组table,ThreadLocal确定了一个数组下标,此下标就是value参数在数组中存储的位置。

ThreadLocalMap的底层是以数组形式存储参数value的(即存储在 Entry[] 数组中)。

虽然命名上,存在一个Map,但是通过源码发现,ThreadLocalMap并未实现Map的任何接口。它的Entry继承了WeakReference(弱引用)

static class ThreadLocalMap {
    
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    ... ...
}
  • set()时:ThreadLocalMap通过当前的ThradLocal对象获取到它的threadLocalHashCode,之后就行数组的坐标计算,如果得到当前数组位置不为空,就会判断下一个位置,知道找到空位置。
  • get()时:计算得到当前ThreadLocal对象的threadLocalHashCode,在得到数组坐标,比对ThreadLocal对象使用与存储的对象相同,本相同就判断下一个位置,匹配就取出数据。(如果hash冲突严重,效率依旧低下)。
  • remove()时:与get类似,后面的取出变为删除。

4.参数存储位置

每一个线程具有自己的栈内存,可以理解为线程的私有内存,而堆内存中的对象所有线程可见、可访问。

但是,ThradLocal的实例和存储的参数并不是存储在栈内存上,相反,它们存储在堆内存中。原因如下:

ThreadLocal实例是被其创建的类所持有(更顶端应该是被线程所持有),它们都位于堆上,通过一些技巧将可见性修改成为仅指定线程可见。

三、ThreadLocal内存泄漏

先复习一下强引用和弱引用:

  • 强引用:具有强引用的对象,不会被垃圾回收器回收。即使抛出OutOfMemoryError错误,也不回收。
  • 弱引用:JVM进行垃圾回收时,会回收被弱引用关联的对象。在java中用java.lang.ref.WeakReference类来表示弱引用。
    取消某个对象的强引用,可以显式的将引用赋值为null,JVM即可将之前被强引用的对象回收。

每一个Thread都会维护一个ThreadLocalMap(Thread源码可知),key为使用弱引用的ThreadLocal实例,value为线程存储的变量值。

再来看一张很熟悉的图片:

其中实线代表强引用,虚线代表弱引用。也就是ThreadLocalMap里面的key为弱引用(如果不是弱引用,会内存泄漏),就会出现如下问题:

如果当前线程还存在,当前线程的ThreadLocalMap里面的key,弱引用于ThreadLocal变量,在gc的时候就key被回收,但是对应的value还是存在。这就可能造成内存泄漏(因为这个时候ThreadLocalMap会存在key为null,value不为null的entry项,value存储在当前线程的空间上,会一直存在系统的本地内存中)。

回到TheadLocal的set()方法:一个ThradLocal对象它只能存储唯一的映射,key是当前ThreadLocal对象,value是存储的对象(注意,他不会自己被回收)。当调用set() 方法时,会重新构建一个key-value的entry映射,之前存储的value并不会被gc回收,这就导致了之前存储的value还存在,新存储的value也存在。此时却只能获取到新构建的映射对象,之前存储的对象获取不到,当这种value过多的时候,就出现了内存泄漏的问题。

这就需要在使用完毕时,及时调用remove方法把key和value都删除,避免内存泄漏

四、常见使用场景

1.spring隔离级别

Spring事物隔离级别源码有使用到(Spring的事物主要是ThreadLocal和AOP去实现的。)。

Spring采用ThreadLocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接(即每个线程的连接数据库的connection对象存储在Thread中的ThreadLocalMap中),采用这种方式使得业务层在使用事物时不需要感知和管理connection对象(直接获取就能使用)。在通过事物的传播级别,巧妙的管理多个事物配置之间的切换、挂起和恢复。

2.传递对象参数

在一个线程中横跨若干方法调用一个对象,也就是上下文(Context)(它是一种状态,经常就是是用户身份、任务信息等),就会存在如何传参的问题。

把参数传递一层方法一层方法的传递显然不合适,而使用ThreadLocal就可以很好的解决这个问题:

线程初始时可初始化一个泛型的ThreadLocal对象,在参数初始位置使用ThreadLocal调用 set('参数') 方法后,可在同一个线程的不同地方使用ThreadLocal类的get()方法获取(remove之前),获取完数据后务必及时remove掉,避免内存泄漏。

例如编写一个工具(保存用户信息到当前线程空间):

public class UserThreadLocal {

    private UserThreadLocal(){}

    //线程变量隔离
    private static final ThreadLocal<SysUser> THREAD_LOCAL = new ThreadLocal<>();

    /** * 存储sysUser * @param sysUser */
    public static void putUser(SysUser sysUser){
        THREAD_LOCAL.set(sysUser);
    }

    /** * 获取sysUser * @return */
    public static SysUser getUser(){
        return THREAD_LOCAL.get();
    }

    /** * 删除sysUser */
    public static void removeUser(){
        THREAD_LOCAL.remove();
    }
}

在适当的位置调用对应的方法即可实现同线程之间传递用户参数。

3.更多的使用场景

  • cookie、session的数据隔离通过ThreadLocal实现。
  • 需要备份一份对象参数的场景。
  • … …

五、ThreadLocal与Synchronized

ThreadLocal和Synchronized都是可以解决多线程中相同变量的访问冲突问题,不同的点是:

  • Sysnchronized是通过线程等待,牺牲时间来解决多线程安全问题。
  • ThreadLocal是通过为每个线程单独存储一个ThreadLocalMap,牺牲空间来解决多线程安全问题。

相比于Synchronized,ThreadLocal具有线程隔离的效果,实现在单独的线程内才能获取到对于的值,其他线程则访问不到。很明显,在两者都能使用的情况下,优先选择ThreadLocal(Synchronized也有它必须做的事)。

相关文章