ThreadLocal:Java中的影分身

  • 时间:2018-12-31 23:06 作者:杜琪 来源:杜琪 阅读:749
  • 扫一扫,手机访问
摘要:image.png老套路,先列举下关于ThreadLocal常见的疑问,希望可以通过这篇学习笔记来处理这几个问题:ThreadLocal是用来处理什么问题的?如何使用ThreadLocal?ThreadLocal的实现原理是什么?可否举几个实际项目中使用ThreadLocal的案例?基础知识Thre
image.png

老套路,先列举下关于ThreadLocal常见的疑问,希望可以通过这篇学习笔记来处理这几个问题:

  1. ThreadLocal是用来处理什么问题的?
  2. 如何使用ThreadLocal?
  3. ThreadLocal的实现原理是什么?
  4. 可否举几个实际项目中使用ThreadLocal的案例?

基础知识

ThreadLocal是线程局部变量,和普通变量的不同在于:每个线程持有这个变量的一个副本,可以独立修改(set方法)和访问(get方法)这个变量,并且线程之间不会发生冲突。

类中定义的ThreadLocal实例一般会被private static修饰,这样可以让ThreadLocal实例的状态和Thread绑定在一起,业务上,一般用ThreadLocal包装少量业务ID(user ID或者事务ID)——不同的线程使用的ID是不相同的。

如何使用

case1

从某个角度来看,ThreadLocal为Java并发编程提供了额外的思路——避免并发,假如某个对象本身是非线程安全的,但是你想实现多线程同步访问的效果,例如SimpleDateFormat,你可以使用ThreadLocal变量。

