Java关键字ThreadLocal实践

微信公众号:Java流水账
本号记录国服安琪拉日常编程流水帐,欢迎后台留言

为啥写ThreadLocal

背景:发现很多博客关于ThreadLocal的说明写错了,ThreadLocal不是维护了key为Thread对象的Map,而是Thread对象维护了一个key为ThreadLocal的Map。
下面截取的源码说明了这个问题,如果觉得晦涩,可以先看后面的实例,再回过头来看源码。

//类Thread
public class Thread implements Runnable {
    /***Thread类内部维护了一个ThreadLocalMap变量***/
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

//类ThreadLocal
public class ThreadLocal<T> {
    public void set(T value) {
        Thread t = Thread.currentThread(); //获取当前线程对象
        ThreadLocalMap map = getMap(t);  //拿到线程私有的ThreadLocalMap
        if (map != null)
            map.set(this, value);  //ThreadLocal对象为key 
        else
            createMap(t, value);  
    }

   public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t); //获取Thread对象私有ThreadLocalMap对象
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);//拿到节点
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue(); //设置初始值
    }
   //设置Thread对象的ThreadLocalMap
   void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);  
    }

   ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //hashcode & 运算去除高位  等同于取余
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }
}

一般像网络请求,使用线程池技术的,Tomcat/Jetty等容器,一个请求一个线程来处理的,如果有一些信息是线程共享的,有二种方式保证线程安全:

  • 一种就是常规的加锁,当然也包括cas这种;
  • 一种就是使用ThreadLocal做线程隔离,线程访问的是私有的变量,修改的也是线程对象Map自己的那份。

这种思想是典型的用空间换取降低线程安全风险和加锁耗时的做法。
看一个多线程处理请求的例子,这张图可以结合源码看:
在这里插入图片描述
Request对象是Handler的成员变量,多线程访问有线程安全问题,使用ThredLocal对Request对象做线程隔离,可以让多线程执行handle()线程安全,再次说明:线程内部Map key为ThreadLocal对象,value为Request对象,因此实际上是每个线程有自己的线程局部变量,省去了锁的开销。

下面是一段简单用法,只是为了演示写的。

public static void main(String[] args) {

        ThreadLocal<Person> threadLocal = new ThreadLocal<>(); //每个线程局部变量都需要创建一个ThreadLocal对象
        ThreadLocal<String> threadLocal2 = new ThreadLocal<>();
        AtomicInteger atomicInteger = new AtomicInteger(1000);

        ExecutorService executorService = Executors.newCachedThreadPool();

     //for(int i 0->10)
            executorService.submit(() -> {
                Person person = new Person();
                person.setIdcard(atomicInteger.getAndIncrement());
                person.setName(Thread.currentThread().getName());
                threadLocal.set(person);
                threadLocal2.set("haha");
                System.out.println(threadLocal.get());
                System.out.println(threadLocal2.get());

                System.out.println();

                threadLocal.remove();
            });
        executorService.shutdown();
    }

我的实际应用

举一个我的项目中使用场景:
 背景:上一个项目在优化整体代码,把项目我负责的主体的实现方式做了调整,ThreadLocal在其中扮演了非常重要的角色。需求(使用场景):客户端有一个请求过来,Java程序根据请求报文的参数决定调用某个服务提供数据。相信大家都会有类似的需求,拿我们业务来说,请求报文如下:
{ "appid": "88888888", "userid": 80, "datatype": "***", "data":"{"dataids":[146,147,148]}" }
一个用户请求过来,需要根据报文中的datatype字段决定调取不同的数据提供服务。大体如下图所示:
在这里插入图片描述

我在基类定义了一个ThreadLocal 对象来隔离PullDataNotification(封装请求信息)这个类成员变量,因为之前项目由于线程安全问题,每一个请求都是new 一个全新的Provider 处理来规避这个问题,看Cat上GC新生代很频繁,为了性能提升使用ThreadLocal 只创建一个对象,降低对象的频繁创建和销毁。

public abstract class CommonCreditProvider<E extends CreditArgument> implements IDataProvider{
    //基类定义ThreadLocal对象,如果有多个变量做线程隔离可以初始化多个对象
    protected ThreadLocal<PullDataNotification> notificationThreadLocal = new ThreadLocal<>()

    //主要处理函数  实际处理逻辑在process,由子类完成
    @Override
    PullDataResult getData(PullDataNotification notification) throws Exception {
        PullDataResult pullDataResult = null
        try{
            notificationThreadLocal.set(notification)
            getThreeElements()
            E creditArgument = initArguments()

            pullDataResult = process(creditArgument)
        }catch (Exception ex){
            throw ex
        }finally{
            //*****使用完手动remove******
            notificationThreadLocal.remove()
        }

        return pullDataResult
    }
}

重点:关注一个细节,我finally 代码块手动remove把线程变量从Thread对象的ThreadLocalMap中移除,因为如果不这么做可能有内存泄漏风险。
原因:看源码,引用链,Thread -> ThreadLoalMap -> Entry[ WeakRefrence, Object] 因为线程对象(Tomcat请求线程池等)存在,线程维护的Map的Entry数组如果不手动清除也还存在,根据JVM GC的根路径定位,那数组中Entry的key,value是变量都在链上,所以会有内容泄漏的风险,所以要在使用完之后手动调用remove()函数清除。

后记

网上的很多文章,讲ThreadLocal中的ThreadLocalMap存放的key是线程对象,value是设置的线程局部变量,乍一看觉得挺有道理的,也算实现了线程数据隔离。但是仔细想不合理,如果有多个变量都要线程隔离,key又是线程对象,那key不够用啊!看源码,发现网上博客很多写的是错的,所以还是以后遇到有疑问的多想想What? How?Why?这东西是什么?怎么用? 为什么这么实现?合理吗?


这里没有敷衍的复制粘贴,博眼球的面试资料分享,有的只是尽可能清晰的讲清个人开发中遇到的一个个问题和总结。欢迎大家关注Java流水账,纯粹的个人技术公众号。


在这里插入图片描述

欢迎关注Java流水账公众号
原文地址:https://www.cnblogs.com/guofu-angela/p/12403790.html