一针见血理解ThreadLocal类

ThreadLocal类具有两个维度:线程维度和变量维度。扔掉线程维度,保留并放大变量维度,虽然思想片面,但是给人的印象却是极深,才能用之出神入化。

ThreadLocal类是修饰变量的,重点是在控制变量的作用域,初衷可不是为了解决线程并发和线程冲突的,而是为了让变量的种类变的更多更丰富,方便人们使用罢了。很多开发语言在语言级别都提供这种作用域的变量类型。

根据变量的作用域,可以将变量分为全局变量,局部变量。简单的说,类里面定义的变量是全局变量,函数里面定义的变量是局部变量。

还有一种作用域是线程作用域,线程一般是跨越几个函数的。为了在几个函数之间共用一个变量,所以才出现:线程变量,这种变量在Java中就是ThreadLocal变量。

全局变量,范围很大;局部变量,范围很小。无论是大还是小,其实都是定死的。而线程变量,调用几个函数,则决定了它的作用域有多大。

ThreadLocal是跨函数的,虽然全局变量也是跨函数的,但是跨所有的函数,而且不是动态的。

ThreadLocal是跨函数的,但是跨哪些函数呢,由线程来定,更灵活。

一针见血理解ThreadLocal类

问题

  1. 和Synchronized的区别
  2. 存储在jvm的哪个区域
  3. 真的只是当前线程可见吗
  4. 会导致内存泄漏么
  5. 为什么用Entry数组而不是Entry对象
  6. 你学习的开源框架哪些用到了ThreadLocal
  7. ThreadLocal里的对象一定是线程安全的吗
  8. 笔试题

概述

官方术语

ThreadLocal类是用来提供线程内部的局部变量。让这些变量在多线程环境下访问(get/set)时能保证各个线程里的变量相对独立于其他线程内的变量。

大白话

ThreadLocal是一个关于创建线程局部变量的类。

通常情况下,我们创建的成员变量都是线程不安全的。因为他可能被多个线程同时修改,此变量对于多个线程之间彼此并不独立,是共享变量。而使用ThreadLocal创建的变量只能被当前线程访问,其他线程无法访问和修改。也就是说:将线程公有化变成线程私有化。

应用场景

  • 每个线程都需要一个独享的对象(比如工具类,典型的就是SimpleDateFormat,每次使用都new一个多浪费性能呀,直接放到成员变量里又是线程不安全,所以把他用ThreadLocal管理起来就完美了。)
/**
 * Description: SimpleDateFormat就一份,不浪费资源。
 *
 * @author TongWei.Chen 2020-07-10 14:00:29
 */
public class ThreadLocalTest05 {

    public static String dateToStr(int millisSeconds) {
        Date date = new Date(millisSeconds);
        SimpleDateFormat simpleDateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
        return simpleDateFormat.format(date);
    }

    private static final ExecutorService executorService = Executors.newFixedThreadPool(100);

    public static void main(String[] args) {
        for (int i = 0; i < 3000; i++) {
            int j = i;
            executorService.execute(() -> {
                String date = dateToStr(j * 1000);
                // 从结果中可以看出是线程安全的,时间没有重复的。
                System.out.println(date);
            });
        }
        executorService.shutdown();
    }
}

class ThreadSafeFormatter {
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        }
    };

    // java8的写法
//    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =
//            ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));
}

细心的朋友已经发现了,这也是每个线程都创建一个SimpleDateFormat,跟直接在方法内部new没区别,错了,大错特错!1个请求进来是一个线程,他可能贯穿了N个方法,你这N个方法假设有3个都在使用dateToStr(),你直接new的话会产生三个SimpleDateFormat对象,而用ThreadLocal的话只会产生一个对象,一个线程一个。

  • 每个线程内需要保存全局变量(比如在登录成功后将用户信息存到ThreadLocal里,然后当前线程操作的业务逻辑直接get取就完事了,有效的避免的参数来回传递的麻烦之处),一定层级上减少代码耦合度。

