美文网首页
源码修炼笔记之ThreadLocal详解

源码修炼笔记之ThreadLocal详解

作者: 花醉霜寒 | 来源:发表于2019-07-30 10:46 被阅读0次
    图文不相关系列

    \color{green}{吹牛逼}
    多线程线程安全的根源就是“共享”,即多个线程操作共享变量会引起可见性、原子性和顺序性的问题。解决线程安全首先我们想到的是加锁,比如我们熟知的Synchronized和ReentrantLock等,这里介绍另外一种解决共享问题的模式,线程本地储存模式,没有共享就没有伤害,每个线程都有自己的变量,彼此之间不共享,Java中实现这一思想的就是ThreadLocal类。

    \color{green}{ThreadLocal源码解析}

    ThreadLocal类图
    如果我们自己去实现一个ThreadLocal,相信大家都会想到用HashMap这种数据结构能够完成,很多了解ThreadLocal的人也说ThreadLocal其实就是一个HashMap,其实这种说法不是很准确,我们来看看JDK源码是如何实现ThreadLocal的,首先看一下ThreadLocal类的结构,ThreadLocalMap和SuppliedThreadLocal是两个内部类,ThreadLocalMap就是用来储存线程变量的,和HashMap类似其内部维护一个Entry数组,初始大小都是16,但是ThreadLocalMap不存在负载因子的说法,因为它的key计算如下所示,
    private static AtomicInteger nextHashCode = new AtomicInteger();
    private static int nextHashCode() {
            return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
    

    是原子加一操作,不会存在hash冲突问题,当Entry数组满了之后会按照乘以2的方式扩容。SuppliedThreadLocal应该是JDK1.8引入的,采用函数式的编程思想通过Supplier.get()给ThreadLocal赋值。下面着重分析一下ThreadLocal的几个重要方法:
    1)get()

    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;
                }
            }
            //此处value为null
            return setInitialValue();
    }
    private T setInitialValue() {
            //return null
            T value = initialValue();
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null)
                map.set(this, value);
            else
                createMap(t, value);
            return value;
    }
    

    get方法首先会获取当前线程,这里可以看到ThreadLocalMap不是ThreadLocal类持有的,而是Thread类持有的,这里不得不佩服JDK大佬们对现象对象程序设计理解之深入骨髓,线程局部变量是属于线程的,应该封装在Thread类中。获取map之后通过hash获取value,如果不存在就初始化ThreadLocalMap的value为空,并返回null。
    2)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);
    }
    void createMap(Thread t, T firstValue) {
            t.threadLocals = new ThreadLocalMap(this, firstValue);
     }
    

    其实也比较简单,首先获取当前线程的ThreadLocalMap对象,如果不为空则塞值,如果为空则根据value初始化ThreadLocalMap。

    \color{green}{ThreadLocal使用}

    public class ThreadLocalDemo {
        public static void main(String[] args) throws InterruptedException {
            Thread thread = new Thread(() -> {
                System.out.println("子线程Id为:" + printThreadId());
            });
            thread.start();
            thread.join();
            System.out.println("主线程Id为:" + printThreadId());
        }
        static int printThreadId() {
            return ThreadId.get();
        }
    }
    
    class ThreadId {
        private static final AtomicInteger nextId = new AtomicInteger(0);
        private static final ThreadLocal<Integer>  threadId = ThreadLocal.withInitial(() -> nextId.getAndIncrement());
        public static int get() {
            return threadId.get();
        }
    }
    //运行结果为
    子线程Id为:0
    主线程Id为:1
    

    上面展示了ThreadLocal的基本用法,实现每个线程分配一个线程ID。

    \color{green}{使用ThreadLocal踩过的坑}
    1)内存泄漏
    这个我并未遇到过,但是这好像是一个很重要的知识点(手动滑稽),内存泄漏的原因是,ThreadLocalMap是Thread维护的(前面还说JDK大佬的思想无敌,现在又说给我们使用留下的坑,再次手动滑稽,当然多半是我们使用不当),生命周期和Thread一样,ThreadLocalMap中的value无法被回收,因此应该手动remove。
    2)异步化带来的坑
    JDK1.8的CompletableFuture的引入使得异步编程更加简单,代码中各种开异步线程,有时候我们为了避免方法参数的链式传递,将基本的方法参数放在ThreadLocal中进行传递,比如一个办公系统中员工的工号和认证token,如果使用ThreadLocal,异步线程中就会出问题了,这时可以使用InheritableThreadLocal,支持子线程继承父线程的线程局部变量。当然我还遇到过更加奇葩的问题,通过消息中间件MQ,生产者和消费者部署在不同的服务上,在生产者端将基本参数放入ThreadLocal中,然后在消费端尝试获取这些参数导致出现问题。

    相关文章

      网友评论

          本文标题:源码修炼笔记之ThreadLocal详解

          本文链接:https://www.haomeiwen.com/subject/tbsgrctx.html