源码修炼笔记之ThreadLocal详解

图文不相关系列


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

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  threadId = ThreadLocal.withInitial(() -> nextId.getAndIncrement());
    public static int get() {
        return threadId.get();
    }
}
//运行结果为
子线程Id为:0
主线程Id为:1

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


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

你可能感兴趣的:(源码修炼笔记之ThreadLocal详解)