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详解(介绍使用原理应用场景)