ThreadLocal使用和原理(完结)

一 是什么

作者的描述:

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.csdn.net/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.csdn.net/wangnanwlw/article/details/108866086

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值