核心知识

类关系

每个Thread对象中都持有一个ThreadLocalMap的成员变量。每个ThreadLocalMap内部又维护了N个Entry节点,也就是Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型值。

核心源码如下

// java.lang.Thread类里持有ThreadLocalMap的引用
public class Thread implements Runnable {
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

// java.lang.ThreadLocal有内部静态类ThreadLocalMap
public class ThreadLocal<T{
    static class ThreadLocalMap {
        private Entry[] table;
        
        // ThreadLocalMap内部有Entry类,Entry的key是ThreadLocal本身,value是泛型值
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
    }
}

类关系图– ThreadLocal内存结构图。

主要方法

  • initialValue:初始化。在get方法里懒加载的。
  • get:得到这个线程对应的value。如果调用get之前没set过,则get内部会执行initialValue方法进行初始化。
  • set:为这个线程设置一个新值。
  • remove:删除这个线程对应的值,防止内存泄露的最佳手段。

initialValue

什么意思

见名知意,初始化一些value(泛型值)。懒加载的。

触发时机

调用get方法之前没有调用set方法,则get方法内部会触发initialValue,也就是说get的时候如果没拿到东西,则会触发initialValue

补充说明

  • 通常,每个线程最多调用一次此方法。但是如果已经调用了remove(),然后再次调用get()的话,则可以再次触发initialValue
  • 如果要重写的话一般建议采取匿名内部类的方式重写此方法,否则默认返回的是null。

比如:

public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal() {
    @Override
    protected SimpleDateFormat initialValue() {
        return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
    }
};
// Java8的高逼格写法
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =
            ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));

源码

// 由子类提供实现。
// protected的含义就是交给子类干的。
protected T initialValue() {
    return null;
}

get

什么意思

获取当前线程下的ThreadLocal中的值。

源码

/**
 * 获取当前线程下的entry里的value值。
 * 先获取当前线程下的ThreadLocalMap,
 * 然后以当前ThreadLocal为key取出map中的value
 */
public T get() {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取当前线程对应的ThreadLocalMap对象。
    ThreadLocalMap map = getMap(t);
    // 若获取到了。则获取此ThreadLocalMap下的entry对象,若entry也获取到了,那么直接获取entry对应的value返回即可。
    if (map != null) {
        // 获取此ThreadLocalMap下的entry对象
        ThreadLocalMap.Entry e = map.getEntry(this);
        // 若entry也获取到了
        if (e != null) {
            @SuppressWarnings("unchecked")
            // 直接获取entry对应的value返回。
            T result = (T)e.value;
            return result;
        }
    }
    // 若没获取到ThreadLocalMap或没获取到Entry,则设置初始值。
    // 知识点:我早就说了,初始值方法是延迟加载,只有在get才会用到,这下看到了吧,只有在这获取没获取到才会初始化,下次就肯定有值了,所以只会执行一次!!!
    return setInitialValue();
}

getEntry

private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}

通过threadLocalHashCode来获取table[]中存放的Entry对象, 也就是ThreadLocal对象

set

什么意思

其实干的事和initialValue是一样的,都是set值,只是调用时机不同。set是想用就用,api摆在这里,你想用就调一下set方法。很自由。

源码

/**
 * 设置当前线程的线程局部变量的值
 * 实际上ThreadLocal的值是放入了当前线程的一个ThreadLocalMap实例中,所以只能在本线程中访问。
 */
public void set(T value) {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取当前线程对应的ThreadLocalMap实例,注意这里是将t传进去了,t是当前线程,就是说ThreadLocalMap是在线程里持有的引用。
    ThreadLocalMap map = getMap(t);
    // 若当前线程有对应的ThreadLocalMap实例,则将当前ThreadLocal对象作为key,value做为值存到ThreadLocalMap的entry里。
    if (map != null)
        map.set(this, value);
    else
        // 若当前线程没有对应的ThreadLocalMap实例,则创建ThreadLocalMap,并将此线程与之绑定
        createMap(t, value);
}

