ThreadLocal使用以及原理

什么是Threadlocal?

ThreadLocal意为线程本地变量,用于解决多线程并发时访问共享变量的问题。

所谓的共享变量指的是在堆中的实例、静态属性和数组; 对于共享数据的访问受Java的内存模型JMM的控制

下图为java内存模型图

主内存中(堆)主要就是存放的各种共享变量,并且每个线程都会有自己单独的本地内存也叫做工作者内存,当一个线程要使用主存的共享变量的时候,线程会复制一份到自己的本地内存中,当线程修改了共享变量后,就会通过JMM的管理控制写回到主内存中。

但是在多线程下的时候,多个线程同时对共享变量进行修改,而且线程之间是不可见的,所以就会出现线程安全问题,即是数据不一致的问题。一般的解决方案是给访问的共享变量的代码加锁synchronized或者Lock,但是会对性能消耗比较大,当然也可以使用volatile关键字,CAS+原子类也是可以的,但是今天在这里主要讲解在JDK1.2中引入了ThreadLocal类,来修饰共享变量,使每个线程都单独拥有一份共享变量,这样就可以做到线程之间对于共享变量的隔离问题

锁和ThreadLocal使用场景还是有区别的

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

ThreadLocal的使用

一般都会将ThreadLocal声明成一个静态字段,同时初始化如下:

swift 复制代码
public static ThreadLocal<Object> threadLocal = new ThreadLocal<>();
​

其中Object就是原本堆中共享变量的数据。

例如,有个User对象需要在不同线程之间进行隔离访问,可以定义ThreadLocal如下:

ini 复制代码
    static ThreadLocal<User> threadLocal = new ThreadLocal<>();

常用的方法

  • set(T value):设置线程本地变量的内容。
  • get():获取线程本地变量的内容。
  • remove():移除线程本地变量。注意在线程池的线程复用场景中在线程执行完毕时一定要调用remove,避免在线程被重新放入线程池中时被本地变量的旧状态仍然被保存。
csharp 复制代码
public class threadlocalTest {
    //创建线程本地变量
    static ThreadLocal<User> threadLocal = new ThreadLocal<>();
    //添加
    public void add(User user) {
        threadLocal.set(user);
    }
   //获取和删除
    public void getvalue() {
        User user = threadLocal.get();
        // 使用
        //进行一些操作
        // 使用完清除
        threadLocal.remove();
    }
}

ThreadLocal的原理

hreadLocal可以理解为线程本地变量,他会在每个线程都创建一个副本,那么在线程之间访问内部副本变量 就行了,做到了线程之间互相隔离, 相比于synchronized的做法是用空间来换时间。

ThreadLocal有一个静态内部类ThreadLocalMapThreadLocalMap又包含了一个Entry数组,Entry本身是一个弱引用,他的key是指向ThreadLocal的弱引用,Entry具备了保存key value键值对的能力。

弱引用的目的

是为了防止内存泄露 ,如果是强引用那么ThreadLocal对象除非线程结束否则始终无法被回收,弱引用则会在下一次GC的时候被回收。但是这样还是会存在内存泄露的问题,假如key和ThreadLocal对象被回收之后,entry中就存在key为null,但是value有值的entry对象,但是永远没办法被访问到,同样除非线程结束运行。

但是只要ThreadLocal使用恰当,在使用完之后调用remove方法删除Entry对象,实际上是不会出现这个问题的

ThreadLocal::set方法的原理

scss 复制代码
public void set(T value) {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取当前线程的threadLocals字段
    ThreadLocalMap map = getMap(t);
    // 判断线程的threadLocals是否初始化了
    if (map != null) {
        map.set(this, value);
    } else {
        // 没有则创建一个ThreadLocalMap对象进行初始化
        createMap(t, value);
    }
}

createMap方法的源码如下:

javascript 复制代码
void createMap(Thread t, T firstValue) {
    //为当前线程初始化threadLocalszi
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

可以看到他首先调用了Thread.currentThread()获取当先线程然后调用了getMap()获取到线程对象,后面就是为线程对象set指定的值可以发现set()源码非常简单,主要是ThreadLocalMap需要我们注意,直接看看getMap!

javascript 复制代码
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
java 复制代码
class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

JDK8之后,每个Thread维护一个ThreadLocalMap对象这个Map的key是ThreadLocal实例本身,value是存储的值要隔离的变量 ,是泛型,可以看到每个线程Thread都会维护自己的threadLocal变量,所以每次使用ThreadLoacl.get() 方法的时候都是从自己的线程里面拿到自己的ThreadLocals变量,这样是拿不到其他线程的变量,从而实现了数据隔离

源码可以看出threadLocals是ThreadLocal里面的ThreadLocalMap,那么ThreadLocalMap底层结构长什么样呢?

ThreadLocalMap

ThreadLocal中定义的Map对象,保存了该线程中的所有本地变量。ThreadLocalMap中的Entry的定义如下

是继承了WeakReference(弱引用)

scala 复制代码
static class ThreadLocalMap {
​
    /**
     * The entries in this hash map extend WeakReference, using
     * its main ref field as the key (which is always a
     * ThreadLocal object).  Note that null keys (i.e. entry.get()
     * == null) mean that the key is no longer referenced, so the
     * entry can be expunged from table.  Such entries are referred to
     * as "stale entries" in the code that follows.
     */
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;
// key为一个ThreadLocal对象,v就是我们要在线程之间隔离的对象
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    
    
        /**
         * The initial capacity -- MUST be a power of two.
         */
        private static final int INITIAL_CAPACITY = 16;
​
        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
    //可以看出是一个数组
        private Entry[] table;
​

从上面源码可以看到是使用的数组来进行保存变量的

那么用了数组怎么解决Hash冲突问题呢?

在ThreadLocalMap的set方法中,会根据ThreadLocal对象的hash值,定位到指定位置,然后判断该位置key和Entry对象是否和get的key一样,一样就直接赋值在索引上,如果为null这则初始化一个Entry对象放在索引上

ThreadLocal::get方法的原理

ini 复制代码
public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

如果调用返回值不为null,则返回它的value值,如果为空则执行设置初始值setInitiaValue()方法,这里主要看ThreadLocalMap的getEntry()方法

vbnet 复制代码
private Entry getEntry(ThreadLocal<?> key) {
        int i = key.threadLocalHashCode & (table.length - 1);
        Entry e = table[i];
        if (e != null && e.get() == key)
            return e;
        else
            return getEntryAfterMiss(key, i, e);
    }

getEntry()的实现逻辑就是拿到key的hash值,然后判断此处是否等于key,如果是则返回这个Entry对象,如果不是则执行getEntryAfterMiss()方法。

getEntryAfterMiss():法就是一直往下查找,直到找到对应的位置。

应用场景

1、在重入方法中替代参数的显式传递

假如在我们的业务方法中需要调用其他方法,同时其他方法都需要用到同一个对象时,可以使用ThreadLocal替代参数的传递或者static静态全局变量。这是因为使用参数传递造成代码的耦合度高,使用静态全局变量在多线程环境下不安全。当该对象用ThreadLocal包装过后,就可以保证在该线程中独此一份,同时和其他线程隔离。

例如在Spring的@Transaction事务声明的注解中就使用ThreadLocal保存了当前的Connection对象,避免在本次调用的不同方法中使用不同的Connection对象。

2、全局存储用户信息

可以尝试使用ThreadLocal替代Session的使用,当用户要访问需要授权的接口的时候,可以先在拦截器中将用户的Token存入ThreadLocal中;之后在本次访问中任何需要用户信息的都可以直接向ThreadLocal中拿取数据

3、解决线程安全问题

依赖于ThreadLocal本身的特性,对于需要进行线程隔离的变量可以使用ThreadLocal进行封装。

总结

  • ThreadLocal更像是对其他类型变量的一层包装,通过ThreadLocal的包装使得该变量可以在线程之间隔离当前线程全局共享
  • ThreadLocalMap中Entry的Key不管是否使用弱引用 都有内存泄露的可能。引起内存泄露主要在于ThreadLocal对象和Entry中的Value对象,因此要确保每次使用完之后都remove掉Entry!
相关推荐
考虑考虑41 分钟前
JDK9中的dropWhile
java·后端·java ee
想躺平的咸鱼干1 小时前
Volatile解决指令重排和单例模式
java·开发语言·单例模式·线程·并发编程
hqxstudying1 小时前
java依赖注入方法
java·spring·log4j·ioc·依赖
·云扬·1 小时前
【Java源码阅读系列37】深度解读Java BufferedReader 源码
java·开发语言
martinzh2 小时前
Spring AI 项目介绍
后端
Bug退退退1232 小时前
RabbitMQ 高级特性之重试机制
java·分布式·spring·rabbitmq
小皮侠2 小时前
nginx的使用
java·运维·服务器·前端·git·nginx·github
前端付豪3 小时前
20、用 Python + API 打造终端天气预报工具(支持城市查询、天气图标、美化输出🧊
后端·python
爱学习的小学渣3 小时前
关系型数据库
后端
武子康3 小时前
大数据-33 HBase 整体架构 HMaster HRegion
大数据·后端·hbase