技术干货

Java关键字ThreadLocal实践

2020-03-03  本文已影响0人  9662aed9a68b

微信公众号: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等容器,一个请求一个线程来处理的,如果有一些信息是线程共享的,有二种方式保证线程安全:

这种思想是典型的用空间换取降低线程安全风险和加锁耗时的做法。

看一个多线程处理请求的例子,这张图可以结合源码看:

在这里插入图片描述

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流水账,纯粹的个人技术公众号。

在这里插入图片描述
上一篇下一篇

猜你喜欢

热点阅读