这里写目录标题
一 是什么
作者的描述:
This class provides thread-local variables. These variables differ
from their normal counterparts in that each thread that accesses one
(via its {@code get} or {@code set} method) has its own, independently
initialized copy of the variable. {@code ThreadLocal} instances are
typically private static fields in classes that wish to associate
state with a thread (e.g.,a user ID or Transaction ID).
我笨拙的翻译:
这个类提供线程局部变量。
线程中这个变量与平常类的变量不同,不同在于每一个线程在访问它自己的ThreadLocal实例的时候(通过其get或set方法),都有自己独立的、已经初始化的局部变量副本。
ThreadLocal实例通常是类中的私有静态属性,目的是希望将这个属性状态与线程关联起来(例如,用户ID或事务ID)。
作者在类上面写的demo:
For example, the class below generates unique identifiers local to
each thread.A thread’s id is assigned the first time it invokes {@code
ThreadId.get()} and remains unchanged on subsequent calls.
我笨拙的翻译:
下面的类为每一个线程生成唯一标识 线程的id在第一次调用时被分配,并且在随后的调用时保持不变。
public class ThreadId{
private static final AtomicInteger nextId = new AtomicInteger(0);
// Thread local variable containing each thread's ID
// 线程的局部变量包含每个线程的id
private static final ThreadLocal <Integer> threadId = new ThreadLocal<Integer> () {
@Override
protected Integer initialValue() {
return nextId.getAndIncrement();
}
};
// Returns the current thread's unique ID, assigning it if necessary
// 需要的时候返回当前线程的唯一ID
public static int get() {
return threadId.get();
}
}
作者继续描述:
Each thread holds an implicit reference to its copy of a thread-local
variable as long as the thread is alive and the {@code ThreadLocal}
instance is accessible; after a thread goes away, all of its copies of
thread-local instances are subject to garbage collection (unless
otherreferences to these copies exist).
我笨拙的翻译:
只要线程是活的,同时ThreadLocal实例是可以访问的,每个线程都包含一个指向了ThreadLocal副本(copy of a thread-local variable)的隐式引用。在一条线程消失后。线程所有的ThreadLocal实例副本都需要进行垃圾回收。
(除非对这些副本有其他引用)
看英文的时候懂那个意思,可能翻译的时候不太流畅。
大白话总结一下作者的意思:
1 每个线程都会将ThreadLocal实例赋值给线程内的一个变量,也就是每个线程维护自己的ThreadLocal实例。
2 线程内部的ThreadLocal的状态跟线程本身是有关联的,具体怎么关联,要看你的业务了。
3 线程可以访问线程内部的ThreadLocal,线程结束之后,线程内部所有的的ThreadLocal(一个线程可能有多个ThreadLocal)要进行垃圾回收,除非线程外有其他引用指向了这个线程内部的ThreadLocal。
我总结的时候多次用到了线程内和线程内部,这是因为ThreadLocal的作用就是用来做线程间数据隔离的,就是因为线程将ThreadLocal拷贝到自己本身内部,每个线程都访问自己内部的ThreadLocal,所以就实现了数据隔离。
看了其他大神的说法:
保存某个线程某一段旅程内的部分记忆。
线程内的全局变量。
线程级别的全局变量。
嗯。。。自己体会。。。
二 实际使用和使用场景
2.1 Helloworld:
//每个线程可以有多个ThreadLocal
//每个线程的ThreadLocal自己独享,别人无法get和set
public class T_threadlocal {
private static ThreadLocal<String> name = new ThreadLocal<>();
private static ThreadLocal<Integer> age = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
int threads = 9;
Random random = new Random();
for (int i = 0; i < threads; i++) {
Thread thread = new Thread(() -> {
name.set(Thread.currentThread().getName());
age.set(random.nextInt(40));
System.out.println("threadLocal.get()================>" + name.get() + "---" + age.get());
}, "执行线程 - " + i);
thread.start();
}
}
}
运行结果:
2.2 工作中实际应用
2.2.1 网关
需求:
新上线一个需求(例如微信上的炸弹表情,刚开始只能一部分人看到),在生产环境采用蓝绿环境发布,用户访问时在请求头携带用户类型参数,一部分访问蓝环境(已上线新需求),另外一部分访问绿环境(未上线新需求)。在网关处拦截用户请求,获取用户类型字段,将不同用户导流到不同环境。
//负载均衡,根据用户找到对应的服务并返回
@Component
public class RibbonParameters {
private static final ThreadLocal local = new ThreadLocal();
// get
public static <T> T get(){
return (T)local.get();
}
// set
public static <T> void set(T t){
local.set(t);
}
}
//切面类,拦截用户请求,
//将用户请求头version字段放进当前进程的ThreadLocal
@Aspect
@Component
public class RequestAspect {
@Pointcut("execution(* com.struggle.controller..*Controller*.*(..))")
private void anyMehtod(){
}
@Before(value = "anyMehtod()")
public void before(JoinPoint joinPoint){
HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();
String version = request.getHeader("version");
Map<String,String> map = new HashMap<>();
map.put("version",version);
RibbonParameters.set(map);
}
}
public class GrayRule extends AbstractLoadBalancerRule {
/**
* 根据用户选出一个服务
* @param iClientConfig
* @return
*/
@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {
}
@Override
public Server choose(Object key) {
return choose(getLoadBalancer(),key);
}
public Server choose(ILoadBalancer lb, Object key){
System.out.println("灰度 rule");
Server server = null;
while (server == null){
// 获取所有 可达的服务
List<Server> reachableServers = lb.getReachableServers();
// 获取 当前线程的参数 用户id verion=1
Map<String,String> map = RibbonParameters.get();
String version = "";
if (map != null && map.containsKey("version")){
version = map.get("version");
}
System.out.println("当前rule version:"+version);
// 根据用户选服务
for (int i = 0; i < reachableServers.size(); i++) {
server = reachableServers.get(i);
//获取服务的自定义meta(标记服务类型)
Map<String, String> metadata = ((DiscoveryEnabledServer) server).getInstanceInfo().getMetadata();
String version1 = metadata.get("version");
// 服务的meta也有了,用户的version也有了。
if (version.trim().equals(version1)){
return server;
}
}
}
//找到服务后返回
return server;
}
}
2.2.2 调用链路上下文
在项目调用链路中会连续调用很多个方法。以我比较熟悉的保险业务举例:
做过保险业务的同学一眼就懂:
没用ThreadLocal之前:
@Service
public class XXXBL extends AbstractBL {
@Override
public TradeInfo getData(TradeInfo requestInfo) {
}
@Override
public TradeInfo checkData(TradeInfo requestInfo) {
//取出保险行业的那些表进行检查 lccont lcpol LCInsured。。。 这个行业都懂的
}
@Override
public TradeInfo dealData(TradeInfo requestInfo) {
}
}
@Controller
public class XXXController(){
@autowired
private XXXBL xxxBL;
public TradeInfo xxx(TradeInfo requestInfo){
requestInfo = xxxBL.getData(requestInfo);
requestInfo = xxxBL.checkData(requestInfo);
requestInfo = xxxBL.dealData(requestInfo);
return requestInfo;
}
}
用了之后:
@Component
public class TradeInfoThreadLocal {
private static final ThreadLocal TradeInfoThreadLocal = new ThreadLocal();
// get
public static <T> T get(){
return (T)TradeInfoThreadLocal.get();
}
// set
public static <T> void set(T t){
TradeInfoThreadLocal.set(t);
}
//remove
}
@Service
public class XXXBL extends AbstractBL {
@Override
public TradeInfo getData() {
TradeInfo requestInfo = TradeInfoThreadLocal.get();
}
@Override
public TradeInfo checkData() {
//取出保险行业的那些表进行检查 lccont lcpol LCInsured。。。 这个行业都懂的
TradeInfo requestInfo = TradeInfoThreadLocal.get();
}
@Override
public TradeInfo dealData() {
TradeInfo requestInfo = TradeInfoThreadLocal.get();
}
}
@Controller
public class XXXController(){
@autowired
private XXXBL xxxBL;
public TradeInfo xxxMethod(TradeInfo requestInfo){
try{
TradeInfoThreadLocal.set(requestInfo);
xxxBL.getData();
xxxBL.checkData();
xxxBL.dealData();
}catch(Exception e){
log();
}finally {
TradeInfoThreadLocal.remove();
}
return requestInfo;
}
}
2.3 框架中的应用
2.3.1 Spring的数据库连接
Spring采用Threadlocal的方式,来保证线程中的数据库操作使用的是同一个数据库连接。同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象。通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。
Spring的TransactionSynchronizationManager类,它将一个以DataSource为key,Connection为value的一个Map存放到Threadlocal中,也就意味着同一个线程的同一个DataSource一定会取到同一个连接。所以如果我们想写一个类似mybatis的框架来接入spring事务管理,是需要在获取数据库连接这一块使用Spring的数据库连接管理的,也就是这种采用Threadlocal的方式。
这里参考了 https://blog.youkuaiyun.com/weixin_44366439/article/details/90381619
2.3.2 全链路跟踪
在全链路跟踪框架中,Trace信息的传递功能是基于ThreadLocal的。
ThreadLocal<String> traceContext = new ThreadLocal<>();
String traceId = Tracer.startServer();
traceContext.set(traceId) //生成trace信息 传入threadlocal
...
Tracer.startClient(traceContext.get()); //从threadlocal获取trace信息
Tracer.endClient();
...
Tracer.endServer();
2.3.4 MDC
相信很多人都用过。
有空专门写一篇。
2.3.5 局限和不足
来看看阿里的规范:
每个线程往ThreadLocal中读写数据是线程隔离的,线程间不会影响,所以ThreadLocal无法解决共享对象的更新问题。
由于不需要共享信息,自然也就不存在竞争问题,从而保证了线程的安全问题,以及避免了需要考虑线程安全必须同步带来的性能损失。
类似场景阿里规范里面也提到了:
如果想共享线程的ThreadLocal数据怎么办?
使用InheritableThreadLocal可以实现多个线程访问ThreadLocal的值,我们在主线程中创建一个InheritableThreadLocal的实例,然后在子线程中得到这个InheritableThreadLocal实例中设置的值。
public static void testInheritableThreadLocal() {
final ThreadLocal threadLocal = new InheritableThreadLocal();
threadLocal.set("线程共享ThreadLocal的值");
Thread t = new Thread() {
@Override
public void run() {
super.run();
System.out.println(threadLocal.get());
}
};
t.start();
}
3 数据结构和源码分析
3.1 类结构
threadLocals是thread是一个属性
类型为threadlocalmap
threadlocalmap为 threadlocal的内部类
threadlocalmap内部有一个继承了WeakReference的Entry内部类
和一个Entry数组
Entry内部的key被修饰为WeakReference,也就是ThreadLocal
简化版的类结构
public class ThreadLocal<T> {
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private Entry[] table;
}
}
传递逻辑:
Thread初始化init的逻辑:
如果线程的inheritThreadLocals变量不为空,而且父线程的inheritThreadLocals也存在,那么就把父线程的inheritThreadLocals赋给当前线程的inheritThreadLocals。
3.2 增删查
3.2.1 get方法
/**
* 返回此线程局部变量的当前线程副本中的值。如果该变量没有当前线程的值,
* 则首先通过调用{@link #initialValue}方法将其初始化为*返回的值。
*
* @return 当前线程局部变量中的值
*/
public T get() {
//获取当前线程的实例
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
//根据当前的ThreadLocal实例获取ThreadLocalMap中的Entry,使用的是ThreadLocalMap的getEntry方法
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T) e.value;
return result;
}
}
//线程实例中的threadLocals为null,则调用initialValue方法,并且创建ThreadLocalMap赋值到threadLocals
return setInitialValue();
}
private T setInitialValue() {
// 调用initialValue方法获取值
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
// ThreadLocalMap如果未初始化则进行一次创建,已初始化则直接设置值
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
protected T initialValue() {
return null;
}
3.2.2 set方法
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1); // 用key的hashCode计算槽位
// hash冲突时,使用开放地址法
// 因为独特和hash算法,导致hash冲突很少,一般不会走进这个for循环
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) { // key 相同,则覆盖value
e.value = value;
return;
}
if (k == null) { // key = null,说明 key 已经被回收了,进入替换方法
replaceStaleEntry(key, value, i);
return;
}
}
// 新增 Entry
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold) // 清除一些过期的值,并判断是否需要扩容
rehash(); // 扩容
}
3.2.3 remove方法
/**
* Remove the entry for key.
*/
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
4 内存泄露
其实ThreadLocal本身不存放任何的数据,而ThreadLocal中的数据实际上是存放在线程实例中,从实际来看是线程内存泄漏,底层来看是Thread对象中的成员变量threadLocals持有大量的K-V结构,并且线程一直处于活跃状态导致变量threadLocals无法释放被回收。
ThreadLocalMap中的Entry结构的Key用到了弱引用WeakReference,java虚拟机GC时会回收ThreadLocalMap中的这些Key,此时,ThreadLocalMap中会出现一些Key为null,但是Value不为null的Entry项,这些Entry项如果不主动清理,就会一直驻留在ThreadLocalMap中。也就是为什么ThreadLocal中get()、set()、remove()这些方法中都存在清理ThreadLocalMap实例key为null的代码块。总结下来,内存泄漏可能出现的地方是:
大量地(静态)初始化ThreadLocal实例,初始化之后不再调用get()、set()、remove()方法。
5 总结
5.1 使用场景
主要用于线程内进行数据传递并且线程间数据隔离。
5.2 数据结构:
threadLocals是thread是一个属性
类型为threadlocalmap
threadlocalmap内部有一个继承了WeakReference的Entry内部类
和一个Entry数组
Entry内部的key被修饰为WeakReference,也就是ThreadLocal。
5.3 内存泄露
ThreadLocalMap中的Entry结构的Key用到了弱引用WeakReference,java虚拟机GC时会回收ThreadLocalMap中的这些Key,此时,ThreadLocalMap中会出现一些Key为null,但是Value不为null的Entry项。
所以使用完要及时调用remove方法主动回收。
5.4 拓展
除了上面这些能在开发和面试中使用的知识外,还有一些其他有意思的知识点:
5.4.1 hash冲突
ThreadLocal根据key算完hash冲突后,并不是像Hashmap那样使用链表存储,而是使用了开放地址法,通过算法继续寻找其他空闲的槽位。
使用这种算法的原因我猜测是ThreadLocal并不像Hashmap那样频繁的大量的存入和获取值。默认和扩充的槽位基本就够用了。
5.4.2 黄金分割数 魔数0x61c88647
把一条线段分割为两部分,使其中一部分与全长之比等于另一部分与这部分之比。其比值是一个无理数,取其前三位数字的近似值是0.618。由于按此比例设计的造型十分美丽,因此称为黄金分割,也称为中外比。
黄金分割奇妙之处,在于其比例与其倒数是一样的。
例如:1.618的倒数是0.618,而1.618:1与1:0.618是一样的。
确切值为(√5-1)/2 ,即黄金分割数。
这个数值的作用不仅仅体现在诸如绘画、雕塑、音乐、建筑等艺术领域,而且在管理、工程设计等方面也有着不可忽视的作用。
我们先尝试求出无符号整型和带符号整型的黄金分割数的具体值:
public static void main(String[] args) throws Exception {
//黄金分割数 * 2的32次方 = 2654435769 - 这个是无符号32位整数的黄金分割数对应的那个值
long c = (long) ((1L << 32) * (Math.sqrt(5) - 1) / 2);
System.out.println(c);
//强制转换为带符号为的32位整型,值为-1640531527
int i = (int) c;
System.out.println(i);
}
也就是2654435769为32位无符号整数的黄金分割值,而-1640531527就是32位带符号整数的黄金分割值。
而ThreadLocal中的哈希魔数正是1640531527(十六进制为0x61c88647)
为什么要使用0x61c88647作为哈希魔数?这里提前说一下ThreadLocal在ThreadLocalMap(ThreadLocal在ThreadLocalMap以Key的形式存在)中的哈希求Key下标的规则:
哈希算法:keyIndex = ((i + 1) * HASH_INCREMENT) & (length - 1)
其中,i为ThreadLocal实例的个数,这里的HASH_INCREMENT就是哈希魔数0x61c88647,length为ThreadLocalMap中可容纳的Entry(K-V结构)的个数(或者称为容量)。在ThreadLocal中的内部类ThreadLocalMap的初始化容量为16,扩容后总是2的幂次方,因此我们可以写个Demo模拟整个哈希的过程:
public class ThreadLocalTest{
private static final int HASH_INCREMENT = 0x61c88647;
public static void main(String[] args) throws Exception {
hashCode(4);
hashCode(16);
hashCode(32);
}
private static void hashCode(int capacity) throws Exception {
int keyIndex;
for (int i = 0; i < capacity; i++) {
keyIndex = ((i + 1) * HASH_INCREMENT) & (capacity - 1);
System.out.print(keyIndex);
System.out.print(" ");
}
System.out.println();
}
}
上面的例子中,我们分别模拟了ThreadLocalMap容量为4,16,32的情况下,不触发扩容,并且分别”放入”4,16,32个元素到容器中,输出结果如下:
3 2 1 0
7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0
7 14 21 28 3 10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0
每组的元素经过散列算法后恰好填充满了整个容器,也就是实现了完美散列。
看看源码咋说的:
/**
* The difference between successively generated hash codes - turns
* implicit sequential thread-local IDs into near-optimally spread
* multiplicative hash values for power-of-two-sized tables.
*/
private static final int HASH_INCREMENT = 0x61c88647;
突然觉得数学很奇妙。
这部分参考了 https://blog.youkuaiyun.com/wangnanwlw/article/details/108866086