remove

什么意思

将当前线程下的ThreadLocal的值删除,目的是为了减少内存占用。主要目的是防止内存泄漏。内存泄漏问题下面会说。

源码

/**
 * 将当前线程局部变量的值删除,目的是为了减少内存占用。主要目的是防止内存泄漏。内存泄漏问题下面会说。
 */
public void remove() {
    // 获取当前线程的ThreadLocalMap对象,并将其移除。
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        // 直接移除以当前ThreadLocal为key的value
        m.remove(this);
}

ThreadLocalMap

为啥单独拿出来说下,我就是想强调一点:这个东西是归Thread类所有的。它的引用在Thread类里,这也证实了一个问题:ThreadLocalMap类内部为什么有Entry数组,而不是Entry对象?

因为你业务代码能new好多个ThreadLocal对象,各司其职。但是在一次请求里,也就是一个线程里,ThreadLocalMap是同一个,而不是多个,不管你new几次ThreadLocalThreadLocalMap在一个线程里就一个,因为再说一次,ThreadLocalMap的引用是在Thread里的,所以它里面的Entry数组存放的是一个线程里你new出来的多个ThreadLocal对象。

核心源码如下:

// 在你调用ThreadLocal.get()方法的时候就会调用这个方法,它的返回是当前线程里的threadLocals的引用。
// 这个引用指向的是ThreadLocal里的ThreadLocalMap对象
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

public class Thread implements Runnable {
    // ThreadLocal.ThreadLocalMap
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

完整源码

核心源码

// 本地线程。Thread:线程。Local:本地
public class ThreadLocal<T{

    // 构造器
 public ThreadLocal() {}

    // 初始值,用来初始化值用的,比如:ThreadLocal<Integer> count = new ThreadLocal<>();
    // 你想Integer value = count.get(); value++;这样是报错的,因为count现在还没值,取出来的是个null,所以你需要先重写此方法为value赋上初始值,本身方法是protected也代表就是为了子类重写的。
    // 此方法是一个延迟调用方法,在线程第一次调用get的时候才执行,下面具体分析源码就知道了。
 protected T initialValue() {}

    // 创建ThreadLocalMap,ThreadLocal底层其实就是一个map来维护的。
 void createMap(Thread t, T firstValue) {}

    // 返回该当前线程对应的线程局部变量值。
 public T get() {}

    // 获取ThreadLocalMap
 ThreadLocalMap getMap(Thread t) {}

    // 设置当前线程的线程局部变量的值
 public void set(T value) {}

    // 将当前线程局部变量的值删除,目的是为了减少内存占用。其实当线程结束后对应该线程的局部变量将自动被垃圾回收,所以无需我们调用remove,我们调用remove无非也就是加快内存回收速度。
 public void remove() {}

    // 设置初始值,调用initialValue
 private T setInitialValue() {}

    // 静态内部类,一个map来维护的!!!
 static class ThreadLocalMap {
  