public class Foo{    // SimpleDateFormat is not thread-safe, so give one to each thread    private static final ThreadLocal<SimpleDateFormat> formatter = new ThreadLocal<SimpleDateFormat>(){        @Override        protected SimpleDateFormat initialValue()        {            return new SimpleDateFormat("yyyyMMdd HHmm");        }    };    public String formatIt(Date date)    {        return formatter.get().format(date);    }}

注意,这里针对每个线程只要要初始化一次SimpleDateFormat对象,其实跟在自己设置线程中定义一个SimpleDateFormat成员变量,并在线程初始化的时候new这个对象,效果是一样的,只是这样看起来代码更规整。

case2

之前在yunos做酷盘项目的数据迁移时,我们需要按照客户维度去加锁,每个线程在解决迁移之前,都需要先获取当前客户的锁,每个锁的key是带着客户信息的,因而也可以使用ThreadLocal变量实现:image.png

case3

下面这个例子,我们定义了一个MyRunnable对象,这个MyRunnable对象会被线程1和线程2使用,但是通过内部的ThreadLocal变量,每个线程访问到的整数都是自己单独的一份。

package org.java.learn.concurrent.threadlocal;/** * @author duqi * @createTime 2018-12-29 23:25 **/public class ThreadLocalExample {    public static class MyRunnable implements Runnable {        private ThreadLocal<Integer> threadLocal =                new ThreadLocal<Integer>();        @Override        public void run() {            threadLocal.set((int) (Math.random() * 100D));            try {                Thread.sleep(2000);            } catch (InterruptedException e) {            }            System.out.println(threadLocal.get());        }    }    public static void main(String[] args) throws InterruptedException {        MyRunnable sharedRunnableInstance = new MyRunnable();        Thread thread1 = new Thread(sharedRunnableInstance);        Thread thread2 = new Thread(sharedRunnableInstance);        thread1.start();        thread2.start();        thread1.join(); //wait for thread 1 to terminate        thread2.join(); //wait for thread 2 to terminate    }}

ThreadLocal关键知识点

源码分析

ThreadLocal是如何被线程使用的?原理如下图所示:Thread引用和ThreadLocal引用都在栈上,Thread引用会引用一个ThreadLocalMap对象,这个map中的key是ThreadLocal对象(使用WeakReference包装),value是业务上变量的值。image.png

首先看java.lang.Thread中的代码:

publicclass Thread implements Runnable {    //......其余源码    /* ThreadLocal values pertaining to this thread. This map is maintained by the ThreadLocal class. */    ThreadLocal.ThreadLocalMap threadLocals = null;    /*     * InheritableThreadLocal values pertaining to this thread. This map is maintained by the InheritableThreadLocal class.     */    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;    //......其余源码

Thread中的threadLocals变量指向的是一个map,这个map就是ThreadLocal.ThreadLocalMap,里面存放的是跟当前线程绑定的ThreadLocal变量;inheritableThreadLocals的作用相同,里面也是存放的ThreadLocal变量,但是存放的是从当前线程的父线程继承过来的ThreadLocal变量。

在看java.lang.ThreadLocal类,主要的成员和接口如下:

image.png

  1. withInitial方法,Java 8以后用于初始化ThreadLocal的一种方法,在外部调用get()方法的时候,会通过Supplier确定变量的初始值;

    public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {    return new SuppliedThreadLocal<>(supplier);}
  2. get方法,获取当前线程的变量副本,假如当前线程还没有创立该变量的副本,则需要通过调用initialValue方法来设置初始值;get方法的源代码如下,首先通过当前线程获取当前线程对应的map,假如map不为空,则从map中取出对应的Entry,而后取出对应的值;假如map为空,则调用setInitialValue设置初始值;假如map不为空,当前ThreadLocal实例对应的Entry为空,则也需要设置初始值。

    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();}
  3. set方法,跟get方法一样,先获取当前线程对应的map,假如map为空,则调用createMap创立map,否则将变量的值放入map——key为当前这个ThreadLocal对象,value为变量的值。

    public void set(T value) {    Thread t = Thread.currentThread();    ThreadLocalMap map = getMap(t);    if (map != null)        map.set(this, value);    else        createMap(t, value);}
  4. remove方法,删除当前线程绑定的这个副本

         public void remove() {         ThreadLocalMap m = getMap(Thread.currentThread());         if (m != null)             m.remove(this);     }
  5. 数字0x61c88647,这个值是HASH_INCREMENT的值,普通的hashmap是使用链表来解决冲突的,但是ThreadLocalMap是使用线性探测法来解决冲突的,HASH_INCREMENT就是每次添加的步长,根据参考资料1所说,选择这个数字是为了让冲突概率最小。

    /**     * The difference between successively generated hash codes - turns     * implicit sequential thread-local IDs into near-optimally spread     * multiplicative hash values for power-of-two-sized tables.     */    private static final int HASH_INCREMENT = 0x61c88647;

父子进程数据共享

InheritableThreadLocal主要用于子线程创立时,需要自动继承父线程的ThreadLocal变量,实现子线程访问父线程的threadlocal变量。InheritableThreadLocal继承了ThreadLocal,并重写了childValue、getMap、createMap三个方法。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {    /**     * 创立线程的时候,假如需要继承且父线程中Thread-Local变量,则需要将父线程中的ThreadLocal变量一次拷贝过来。     */    protected T childValue(T parentValue) {        return parentValue;    }    /**    * 因为重写了getMap,所以在操作InheritableThreadLocal变量的时候,将只操作Thread类中的inheritableThreadLocals变量,与threadLocals变量没有关系    **/    ThreadLocalMap getMap(Thread t) {       return t.inheritableThreadLocals;    }    /**     * 跟getMap相似,set或者getInheritableThreadLocal变量的时候,将只操作Thread类中的inheritableThreadLocals变量     */    void createMap(Thread t, T firstValue) {        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);    }}

关于childValue多说两句,拷贝是如何发生的?
首先看Thread.init方法,

    private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) {        //其余源码        if (inheritThreadLocals && parent.inheritableThreadLocals != null)            this.inheritableThreadLocals =                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);        /* Stash the specified stack size in case the VM cares */        this.stackSize = stackSize;        /* Set thread ID */        tid = nextThreadID();    }

而后看ThreadLocal.createInheritedMap方法,最终会调用到newThreadLocalMap方法,这里InheritableThreadLocal对childValue做了重写,可以看出,这里的确是将父线程关联的ThreadLocalMap中的内容依次拷贝到子线程的ThreadLocalMap中了。

       private ThreadLocalMap(ThreadLocalMap parentMap) {            Entry[] parentTable = parentMap.table;            int len = parentTable.length;            setThreshold(len);            table = new Entry[len];            for (int j = 0; j < len; j++) {                Entry e = parentTable[j];                if (e != null) {                    @SuppressWarnings("unchecked")                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();                    if (key != null) {                        Object value = key.childValue(e.value);                        Entry c = new Entry(key, value);                        int h = key.threadLocalHashCode & (len - 1);                        while (table[h] != null)                            h = nextIndex(h, len);                        table[h] = c;                        size++;                    }                }            }        }

ThreadLocal对象何时被回收?

ThreadLocalMap中的key是ThreadLocal对象,而后ThreadLocal对象时被WeakReference包装的,这样当没有强引用指向该ThreadLocal对象之后,或者者说Map中的ThreadLocal对象被判定为弱引用可达时,就会在垃圾收集中被回收掉。看下Entry的定义:

 static class Entry extends WeakReference<ThreadLocal<?>> {     /** The value associated with this ThreadLocal. */     Object value;     Entry(ThreadLocal<?> k, Object v) {         super(k);         value = v;     }}

ThreadLocal和线程池一起使用?

ThreadLocal对象的生命周期跟线程的生命周期一样长,那么假如将ThreadLocal对象和线程池一起使用,即可能会遇到这种情况:一个线程的ThreadLocal对象会和其余线程的ThreadLocal对象串掉,一般不建议将两者一起使用。

案例学习

Dubbo中对ThreadLocal的使用

我从Dubbo中找到了ThreadLocal的例子,它主要是用在请求缓存的场景,具体代码如下:

@Activate(group = {Constants.CONSUMER, Constants.PROVIDER}, value = Constants.CACHE_KEY)public class CacheFilter implements Filter {    private CacheFactory cacheFactory;    public void setCacheFactory(CacheFactory cacheFactory) {        this.cacheFactory = cacheFactory;    }    @Override    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {        if (cacheFactory != null && ConfigUtils.isNotEmpty(invoker.getUrl().getMethodParameter(invocation.getMethodName(), Constants.CACHE_KEY))) {            Cache cache = cacheFactory.getCache(invoker.getUrl(), invocation);            if (cache != null) {                String key = StringUtils.toArgumentString(invocation.getArguments());                Object value = cache.get(key);                if (value != null) {                    if (value instanceof ValueWrapper) {                        return new RpcResult(((ValueWrapper)value).get());                    } else {                        return new RpcResult(value);                    }                }                Result result = invoker.invoke(invocation);                if (!result.hasException()) {                    cache.put(key, new ValueWrapper(result.getValue()));                }                return result;            }        }        return invoker.invoke(invocation);    }

可以看出,在RPC调用(invoke)的链路上,会先使用请求参数判断当前线程能否刚刚发起过同样参数的调用——这个调用会使用ThreadLocalCache保存起来。具体的看,ThreadLocalCache的实现如下:

package org.apache.dubbo.cache.support.threadlocal;import org.apache.dubbo.cache.Cache;import org.apache.dubbo.common.URL;import java.util.HashMap;import java.util.Map;/** * ThreadLocalCache */public class ThreadLocalCache implements Cache {    //ThreadLocal里存放的是参数到结果的映射    private final ThreadLocal<Map<Object, Object>> store;    public ThreadLocalCache(URL url) {        this.store = new ThreadLocal<Map<Object, Object>>() {            @Override            protected Map<Object, Object> initialValue() {                return new HashMap<Object, Object>();            }        };    }    @Override    public void put(Object key, Object value) {        store.get().put(key, value);    }    @Override    public Object get(Object key) {        return store.get().get(key);    }}

RocketMQ

在RocketMQ中,我也找到了ThreadLocal的身影,它是用在消息发送的场景,MQClientAPIImpl是RMQ中负责将消息发送到服务端的实现,其中有一个步骤需要选择一个具体的队列,选择具体的队列的时候,不同的线程有自己负责的index值,这里使用了ThreadLocal的机制,可以看下ThreadLocalIndex的实现:

package org.apache.rocketmq.client.common;import java.util.Random;public class ThreadLocalIndex {    private final ThreadLocal<Integer> threadLocalIndex = new ThreadLocal<Integer>();    private final Random random = new Random();    public int getAndIncrement() {        Integer index = this.threadLocalIndex.get();        if (null == index) {            index = Math.abs(random.nextInt());            if (index < 0)                index = 0;            this.threadLocalIndex.set(index);        }        index = Math.abs(index + 1);        if (index < 0)            index = 0;        this.threadLocalIndex.set(index);        return index;    }    @Override    public String toString() {        return "ThreadLocalIndex{" +            "threadLocalIndex=" + threadLocalIndex.get() +            '}';    }}

总结

这篇文章主要是处理了关于ThreadLocal的几个问题:(1)具体的概念是啥?(2)在Java开发中的什么场景下使用?(3)ThreadLocal的实现原理是怎么的?(4)开源项目中有哪些案例可以参考?不知道你能否对这几个问题有了肯定的理解呢?假如还有疑问,欢迎交流。

参考资料

  1. Why 0x61c88647?
  2. Java ThreadLocal
  3. When and how should I use a ThreadLocal variable?
  4. 技术小黑屋:了解Java中的ThreadLocal
  5. 深入分析ThreadLocal的内存泄漏问题
  6. 《Java并发编程实战》
  7. InheritableThreadLocal详解
  8. ThreadLocal详解
  9. ThreadLocal的使用场景
  10. 数据结构:哈希表
  • 全部评论(0)
最新发布的资讯信息
【系统环境|】2FA验证器 验证码如何登录(2024-04-01 20:18)
【系统环境|】怎么做才能建设好外贸网站?(2023-12-20 10:05)
【系统环境|数据库】 潮玩宇宙游戏道具收集方法(2023-12-12 16:13)
【系统环境|】遥遥领先!青否数字人直播系统5.0发布,支持真人接管实时驱动!(2023-10-12 17:31)
【系统环境|服务器应用】克隆自己的数字人形象需要几步?(2023-09-20 17:13)
【系统环境|】Tiktok登录教程(2023-02-13 14:17)
【系统环境|】ZORRO佐罗软件安装教程及一键新机使用方法详细简介(2023-02-10 21:56)
【系统环境|】阿里云 centos 云盘扩容命令(2023-01-10 16:35)
【系统环境|】补单系统搭建补单源码搭建(2022-05-18 11:35)
【系统环境|服务器应用】高端显卡再度登上热搜,竟然是因为“断崖式”的降价(2022-04-12 19:47)
手机二维码手机访问领取大礼包
返回顶部