ThreadLocal 应用及其原理详解

Posted 流楚丶格念

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ThreadLocal 应用及其原理详解相关的知识,希望对你有一定的参考价值。

文章目录

ThreadLocal

概念

ThreadLocal顾名思义是线程私有的局部变量存储容器,可以理解成每个线程都有自己专属的存储容器,它用来存储线程私有变量,其实它只是一个外壳,内部真正存取是一个Map。

每个线程可以通过set() 和 get() 存取变量,多线程间无法访问各自的局部变量,相当于在每个线程间建立了一个隔板。只要线程处于活动状态,它所对应的ThreadLocal实例就是可访问的,线程被终止后,它的所有实例将被垃圾收集。

总之我们要记住总的思想:ThreadLocal存储的变量属于当前线程

应用场景

JDBC 连接多Connection

ThreadLocal经典的使用场景是为每个线程分配一个 JDBC 连接 Connection,这样就可以保证每个线程的都在各自的 Connection 上进行数据库的操作,不会出现 A 线程关了 B线程正在使用的Connection。

管理会话存储

另外ThreadLocal还经常用于管理Session会话,将Session保存在ThreadLocal中,使线程处理多次处理会话时始终是同一个Session。

多线程访问共享变量(在重入方法中替代参数的显式传递)

​ 假如在我们的业务方法中需要调用其他方法,同时其他方法都需要用到同一个对象时,可以使用ThreadLocal替代参数的传递或者使用static静态全局变量。

这是因为使用参数传递造成代码的耦合度高,使用静态全局变量在多线程环境下不安全。当该对象用ThreadLocal包装过后,就可以保证在该线程中独此一份,同时和其他线程隔离。

下面是详细说明:

这里​ ThreadLocal 存储的是本地共享变量,用于解决多线程并发时访问共享变量的问题。

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

每个线程都会有属于自己的本地内存,在堆(也就是上图的主内存)中的变量在被线程使用的时候会被复制一个副本线程的本地内存中,当线程修改了共享变量之后就会通过JMM管理控制写会到主内存中。

​ 很明显,在多线程的场景下,当有多个线程对共享变量进行修改的时候,就会出现线程安全问题,即数据不一致问题。

常用的解决方法是对访问共享变量的代码加(synchronized或者Lock)。但是这种方式对性能的耗费比较大。在JDK1.2中引入了ThreadLocal类,来修饰共享变量,使每个线程都单独拥有一份共享变量,这样就可以做到线程之间对于共享变量的隔离问题。

所以说锁和ThreadLocal使用场景还是有区别的,具体区别如下:

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

实现原理

Thread类中有个变量threadLocals,它的类型为ThreadLocal中的一个内部类ThreadLocalMap,这个类没有实现map接口,就是一个普通的Java类,但是实现的类似map的功能。

每个线程都有自己的一个map,map是一个数组的数据结构存储数据,每个元素是一个Entry,entry的key是ThreadLocal的引用,也就是当前变量的副本,value就是set的值。

threadLocals.ThreadLocalMap 的代码如下所示:

public class Thread implements Runnable 
	/* 与此线程有关的 ThreadLocal 值。此映射由 ThreadLocal 类维护*/
	ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocalMap是ThreadLocal的内部类,每个数据用Entry保存,其中的Entry继承与WeakReference,用一个键值对存储,键为ThreadLocal的引用。为什么是WeakReference呢?如果是强引用,即使把ThreadLocal设置为null,GC也不会回收,因为ThreadLocalMap对它有强引用。

ThreadLocalMap 代码如下所示:

/*
此哈希映射中的条目扩展了 WeakReference,
使用其主 ref 字段作为键(始终是 ThreadLocal 对象)。

请注意,空键(即 entry.get() == null)意味着不再引用该键,
因此可以从表中删除该条目。在下面的代码中,此类条目称为“陈旧条目”。
*/
static class Entry extends WeakReference<ThreadLocal<?>> 
    /** The value associated with this ThreadLocal. */
    Object value;
    Entry(ThreadLocal<?> k, Object v) 
        super(k);
        value = v;
    