        // ThreadLocalMap的静态内部类,继承了弱引用,这正是不会造成内存泄漏根本原因
        // Entry的key为ThreadLocal并且是弱引用。value是值
  static class Entry extends WeakReference<ThreadLocal<?>> {}
 }

}

set()

/**
 * 设置当前线程的线程局部变量的值
 * 实际上ThreadLocal的值是放入了当前线程的一个ThreadLocalMap实例中,所以只能在本线程中访问。
 */
public void set(T value) {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取当前线程对应的ThreadLocalMap实例
    ThreadLocalMap map = getMap(t);
    // 若当前线程有对应的ThreadLocalMap实例,则将当前ThreadLocal对象作为key,value做为值存到ThreadLocalMap的entry里。
    if (map != null)
        map.set(this, value);
    else
        // 若当前线程没有对应的ThreadLocalMap实例,则创建ThreadLocalMap,并将此线程与之绑定
        createMap(t, value);
}

getMap()

// 在你调用ThreadLocal.get()方法的时候就会调用这个方法,它的返回是当前线程里的threadLocals的引用。
// 这个引用指向的是ThreadLocal里的ThreadLocalMap对象
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

public class Thread implements Runnable {
    // ThreadLocal.ThreadLocalMap
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

map.set()

// 不多BB,就和HashMap的set一个道理,只是赋值key,value。
// 需要注意的是这里key是ThreadLocal对象,value是值
private void set(ThreadLocal<?> key, Object value) {}

createMap()

/**
 * 创建ThreadLocalMap对象。
 * t.threadLocals在上面的getMap中详细介绍了。此处不BB。
 * 实例化ThreadLocalMap并且传入两个值,一个是当前ThreadLocal对象一个是value。
 */
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

// ThreadLocalMap构造器。
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    // 重点看这里!!!!!!
    // new了一个ThreadLocalMap的内部类Entry,且将key和value传入。
    // key是ThreadLocal对象。
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

/**
 * 到这里朋友们应该真相大白了,其实ThreadLocal就是内部维护一个ThreadLocalMap,
 * 而ThreadLocalMap内部又维护了一个Entry对象。Entry对象是key-value形式,
 * key是ThreadLocal对象,value是传入的value
 * 所以我们对ThreadLocal的操作其实都是对内部的ThreadLocalMap.Entry的操作
 * 所以保证了线程之前互不干扰。
 */

get()

/**
 * 获取当前线程下的entry里的value值。
 * 先获取当前线程下的ThreadLocalMap,
 * 然后以当前ThreadLocal为key取出map中的value
 */
public T get() {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取当前线程对应的ThreadLocalMap对象。
    ThreadLocalMap map = getMap(t);
    // 若获取到了。则获取此ThreadLocalMap下的entry对象,若entry也获取到了,那么直接获取entry对应的value返回即可。
    if (map != null) {
        // 获取此ThreadLocalMap下的entry对象
        ThreadLocalMap.Entry e = map.getEntry(this);
        // 若entry也获取到了
        if (e != null) {
            @SuppressWarnings("unchecked")
            // 直接获取entry对应的value返回。
            T result = (T)e.value;
            return result;
        }
    }
    // 若没获取到ThreadLocalMap或没获取到Entry,则设置初始值。
    // 知识点:我早就说了,初始值方法是延迟加载,只有在get才会用到,这下看到了吧,只有在这获取没获取到才会初始化,下次就肯定有值了,所以只会执行一次!!!
    return setInitialValue();
}

setInitialValue()

// 设置初始值
private T setInitialValue() {
    // 调用初始值方法,由子类提供。
    T value = initialValue();
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取map
    ThreadLocalMap map = getMap(t);
    // 获取到了
    if (map != null)
        // set
        map.set(this, value);
    else
        // 没获取到。创建map并赋值
        createMap(t, value);
    // 返回初始值。
    return value;
}

initialValue()

// 由子类提供实现。
// protected
protected T initialValue() {
    return null;
}

remove()

/**
 * 将当前线程局部变量的值删除,目的是为了减少内存占用。
 * 其实当线程结束后对应该线程的局部变量将自动被垃圾回收,所以无需我们调用remove,我们调用remove无非也就是加快内存回收速度。
 */
public void remove() {
    // 获取当前线程的ThreadLocalMap对象,并将其移除。
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

小结

只要捋清楚如下几个类的关系,ThreadLocal将变得so easy!

Thread`、`ThreadLocal`、`ThreadLocalMap`、`Entry

一句话总结就是:Thread维护了ThreadLocalMap,而ThreadLocalMap里维护了Entry,而Entry里存的是以ThreadLocal为key,传入的值为value的键值对。

答疑(面试题)

如何实现一个线程多个ThreadLocal对象,每一个ThreadLocal对象是如何区分的呢?

private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}

对于每一个ThreadLocal对象,都有一个final修饰的int型的threadLocalHashCode不可变属性,对于基本数据类型,可以认为它在初始化后就不可以进行修改,所以可以唯一确定一个ThreadLocal对象。
  但是如何保证两个同时实例化的ThreadLocal对象有不同的threadLocalHashCode属性:在ThreadLocal类中,还包含了一个static修饰的AtomicInteger([əˈtɒmɪk]提供原子操作的Integer类)成员变量(即类变量)和一个static final修饰的常量(作为两个相邻nextHashCode的差值)。由于nextHashCode是类变量,所以每一次调用ThreadLocal类都可以保证nextHashCode被更新到新的值,并且下一次调用ThreadLocal类这个被更新的值仍然可用,同时AtomicInteger保证了nextHashCode自增的原子性。

为什么不直接用线程id来作为ThreadLocalMap的key?

