1 ThreadLocal-原理分析

ThreadLocal可以解决多线程中的什么问题,原理是什么

多线程访问同一个资源时,需要加锁,只有一个线程访问,才能保证操作的原子性。ThreadLocal保证原子性的原理跟锁不一样,是把资源分成N份,每个线程一份,这样就不需要考虑加锁了,ThreadLocal只有当前线程才能访问到相关资源。

原来看到一篇文章,将锁和ThreadLocal的区别写的很生动,文章地址记不得了,先简单说说:你有两个孩子,都喜欢小兔子玩偶,家里就一个。这时候两个孩子就相当于两个线程,小兔子玩偶就是资源, 两个小朋友都想玩,就是竞争资源。

  1. 加锁:孩子的爸爸过来,要求一个孩子玩10分钟,然后轮流玩,中间大人还需要管控顺序,保证每个孩子只能玩10分钟,大人能累死,小朋友估计也少不了哭。
  2. ThreadLocal:家里有两个孩子,直接买两个兔子玩偶,一个小朋友一个,这样就不需要考虑小朋友争抢了,大人也轻松了。

当然ThreadLocal的应用场景比较少,但是也可以解决一部分的多线程问题。

常见使用场景:

  1. CurrentUser,mvc架构中,需要获取当前操作用户,就可以利用ThreadLocal作为User资源,因为每个线程只可能有一个登录用户。
  2. SimpleDateFormate是线程不安全的,那么可以用ThreadLocal包装SimpleDateFormate,这样保证只有当前线程才能访问内部的SimpleDateFormate。
  3. 参数传递(局部缓存):尤其是针对老旧功能的修改,例如:原来订单商品是没有服务商品概念的,现在需要添加服务商品,要么修改原有的接口方法,或者入参的结构,但是也可以用ThreadLocal存储响应的服务商品,这样所有的方法中,都可以获取服务商品信息。其实也是局部缓存,缓存了整个线程全流程的共享数据。

自定义ThreadLocal

ThreadLocal设置的数据,只对当前线程有效,第一个想到的是Map,key=线程,value=设置的值。

public class MyThreadLocal<T> {
    private Map<Thread, T> localMap = new HashMap<>();

    public T set(T t) {
        return localMap.put(Thread.currentThread(), t);
    }

    public T remove() {
        return localMap.remove(Thread.currentThread());
    }

    public T get() {
        return localMap.get(Thread.currentThread());
    }
}


public class CurrentUserMyThreadLocal {
    private static MyThreadLocal<User> USER_TL = new MyThreadLocal<User>();

    public static User set(User user) {
        return USER_TL.set(user);
    }

    public static User get() {
        return USER_TL.get();
    }

    public static User remove() {
        return USER_TL.remove();
    }


    public static void print() {
        USER_TL.print();
    }
}

public class MyThreadLocalDemo {
    public static void main(String[] args) {
        CurrentUserMyThreadLocal.set(new User(1L, "测试"));
        test();
        int i = 0;
        while (i < 100000) {
            i++;
        }
        System.out.println(CurrentUserMyThreadLocal.get());
    }
    private static void test() {
        System.out.println(CurrentUserMyThreadLocal.get());
        new Thread(new Runnable() {
            @Override
            public void run() {
                CurrentUserMyThreadLocal.set(new User(2L, "test2"));
            }
        }).start();
    }
}

通过MyThreadLocalDemo,设置后,在其他方法中也是可以获取的。
缺点:

  1. 如果存在很多MyThreadLocal变量,会有很多个HashMap存在,会浪费一部分的内存。
  2. 无法保证只有当前线程能访问。
  3. 当线程退出时,会导致map中部分key=null的情况,存在内存泄漏。

ThreadLocal的数据结构

ThreadLocal用法

public class ThreadLocalDemo {
    private static ThreadLocal<User> userTL = new ThreadLocal<User>();

    public static void main(String[] args) {
        userTL.set(new User());
    }
}

查看userTL.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的结构中。但是key却不是想想中的Thread,而是this,从方法调用链上说,this就是threadLocal变量。

ThreadLocalMap

ThreadLocalMap的来源:
ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

通过上述的getMap发现ThreadLocalMap源自Thread,但是ThreadLocalMap又是ThreadLocal的内部类,但是ThreadLocalMap的key又是ThreadLocal实例,这是很奇怪的引用链。
示例:
在这里插入图片描述

ThreadLocalMap为什么是Map

正常情况下,想要通过Thread传递数据,可以直接定义一个Object变量即可,。如下:

class Thread {

    private Object localVar;

    public void set(Object var) {
        this.localVar = var;
    }
    
    public Object get(){
        return localVar;
    }
}

通过set初始化,通过get使用。但是当需要传递多个value时,通过localVar持有变量就存在覆盖。因此当需要传递多个值时,并且按需获取时,最好的数据结构就是Map,因此进化为:

class Thread {

    private Map<String,Object> localMap;

    public void set(String key,Object var) {
        localMap.put(key,var);
    }

