java多线程:ThreadLocal详解

场景: 登录用户的信息保存与获取问题。
在常规的系统设计中,后端系统通常会有一个很长的调用链路(Controller->Service->Dao)。
通常用户在登陆之后,用户信息会保存在session或token中。但假如我们在controller、service及service的多个调用方法中都要用到用户信息相关,我们可以将User对象作为参数进行方法传递,也就是将User作为context上下文。但这样极其繁琐,对于调用链路长的情况也不够优雅简洁;同时若调用链涉及到第三方库,重写的方法无法修改参数的情况下,对象就传递不进去了。我们也不能直接将User对象保存为static,因为在多个用户访问的情况的会有并发的问题。这时候我们就可以用上ThreadLocal对象,进行全局存储用户信息。

提出问题:

  • ThreadLocal是什么?用来解决什么问题?
  • ThreadLocal的使用
  • ThreadLocal的底层实现
  • ThreadLocal的内存泄漏问题

ThreadLocal是什么

ThreadLocal是一个保存线程局部变量的工具,每一个线程都可以独立地通过ThreadLocal保存与获取自己的变量,而该变量不会受其它线程的影响。

ThreadLocal的使用

ThreadLocal主要对外提供三个方法:get(), set(T)和remove()。
通常我们将threadLocal对象设置为static,以便在全局都可获取。
set(T):线程填充只属于自己线程的数据,其他线程无法获取。
get():线程获取自己set的数据。
remove():线程移除自己设置的值。

public class Test {

    public static ThreadLocal<User> threadLocal = new ThreadLocal<>();

    public static void main(String[] args){
        Thread thread1 = new Thread(()->{
            User user = new User(10, "jun");   // 模拟出用户1
            threadLocal.set(user);

            playGame();                                     // 用户1玩游戏
        }, "线程1");
        Thread thread2 = new Thread(()->{
            User user = new User(20, "ge");    // 模拟出用户2
            threadLocal.set(user);

            playGame();                                     // 用户2玩游戏
        }, "线程2");

        thread1.start();
        thread2.start();

    }

    public static void playGame(){
        int age = threadLocal.get().getAge();               // 模拟业务逻辑,登录用户年龄判断的业务逻辑
        if(age < 18){
            System.out.println("Sorry, 您未满18岁,当前" + age + "岁,不能参与当前游戏!当前线程为:" + Thread.currentThread().getName());
            return;
        }
        System.out.println("您已满18岁,当前" + age + "岁,玩得愉快!当前线程为:" + Thread.currentThread().getName());

    }
}

我们模拟创建两个用户登录后,保存进threadLocal中,再分别执行playGame方法。在上述方法中,我们直接根据threadLocal就正确地获取了线程所属的user对象,而没有在方法上传递参数。
如上,我们便解决了调用链路过长时参数传递的繁琐,免去了方法参数传递的过程。每个线程调用threadLocal的get方法时,获取的都是自己set进去的值,解决了并发的问题。

ThreadLocal原理

那么,ThreadLocal是怎么实现线程局部变量的呢?
首先我们看看set方法:

	public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
	ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

看到这里答案就出来了,通过threadLocal.set(T)设置值时,实际上就是获取当前线程的ThreadLocalMap,每个线程都持有一个ThreadLocalMap对象,该map以threadLocal为key,value即为存储的值,保存进map中。
get():

	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();
    }

同上,get方法实际上也是获取当前线程持有的threadLocalMap,以当前threadLoca作为key,从map中获取value。
总结:ThreadLocal实现线程局部变量的方法,就是每个线程都持有维护了一个threadLocalMap,在执行threadLocal对象的get和set方法时,都是获取当前线程的map对象,再以当前的threadLocal为key,进行value的操作,从而实现了线程局部变量的隔离

ThreadLocalMap底层实现:

每个线程中都持有了一个ThreadLocalMap用来存放线程局部变量,而ThreadLocalMap是为了实现ThreadLocal功能特意编写的map类,为什么不用现成的HashMap呢?
阅读ThreadLocalMap的源码,我们可以发现几个不同的点:
1、ThreadLocalMap中Entry的key设置为了弱引用
这是为了防止key的内存泄漏,下面再仔细讲讲ThreadLocal的内存泄漏问题

	static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

2、ThreadLocalMap解决hash冲突的方法。
ThreadLocalMap的hash算法为 threadLocalHashCode & (table.length - 1),而table.length指定为了2的整数次幂,因此等同于threadLocalHashCode % (table.length - 1)。
可以看到通过hash算法定位到数组下标,接着进行判断:若该entry的k为给定的key,则直接更新value;若k为空,说明该k被垃圾回收了,entry也该执行replaceStaleEntry进行清空;若不满足条件,则会获取数组entry为空下一个元素,跳出for循环。因此我们可知,ThreadLocalMap解决hash冲突的方法为定位到的数组下标往后移动。

	private void set(ThreadLocal<?> key, Object value) {

            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)]) {
                ThreadLocal<?> k = e.get();

                if (k == key) {		// k为给定的key,则直接更新value
                    e.value = value;
                    return;
                }

                if (k == null) {	// k为空,说明该k被垃圾回收了,entry也该执行replaceStaleEntry进行清空
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

3、threadLocalHashCode为0x61c88647的整数倍。那为什么是这个魔法值呢?

	private final int threadLocalHashCode = nextHashCode();

    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    private static final int HASH_INCREMENT = 0x61c88647;

    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

我们已知table.length为2的整数幂,接下来以数组长度为16、32、64为例,探讨ThreadLocal的hash值为该魔法值的整数倍,发送hash冲突的情况:

	public static void main(String[] args){
        hash(16);
        hash(32);
        hash(64);
    }

    public static void hash(int length){
        final int HASH_INCREMENT = 0x61c88647;
        int[] table = new int[length];
        int hash = 0;

        for (int i = 0; i <length ; i++) {
            hash += HASH_INCREMENT;
            table[i] = hash & (length-1);
        }

        Arrays.sort(table);
        System.out.println(Arrays.toString(table));
    }

结果分别为:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63]
同时将长度拓展到64、128、256…,都没有发生重复的情况。
因此我们可以得出结论:将ThreadLocal设置为该魔法值的整数倍,可以极大地减少存入ThreadLocalMap中的hash冲突的概率。 同时也不得不感慨作者的数学功底之深厚!

ThreadLocal的内存泄漏问题

刚刚我们有说到,ThreadLocalMap自定义的Entry继承了WeakReference,实际上便是将map中的key对threadLocal进行了弱引用。

弱引用介绍:弱引用是为了解决内存泄漏问题的,若一个对象只存在弱引用,在jvm垃圾回收时便会将该对象进行回收。
场景:A a = new A();
	 B b = new B();
	 b.a = a;
	 a = null;          // 在这里只是将a的引用置为null,因为b.a对a还有强引用,a对象便还会存在内存中而不会被垃圾回收。
解决办法:	WeakReference<A> wr = new WeakReference<>(a);
			b.wr = wr;			//将b对a的引用改为弱引用
	static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

引用关系如图:将key设置为弱引用,便可在threadLocal引用被置为null时,key对threadLocal的因为都为弱引用,jvm便可对threadLocal对象进行gc,从而防止threadLocal对象的内存泄漏。
在这里插入图片描述
但是!可以看到value的引用为强引用,若是线程能正常结束倒也还好说,线程结束了,map、entry、value的强引用都断开了,也就能被gc回收。但是通常情况下,因为线程的创建和销毁比较耗费性能,我们会使用诸如线程池的方法进行线程复用,这时候线程一直不被销毁,则很可能出现内存泄漏的问题。

解决办法

对于value内泄露的问题,ThreadLocal的开发者也注意到了,因此在调用threadLocal的get和set方法时,在碰上key为null的情况会执行replaceStaleEntry()方法清理调entry。而对于线程复用导致的内存泄漏问题,则可以在执行完毕后调用threadLocal.remove()方法手动清理。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值