  这一点很容易理解,因为直接用线程id来作为ThreadLocalMap的key,无法区分放入ThreadLocalMap中的多个value。比如我们放入了两个字符串,你如何知道我要取出来的是哪一个字符串呢?
  而使用ThreadLocal作为key就不一样了,由于每一个ThreadLocal对象都可以由threadLocalHashCode属性唯一区分或者说每一个ThreadLocal对象都可以由这个对象的名字唯一区分,所以可以用不同的ThreadLocal作为key,区分不同的value,方便存取。

和Synchronized的区别

问:他和线程同步机制(如:Synchronized)提供一样的功能,这个很吊啊。

答:放屁!同步机制保证的是多线程同时操作共享变量并且能正确的输出结果。ThreadLocal不行啊,他把共享变量变成线程私有了,每个线程都有独立的一个变量。举个通俗易懂的案例:网站计数器,你给变量count++的时候带上synchronized即可解决。ThreadLocal的话做不到啊,他没发统计,他只能说能统计每个线程登录了多少次。

存储在jvm的哪个区域

问:线程私有,那么就是说ThreadLocal的实例和他的值是放到栈上咯?

答:不是。还是在堆的。ThreadLocal对象也是对象,对象就在堆。只是JVM通过一些技巧将其可见性变成了线程可见。

真的只是当前线程可见吗

问:真的只是当前线程可见吗?

答:貌似不是,貌似通过InheritableThreadLocal类可以实现多个线程访问ThreadLocal的值,但是我没研究过,知道这码事就行了。

会导致内存泄漏么

问:会导致内存泄漏么?

答:分析一下:

  • 1、ThreadLocalMap.Entry的key会内存泄漏吗?
  • 2、ThreadLocalMap.Entry的value会内存泄漏吗?

先看下key-value的核心源码

static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

先看继承关系,发现是继承了弱引用,而且key直接是交给了父类处理super(key),父类是个弱引用,所以key完全不存在内存泄漏问题,因为他不是强引用,它可以被GC回收的。

弱引用的特点:如果这个对象只被弱引用关联,没有任何强引用关联,那么这个对象就可以被GC回收掉。弱引用不会阻止GC回收。这是jvm知识。

看value,发现value是个强引用,但是想了下也没问题的呀,因为线程终止了,我管你强引用还是弱引用,都会被GC掉的,因为引用链断了(jvm用的可达性分析法,线程终止了,根节点就断了,下面的都会被回收)。

这么分析一点毛病都没有,但是忘了一个主要的角色,那就是线程池,线程池的存在核心线程是不会销毁的,只要创建出来他会反复利用,生命周期不会结束掉,但是key是弱引用会被GC回收掉,value强引用不会回收,所以形成了如下场面:

Thread->ThreadLocalMap->Entry(key为null)->value

由于value和Thread还存在链路关系,还是可达的,所以不会被回收,这样越来越多的垃圾对象产生却无法回收,早晨内存泄漏,时间久了必定OOM。

解决方案ThreadLocal已经为我们想好了,提供了remove()方法,这个方法是将value移出去的。所以用完后记得remove()

为什么用Entry数组而不是Entry对象

这个其实主要想考ThreadLocalMap是在Thread里持有的引用。

ThreadLocalMap内部的table为什么是数组而不是单个对象呢?

答:因为你业务代码能new好多个ThreadLocal对象,各司其职。但是在一次请求里,也就是一个线程里,ThreadLocalMap是同一个,而不是多个,不管你new几次ThreadLocalThreadLocalMap在一个线程里就一个,因为ThreadLocalMap的引用是在Thread里的,所以它里面的Entry数组存放的是一个线程里你new出来的多个ThreadLocal对象。

你学习的开源框架哪些用到了ThreadLocal

Spring框架。

DateTimeContextHolder
RequestContextHolder

ThreadLocal里的对象一定是线程安全的吗

未必,如果在每个线程中ThreadLocal.set()进去的东西本来就是多线程共享的同一个对象,比如static对象,那么多个线程的ThreadLocal.get()获取的还是这个共享对象本身,还是有并发访问线程不安全问题。

笔试题

问:下面这段程序会输出什么?为什么?

public class TestThreadLocalNpe {
    private static ThreadLocal<Long> threadLocal = new ThreadLocal();

