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!
相关推荐
爱上语文几秒前
Springboot的三层架构
java·开发语言·spring boot·后端·spring
serve the people4 分钟前
springboot 单独新建一个文件实时写数据,当文件大于100M时按照日期时间做文件名进行归档
java·spring boot·后端
qmx_071 小时前
HTB-Jerry(tomcat war文件、msfvenom)
java·web安全·网络安全·tomcat
为风而战1 小时前
IIS+Ngnix+Tomcat 部署网站 用IIS实现反向代理
java·tomcat
技术无疆3 小时前
快速开发与维护:探索 AndroidAnnotations
android·java·android studio·android-studio·androidx·代码注入
罗政6 小时前
[附源码]超简洁个人博客网站搭建+SpringBoot+Vue前后端分离
vue.js·spring boot·后端
架构文摘JGWZ6 小时前
Java 23 的12 个新特性!!
java·开发语言·学习
拾光师7 小时前
spring获取当前request
java·后端·spring
aPurpleBerry7 小时前
neo4j安装启动教程+对应的jdk配置
java·neo4j
我是苏苏7 小时前
Web开发:ABP框架2——入门级别的增删改查Demo
java·开发语言