    public Object get(String key){
        return localMap.get(key);
    }
}

ThreadLocalMap的key=ThreadLocal

上述确认,利用Thread传递数据,必须是通过map持有数据,才能做到按需获取。map作为value的存储,就涉及到key的问题。

  1. 字符串作为map的key:
    就像上述的实例一样,使用时需要:
public static void main(String[] args) {
        
        Thread.currentThread.set("currentUser",currentUser);
        
        Thread.currentThread.get("currentUser");
    }

设置时,需要手写key,获取时还需要手写key,这就存在手动输错的可能,当然也可以利用常量表示字符串key。

  1. 对象作为map的key:
    刚才使用String字符串作为key,在get时,需要手动再输入一遍,因此可以使用对象作为Map的key。例如:
class ThreadLocal {

}

class Thread {

    private Map<ThreadLocal, Object> localMap;

    public void set(ThreadLocal key, Object var) {
        localMap.put(key, var);
    }

    public Object get(ThreadLocal key) {
        return localMap.get(key);
    }

    public static void main(String[] args) {
        ThreadLocal tl = new ThreadLocal();
        Thread.currentThread.set(tl, currentUser);

        Thread.currentThread.get(tl);
    }
}

定义ThreadLocal变量,这样set和get时,只需要tl作为key即可。还是有一些缺陷:1 当前Thread的Map没有办法处理泛型,需要进行类型转换;2 set和get时仍然繁琐。

  1. 对象作为key,同时作为获取vaue的入口
    上述对象作为key时,获取value时,代码仍然有点冗余,是否可以考虑对象既作为map的key,同时也作为getValue的发起方。
class ThreadLocal<V> {
    public void set(V v){
        Thread thread = Thread.currentThread;
        thread.map.put(this,v);

    }

    public V get(){
        Thread thread = Thread.currentThread;
        return thread.map.get(this);
    }

	public static void main(String[] args) {
        ThreadLocal<CurrentUser> tl = new ThreadLocal();
        tl.set(new CurrentUser(1L,"test"));
        tl.get();
    }
}

通过上述优化,就可以很简洁的使用ThreadLocal,其实通过上述变化,也能发现一个问题,ThreadLocal其实可以看做持有value的一个变量,如下所示:

class ThreadLocal<V> {
    private V value;

    public void set(V v) {
        this.value = v;
    }

    public V get() {
        return value;
    }
}

只不过ThreadLocal并没有实际持有value,因为ThreadLocal如果持有value,就需要考虑多个Thread的value区分,因此ThreadLocal只提供了Thread.map的访问出入口。

ThreadLocalMap为什么是ThreadLocal的内部类

通过上述分析,Thread.map的访问出入口是ThreadLocal,因此ThreadLocalMap就不允许通过常规的get和set进行访问了,因此相关方法必须是私有,但是set和get的私有,也导致ThreadLocal也无法访问,而内部类的private方法可以被外部类访问,因此ThreadLocalMap是ThreadLocal的内部类。

1 把Thread中的map必须进行包装,不允许通过正常map的get和set方法访问,上述方法private。2 包装后的map类型,必须是ThreadLocal的内部类(外部类可以访问内部类的私有方法)。

经过这样处理:Thread的ThreadLocalMap变量,负责存储所有的value,ThreadLocal作为ThreadLocalMap的set/get入口,ThreadLocal作为ThreadLocalMap的key,简化ThreadLocalMap value的set、get操作。

ThreadLocal传递数据,其实有两种方式:
value分散:指一个Thread需要传递的值,分散在Map中。即需要一个Map,key=Thread,value=Map(key=业务key,value=数据)。

value聚集:Thread提供一个Map(key=ThreaLocal,value=数据),存放需要该Thread传递的数据,通过ThreadLocal对外暴露访问。

ThreadLocal会产生内存泄漏?

在ThreadLocal的数据结构中,已经画出了相关的引用图例,发现Thread.ThreadLocalMap中的key=ThreadLocal,但是却对key做了弱引用(gc时,如果对象被弱引用持有,那么是可以直接回收),为什么不用强引用。

ThreadLocalMap的回收

  1. 线程消亡:线程消亡时,ThreadLocalMap会被设置为null,此时就不存在ThreadLocal的内存泄漏。存储都没有了,哪来的泄漏。
  2. 线程长时间存活:主要是线程池内的线程,这些线程存活时间很长,ThreadLocalMap会长时间存在,因此就会存在一个引用链:ThreadLocalRef->ThreadLocalMap->Entry->key->ThreadLocal。

ThreadLocalMap强引用。

因此如果key->ThreadLocal存在强引用时,就会导致ThreadLocal对象无法回收,因为GC可达,但是ThreadLocal变量缺已经被回收。尤其是ThreadLocal作为局部变量,或者普通属性时。例如:

局部变量:

public static void main(String[] args) {
        ThreadLocal<CurrentUser> tl = new ThreadLocal();
        tl.set(new CurrentUser(1L,"test"));
        tl.get();
    }

普通属性:

public class BizService {
	private ThreadLocal<User> userTL = new ThreadLocal();
	
}

上述ThreadLocal的用法,也存在内存泄漏,因为ThreadLocal变量(指针)随着方法结束或者BizService被回收,而被回收,但是ThreadLocal对象,因为ThreadLocalMap的强引用而存活,但是我们已经没有ThreadLocal对象的访问入口(变量已经被回收),因此导致Entry也无法被正常访问,那么此时就会导致Entry和ThreadLocal的内存泄漏有。

综上所述:ThreadLocalMap的key为强引用时,尤其是作为局部变量和普通属性时,会导致ThreadLocal和Entry的内存泄漏。

ThreadLocalMap key 弱引用

弱引用:当一个对象仅被弱引用持有,那么该对象gc时,会被回收,再次通过弱引用获取该对象,返回null。

因此如果ThreadLocalMap的key为弱引用,当ThreadLocal为局部变量和普通属性时,不会影响ThreadLocal对象的回收。但是仍然存在内存泄漏。

因为ThreadLocalMap的key为弱引用,那么上述情况gc后,会导致Entry的key=null,同样也是因为无法通过ThreadLocal(被gc掉了)访问Entry,导致Entry的内存泄漏。

为了解决上述问题:ThreadLocal在set和get操作时,添加了这对Entry中key=null情况的排查,当key=null时,释放持有的value,减少内存泄漏的风险。

private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }


private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                if (k == null)
                	
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

综上所述:ThreadLocalMap的弱引用,只是减少内存泄漏的风险,但是不能避免内存泄漏,举个极端的例子:ThreadLocal整个声明周期中,就只有set和get各一次,这种情况下,无法触发ThreadLocal的自动清除key=null的机制,那么内存泄漏仍然存在。

ThreadLocal正确使用

实际情况下,我们都一般都不把ThreadLocal作为局部变量和普通属性,一般都是作为静态变量。

public class CurrentUser {
    private static ThreadLocal<User> CURRENT_USER = new ThreadLocal<>();

    public static User get() {
        return CURRENT_USER.get();
    }

    public static void set(User user) {
        CURRENT_USER.set(user);
    }

    public static void remove() {
        CURRENT_USER.remove();
    }
}

上述功能中,ThreadLocal作为静态变量,作为GCRoot,正常情况不会被gc,那么ThreadLocalMap key=弱引用,其实没有什么实际效果,因为ThreadLocal变量强引用ThreadLocal对象,key不会为null,那么就不会触发ThreadLocal的get、set自清理机制。

因此上述情况,会存在内存浪费的情况,尤其是线程长时间存活。例如:线程执行业务A时,用到了CurrentUser,执行业务B时,用到了CurrentOrg,甚至以后都不会执行业务A,那么CurrentUser在ThreadLocalMap中的数据存储就是无用的,浪费内存的。

因此当Thread执行业务前,需要set ThreadLocal,防止数据污染,执行完业务后,要手动remove,方式内存浪费。

数据污染:因为线程存在重复执行同一个业务方法,但是不同业务方法的ThreadLocal value不一样,如果不重置ThreadLocal,存在Thread访问到上次执行时,设置的ThreadLocal,导致数据污染。

ThreadLocalMap value 弱引用

如果value也如同key一样,设置为软引用,当ThreadLocal设置的value,有可能被gc回收掉,导致get时为null。

public static void main(String[] args) throws Exception {
    Map<WeakReference<Integer>, WeakReference<Integer>> map = new HashMap<>(8);
    WeakReference<Integer> key = new WeakReference<>(666);
    WeakReference<Integer> value = new WeakReference<>(777);
    map.put(key,value);
    System.out.println("put success");
    Thread.sleep(1000);
    System.gc();
    System.out.println("get " + map.get(key).get());
}

输出时:get null

总结

ThreadLocal 内存泄漏的根源是:由于ThreadLocalMap 的生命周期跟 Thread 一样长,而 Thread与ThreadLocal 的生命周期不一样长, 如果没有手动删除对应 key 就会导致内存泄漏。

ThreadLocalMap的hash冲突

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

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            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) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

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

通过线性探测解决hash冲突,首先根据hashCode与数组长度&运算获取下标,从下标开始,循环遍历整个数组,直到找到为空的。

缺点:当ThreadLocal冲突较多时,可能存在O(n)的遍历,同时移除某一个元素,需要同步前移动该元素的后续的元素。

ThreadLocal的应用场景

替代参数的显式传递(Thread上下文)

例如:methodA已经有4个参数了,现在需要进行添加另外的参数,直接在methodA中添加第五个参数,影响面大,需要修改方法的入参(或者重构),因此可以通过ThreadLocal存储变量数据,方法中直接获取使用。

局部缓存信息(Thread生命周期内部)

上述举例中,存在CurrentUser、CurrentOrg,这些都可以认为是局部缓存信息。

绑定事务、链接。

进行事务操作时,将事务与线程绑定,保证事务的唯一性。
绑定链接也是相同的原理,一一对应。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值