    public static void set() {
        threadLocal.set(1L);
    }

    public static long get() {
        return threadLocal.get();
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            set();
            System.out.println(get());
        }).start();
        // 目的就是为了让子线程先运行完
        Thread.sleep(100);
        System.out.println(get());
    }
}

答:

1
Exception in thread "main" java.lang.NullPointerException
 at com.chentongwei.study.thread.TestThreadLocalNpe.get(TestThreadLocalNpe.java:16)
 at com.chentongwei.study.thread.TestThreadLocalNpe.main(TestThreadLocalNpe.java:26)

为什么?

为什么输出个1,然后空指针了?

首先输出1是没任何问题的,其次主线程空指针是为什么?

如果你这里回答

1
1

那我恭喜你,你连ThreadLocal都不知道是啥,这明显两个线程,子线程和主线程。子线程设置1,主线程肯定拿不到啊,ThreadLocal和线程是嘻嘻相关的。这个不多费口舌。

说说为什么是空指针?

因为你get方法用的long而不是Long,那也应该返回null啊,大哥,long是基本类型,默认值是0,没有null这一说法。ThreadLocal里的泛型是Long,get却是基本类型,这需要拆箱操作的,也就是会执行null.longValue()的操作,这绝逼空指针了。

看似一道Javase的基础题目,实则隐藏了很多知识。

ThreadLocal工具类

package com.duoku.base.util;

import com.google.common.collect.Maps;
import org.springframework.core.NamedThreadLocal;

import java.util.Map;

/**
 * Description:
 *
 * @author TongWei.Chen 2019-09-09 18:35:30
 */
public class ThreadLocalUtil {

    private static final ThreadLocal<Map<String, Object>> threadLocal = new NamedThreadLocal("xxx-threadlocal") {
        @Override
        protected Map<String, Object> initialValue() {
            return Maps.newHashMap();
        }
    };

    public static Map<String, Object> getThreadLocal(){
        return threadLocal.get();
    }
    
    public static <T> get(String key) {
        Map map = threadLocal.get();
        // todo:copy a new one
        return (T)map.get(key);
    }

    public static <T> get(String key,T defaultValue) {
        Map map = threadLocal.get();
        return (T)map.get(key) == null ? defaultValue : (T)map.get(key);
    }

    public static void set(String key, Object value) {
        Map map = threadLocal.get();
        map.put(key, value);
    }

    public static void set(Map<String, Object> keyValueMap) {
        Map map = threadLocal.get();
        map.putAll(keyValueMap);
    }

    public static void remove() {
        threadLocal.remove();
    }

}

ThreadLocal的内存泄露问题

ThreadLocalMap是使用ThreadLocal的弱引用作为Key的。下图是本文介绍到的一些对象之间的引用关系图,实线表示强引用,虚线表示弱引用:

如上图,ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永远无法回收,造成内存泄露。
  
  ThreadLocalMap设计时的对上面问题的对策:
ThreadLocalMap的getEntry函数的流程大概为:

  1. 首先从ThreadLocal的直接索引位置(通过ThreadLocal.threadLocalHashCode & (table.length-1)运算得到)获取Entry e,如果e不为null并且key相同则返回e;
  2. 如果e为null或者key不一致则向下一个位置查询,如果下一个位置的key和当前需要查询的key相等,则返回对应的Entry。否则,如果key值为null,则擦除该位置的Entry,并继续向下一个位置查询。在这个过程中遇到的key为null的Entry都会被擦除,那么Entry内的value也就没有强引用链,自然会被回收。仔细研究代码可以发现,set操作也有类似的思想,将key为null的这些Entry都删除,防止内存泄露。
      但是光这样还是不够的,上面的设计思路依赖一个前提条件:要调用ThreadLocalMap的getEntry函数或者set函数。这当然是不可能任何情况都成立的,所以很多情况下需要使用者手动调用ThreadLocal的remove函数,手动删除不再需要的ThreadLocal,防止内存泄露。所以JDK建议将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露。

即:
1.使用ThreadLocal,建议用static修饰 static ThreadLocal headerLocal = new ThreadLocal();
2.使用完ThreadLocal后,执行remove操作,避免出现内存溢出情况。

Java中的四种引用类型(强、软、弱、虚)

从Java 1.2开始,JVM开发团队发现,单一的强引用类型,无法很好的管理对象在JVM里面的生命周期,垃圾回收策略过于简单,无法适用绝大多数场景。为了更好的管理对象的内存,更好的进行垃圾回收,JVM团队扩展了引用类型,从最早的强引用类型增加到强、软、弱、虚四个引用类型。

Strong Rerence为JVM内部实现。其他三类引用类型全部继承自Reference父类。如下图所示:

img

强引用(Strong Reference)

Strong Rerence这个类并不存在,默认的对象都是强引用类型,因为有后来的新引用所衬托,所以才起了个名字叫”强引用”。

强引用使用示例如下所示:

String web = "www.threadlocal.cn";

如果JVM垃圾回收器 GC 可达性分析结果为可达,表示引用类型仍然被引用着,这类对象始终不会被垃圾回收器回收,即使JVM发生OOM也不会回收。而如果 GC 的可达性分析结果为不可达,那么在GC时会被回收。

软引用(Soft Reference)

软引用是一种比强引用生命周期稍弱的一种引用类型。在JVM内存充足的情况下,软引用并不会被垃圾回收器回收,只有在JVM内存不足的情况下,才会被垃圾回收器回收。所以软引用一般用来实现一些内存敏感的缓存,只要内存空间足够,对象就会保持不被回收掉。

软引用使用示例如下所示:

SoftReference<String> softReference = new SoftReference<String>(new String("www.threadlocal.cn"));
String web = softReference.get();
弱引用(Weak Reference)

弱引用是一种比软引用生命周期更短的引用。它的生命周期很短,不论当前内存是否充足,都只能存活到下一次垃圾收集之前。

WeakReference<String> weakReference = new WeakReference<String>(new String("www.threadlocal.cn"));

System.gc();

if(weakReference.get() == null)
{
System.out.println("weakReference已经被GC回收");
}

输出结果:

weakReference已经被GC回收

虚引用(PhantomReference)

虚引用与前面的几种都不一样,这种引用类型不会影响对象的生命周期,所持有的引用就跟没持有一样,随时都能被GC回收。

需要注意的是,在使用虚引用时,必须和引用队列关联使用。在对象的垃圾回收过程中,如果GC发现一个对象还存在虚引用,则会把这个虚引用加入到与之关联的引用队列中。

程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。

如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象内存被回收之前采取必要的行动防止被回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。

PhantomReference<String> phantomReference = new PhantomReference<String>(new String("www.threadlocal.cn"), new ReferenceQueue<String>());

System.out.println(phantomReference.get());