ThreadLocal中的set方法的实现逻辑,先获取当前线程,取出当前线程的ThreadLocalMap,如果不存在就会创建一个ThreadLocalMap,如果存在就会把当前的threadlocal的引用作为键,传入的参数作为值存入map中。

代码如下所示:

/*
将此线程局部变量的当前线程副本设置为指定值。
大多数子类不需要重写此方法,仅依靠 #initialValue方法来设置线程局部变量的值。

@param value 要存储在此线程本地的当前线程副本中的值。
*/
public void set(T value) 
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);

ThreadLocal中get方法的实现逻辑,获取当前线程,取出当前线程的ThreadLocalMap,用当前的threadlocal作为key在ThreadLocalMap查找,如果存在不为空的Entry,就返回Entry中的value,否则就会执行初始化并返回默认的值。代码如下所示:

/*
返回此线程局部变量的当前线程副本中的值。
如果变量没有当前线程的值,则首先将其初始化为调用 #initialValue方法返回的值。

@return 此线程本地的当前线程的值
*/
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();

ThreadLocal中remove方法的实现逻辑,还是先获取当前线程的ThreadLocalMap变量,如果存在就调用ThreadLocalMap的remove方法。ThreadLocalMap的存储就是数组的实现,因此需要确定元素的位置,找到Entry,把entry的键值对都设为null,最后也Entry也设置为null。其实这其中会有哈希冲突,具体见下文:ThreadLocal解决哈希冲突。

代码如下所示:

public void remove() 
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);

ThreadLocal中的hash code非常简单,就是调用AtomicInteger的getAndAdd方法,参数是个固定值0x61c88647 。

上面说过ThreadLocalMap的结构非常简单只用一个数组存储,并没有链表结构,当出现Hash冲突时采用线性查找的方式,所谓线性查找,就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置

如果产生多次hash冲突,处理起来就没有HashMap的效率高,为了避免哈希冲突,使用尽量少的threadlocal变量。

下面是对ThreadLocal使用的开放寻址法的讲解,感兴趣的可以了解一下:

哈希冲突问题

ThreadLocal解决哈希冲突采用开放寻址法的思想

开放寻址法:直接从冲突的数组位置往下寻找一个空的数组下标进行数据存储

开放定址法也称线性探测法,就是从发生冲突的那个位置开始,按照一定次序从Hash表找到一个空闲位置然后把发生冲突的元素存入到这个位置,而在java中,ThreadLocal就用到了线性探测法来解决Hash冲突

如图,在Hash表索引1的位置存了key=name,再向它添加key=hobby的时候,假设计算得到的索引也是1,那么这个时候发生哈希冲突,而开放开放定址法就是按照顺序向后找到一个空闲的位置,来存储这个冲突的key

代码案例

线程私有变量与普通变量

代码如下:

package com.yyl.threadTest;

public class ThreadLocalTest 
    //定义普通的变量
    private static int num1 = 0;
    //定义线程局部变量
    private static ThreadLocal<Integer> local = new ThreadLocal<Integer>() 
        //重写initialValue方法,如果不重写初始值为null
        protected Integer initialValue() 
            //返回初始值
            return 0;
        
    ;

    public static void main(String[] args) 
        //创建十个线程
        for (int i = 0; i < 10; i++) 
            new Thread(new Runnable() 
                @Override
                public void run() 
                    num1++;//分别修改普通变量
                    System.out.println("普通变量=" + num1);
                    //修改线程局部变量的值
                    System.out.println("线程局部变量=" + local.get());
                    local.set(local.get() + 1);
                    System.out.println("线程局部变量加1后=" + local.get());

                
            ).start();
        
    


我们可以看到每个线程初始的私有ThreadLocal变量都是初始值0,每个线程互不影响

springboot 使用ThreadLocal

可以参考脚本之家:https://www.jb51.net/article/231213.htm

以上是关于ThreadLocal 应用及其原理详解的主要内容,如果未能解决你的问题,请参考以下文章

深入浅出多线程编程实战ThreadLocal详解(介绍使用原理应用场景)

深入浅出多线程编程实战ThreadLocal详解(介绍使用原理应用场景)

ThreadLocal的原理及用法

ThreadLocal用法详解和原理

详解ThreadLocal原理及内存泄漏

注解Annotation原理详解及其应用示例