【Java】深入解析ThreadLocal——Java并发编程的秘密武器

目录

一、常用方法

二、ThreadLocalMap内部结构

三、为什么用ThreadLocal做key

四、ThreadLocalMap如何查找数据

五、父子线程如何共享数据

六、 ThreadLocal如何避免内存泄露

七、ThreadLocal应用场景

1. 线程数据隔离

2. 跨函数传递


   

        ThreadLocal 被称为线程局部变量,用于在线程中保存数据。由于在 ThreadLocal中保存的数据仅属于当前线程,所以该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。

        ThreadLocal 用于在同一个线程间,在不同的类和方法之间共享数据的的场景,也可以用于在不同线程间隔离数据的场景。

        ThreadLocal利用 Thread 中的 ThreadLocalMap 来进行数据存储。

一、常用方法

1. 存储数据至当前线程的 ThreadLocalMap:public void set(T value)

2. 从当前线程的 ThreadLocalMap 中获取数据:public T get()

3. 从当前线程的 ThreadLocalMap 中删除数据:public void remove()

        在线程池的线程复用场景中,线程执行完毕时一定要调用remove(),避免在线程被重新放入线程池中时被本地变量的旧状态仍然被保存。

        ThreadLocal的get()方法、set()方法和remove()方法,其实最终操作的都是 ThreadLocalmap 类中的数据。

二、ThreadLocalMap内部结构

        ThreadLocalMap 内部数据结构是一个 Entry 类型的数组。每个 Entry 对象的 key为 ThreadLocal对象,value 为存储的数据

三、为什么用ThreadLocal做key

        如果在应用中,一个线程中只使用了一个ThreadLocal 对象,那么使用 Thread做 key 也是可以的,代表每个 Thread 线程对应一个 value 。

        但是,在实际应用程序开发过程中,一个线程中很有可能不只使用了一个ThreadLocal 对象。这时使用 Thread 做 key 就会产生混淆

        所以,不能使用 Thread 做 key ,而应该改成用 ThreadLocal 对象做 key,这样才能通过具体 ThreadLocal对象的get()方法,获取到当前线程的 ThreadLocalMap,然后进一步获取到对应的Entry。

public class Test {
    private static ThreadLocal<String> namethreadLocal = new ThreadLocal();
    private static ThreadLocal<String> idthreadLocal = new ThreadLocal();

    public static void main(String[] args) {

        Thread t1 = new Thread(()->{
            try{
                // 保存数据
                namethreadLocal.set("马超");
                idthreadLocal.set("001");

                // 调用方法,查看获取数据
                dosth();
            }finally {
                namethreadLocal.remove();
                idthreadLocal.remove();
            }
        });

        Thread t2 = new Thread(()->{
            try{
                namethreadLocal.set("马云");
                idthreadLocal.set("008");
                dosth();
            }finally {
                namethreadLocal.remove();
                idthreadLocal.remove();
            }
        });

        t1.start();
        t2.start();

    }
    public static void dosth(){
        // 通过threadLocal,获取当前线程中的数据
        String name = namethreadLocal.get();
        System.out.println(Thread.currentThread().getName()+"-dosth:"+name);

        String id = idthreadLocal.get();
        System.out.println(Thread.currentThread().getName()+"-dosth:"+id);

        // 线程继续调用方法,获取当前线程的数据
        show();
    }

    public static void show(){
        // 通过threadLocal,获取当前线程中的数据
        String name = namethreadLocal.get();
        System.out.println(Thread.currentThread().getName()+"-show:"+name);

        String id = idthreadLocal.get();
        System.out.println(Thread.currentThread().getName()+"-show:"+id);
    }

}

四、ThreadLocalMap如何查找数据

        当我们使用 ThreadLocal 获取当前线程中保存的 Value 数据时,是以 ThreadLocal对象作为 Key,通过访问 Thread 当前线程对象的内部 ThreadLocalMap 集合来获取到Value 。

        ThreadLocalMap 集合的底层数据结构使用 Entry[]数组保存 Key-Value 键值对数据。所以,当通过ThreadLocal的get、set()、remove()等方法,访问 ThreadLocalMap 时,最终都会通过一个下标,来完成对数组中的元素访问。

int i = key.threadLocalHashcode &(len-1);

        通过 key 的” hashcode 值”跟"数组的长度减1"做“&按位与”运算。其中 key 就是ThreadLocal 对象。这种计算方式,相当于用“hashcode 值”跟“数组的长度”进行”%取余”运算。

        假设:len=16,key.threadLocalHashCode=31
        
        hash & len-1 的计算结果与 hash % len 的计算结果一直,均为 15 ,但是“&按位与”运算的效率更高

五、父子线程如何共享数据

        在实际工作中,有可能需要在父子线程共享数据的。即:在父线程中往 ThreadLocal 设置了值,在子线程中能够获取到。

        使用JDK自带的InheritableThreadLocal类,该类继承了ThreadLocal

// 父子线程传递数据
public class Test{
    public static void main(String[] args) {

        // ThreadLocal threadLocal = new ThreadLocal();  // 子线程不能获取父线程数据
        InheritableThreadLocal threadLocal = new InheritableThreadLocal(); // 子线程可以获取父线程数据

        threadLocal.set("天王盖地虎");
        System.out.println(Thread.currentThread().getName()+"主线程:"+threadLocal.get());

        Thread t = new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"子线程:"+threadLocal.get());
        });
        t.start();
    }
}

六、 ThreadLocal如何避免内存泄露

        使用完毕后,在 finally 调用 ThreadLocal对象的 remove()方法

        需要特别注意的地方是:一定要在 finally 代码块中,调用 remove()方法清理没用的数据。如果业务代码出现异常,也能及时清理没用的数据。

         remove()方法中会把 Entry 中的 key 和 value 都设置成 null,这样就能被 GC 及时回收,无需触发额外的清理机制,所以它能解决内存泄露问题。
       

七、ThreadLocal应用场景

1. 线程数据隔离

        ThreadLocal 的主要价值在于线程隔离,ThreadLocal 中的数据只属于当前线程,该数据对别的线程是不可见的,起到隔离作用。这样操作,可以在多线程环境下,可以防止当前线程的数据被其他线程修改。另外,由于各个线程之间的数据相互隔离,避免了同步加锁带来的性能损失,大大提升了并发性的性能。

        例如:sqlSession会话对象绑定,避免多个线程使用同一个sqlsession 对象,由于关闭导致异常。


2. 跨函数传递

        数据通常用于同一个线程内,跨类、跨方法传递数据时,如果不用ThreadLocal,那么相互之间的数据传递势必要靠返回值和参数,这样无形之中增加了这些类或者方法之间的耦合度。



        

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值