ThreadLocal特性及使用场景

  1. 1、方便同一个线程使用某一对象,避免不必要的参数传递;
  2. 2、线程间数据隔离(每个线程在自己线程里使用自己的局部变量,各线程间的ThreadLocal对象互不影响);
  3. 3、获取数据库连接、Session、关联ID(比如日志的uniqueID,方便串起多个日志);
  4. ThreadLocal应注意:
  5. 1ThreadLocal并未解决多线程访问共享对象的问题;
  6. 2ThreadLocal并不是每个线程拷贝一个对象,而是直接new(新建)一个;
  7. 3、如果ThreadLocal.set()的对象是多线程共享的,那么还是涉及并发问题。


ThreadLocal示例:

public class Demo {
    //声明ThreadLocal并初始化赋值
    private static final ThreadLocal<Object> threadLocal = new ThreadLocal<Object>(){
        @Override
        protected Object initialValue()
        {
            Map<String,Object> map = new HashMap<>();
            map.put("a","a");
            map.put("b","b");
            map.put("c","c");
            return map;
        }
    };

    public static void main(String[] args){
        //线程1 - 对其获取并修改
        new Thread(() -> {
            Object stringObjectMap = threadLocal.get();
            System.out.println("什么1:" + stringObjectMap + " -> " + Thread.currentThread().getName());
            //赋值
            Map<String,Object> map = new HashMap<>();
            map.put("d","1");
            map.put("e","2");
            map.put("f","3");
            threadLocal.set(map);
            Object stringObjectMap2 = threadLocal.get();
            System.out.println("什么2:" + stringObjectMap2 + " -> " + Thread.currentThread().getName());
        }).start();

        //线程2 - 对其获取 - 为了检测,加了睡眠1秒的代码
        new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Object stringObjectMap = threadLocal.get();
            System.out.println("什么3:" + stringObjectMap + " -> " + Thread.currentThread().getName());
        }).start();
    }
}

//输出
什么1:{a=a, b=b, c=c} -> Thread-0 - 归属线程1
什么2:{d=1, e=2, f=3} -> Thread-0 - 归属线程1
什么3:{a=a, b=b, c=c} -> Thread-1 - 归属线程2

从结果可以看出,虽然两个线程共同使用一个ThreadLocal,但两个线程中所展示的ThreadLocal的数据值
并不会相互影响,也就是说这种情况下的threadLocal变量保存的数据相当于是线程安全的,只能被当前线程访问。


ThreadLocal源码

set()

public void set(T value) {
    //获取当前线程
    Thread t = Thread.currentThread();
    // 获取线程的ThreadLocalMap,返回map
    ThreadLocalMap map = getMap(t);
    if (map != null)
        //将数据放入ThreadLocalMap中,key是当前ThreadLocal对象,值是我们传入的value。
        map.set(this, value);
    else
        //map为空,初始化ThreadLocalMap,并以当前ThreadLocal对象为Key,value为值存入map中。
        createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

set的代码逻辑比较简单,主要是把值设置到当前线程的一个ThreadLocalMap对象中,而ThreadLocalMap可以理解成一个Map,它是定义在Thread类中内部的成员,初始化是为null。

//它在Thread类中
ThreadLocal.ThreadLocalMap threadLocals = null;

与常见的Map实现类,如HashMap之类的不同的是,
ThreadLocalMap中的Entry是继承于WeakReference类的,
保持了对 “键” 的弱引用和对 “值” 的强引用,
static class ThreadLocalMap {

    private Entry[] table;

    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);//调用弱引用构造
            value = v;
        }
    }

    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        table = new Entry[INITIAL_CAPACITY];
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
        setThreshold(INITIAL_CAPACITY);
    }
}

ThreadLocalMap是一种Map,其内部维护着一个Entry[]。
ThreadLocalMap其实是就是将Key和Value包装成Entry,然后放入Entry数组中。看一下它的set方法。
Entry构造函数中的参数 k 就是ThreadLocal实例,调用super(k) 表明对 k 是弱引用,
使用弱引用的原因在于,当没有强引用指向 ThreadLocal 实例时,它可被回收,从而避免内存泄露。
当调用set方法时,其实是将数据写入threadLocals这个Map对象中,
这个Map的key为ThreadLocal当前对象,value就是我们存入的值。
而threadLocals本身能保存多个ThreadLocal对象,相当于一个ThreadLocal集合。

private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    //通过ThreadLocal的threadLocalHashCode与当前Map的长度计算出数组下标 i
    int i = key.threadLocalHashCode & (len-1);

    //从i开始遍历Entry数组,这会有三种情况:
    for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
          //1、Entry的key就是我们要set的ThreadLocal,直接替换Entry中的value。
        if (k == key) {
          //如果已经存在,直接替换value
            e.value = value;
            return;
        }

        //2、Entry的key为空,直接替换key和value。
        if (k == null) {
            //如果当前位置的key ThreadLocal为空,替换key和value。
            //下文ThreadLocal内存分析中会提到为什么会有这段代码。
            replaceStaleEntry(key, value, i);
            return;
        }

        //3、发生了Hash冲突,当前位置已经有了数据,查找下一个可用空间。
    }

    //找到没有数据的位置,将key和value放入。
    tab[i] = new Entry(key, value);//该位置没有数据,直接存入。
    int sz = ++size;

    //检查是否扩容。
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}
应该可以看出ThreadLocalMap就是一种HashMap。
不过它并没有采用java.util.HashMap中数组+链表的方式解决Hash冲突,而是采用index后移的方式。
HashMap是一种get、set都非常高效的集合,它的时间复杂度只有O(1)。
但是如果存在严重的Hash冲突,那HashMap的效率就会降低很多。
我们通过上段代码知道,ThreadLocalMap是通过 key.threadLocalHashCode & (len-1)
计算Entry存放index的。len是当前Entry[]的长度,这没什么好说的。
那看来秘密就在threadLocalHashCode中了。我们来看一下threadLocalHashCode是如何产生的。
public class ThreadLocal<T> {

    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);
    }
}
这段代码非常简单。有个全局的计数器nextHashCode,
每有一个ThreadLocal产生这个计数器就会加0x61c88647,
然后把当前值赋给threadLocalHashCode。什么是0x61c88647?
16进制    0x61c88647
10进制    1640531527
2进制        01100001110010001000011001000111 10011110001101110111100110111001(取反+1)
10011110001101110111100110111001对应的10进制为2654435769;
在斐波那契散列法中2654435769是一个斐波那契散列乘数,
它的优点是通过它散列出来的结果分布会比较均匀,可以很大程度上避免hash冲突;

get()

public T get() {
    Thread t = Thread.currentThread();
    //直接获取当前线程的ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        //如果该对象不为空就返回它的value值
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    //否则为空,设置初识值到ThreadLocal中并返回
    return setInitialValue();
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

其实就是每个线程都维护着一个ThreadLocal的容器,这个容器就是ThreadLocalMap,可以保存
多个ThreadLocal对象。而调用ThreadLocal的set或get方法其实就是对当前线程的ThreadLocal变量操作,
与其他线程是分开的,所以才能保证线程私有,也就不存在线程安全的问题了。
然而,该方案虽然能保证线程私有,但却会占用大量的内存,因为每个线程都维护着一个Map,
当访问某个ThreadLocal变量后,线程会在自己的Map内维护该ThreadLocal变量与具体实现的映射,
如果这些映射一直存在,就表明ThreadLocal 存在引用的情况,那么系统GC就无法回收这些变量,
可能会造成内存泄露。

remove()

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}
//这就比较简单了,删除操作

ThreadLocal是怎么解决内存泄露的

重点就在ThreadLocal弱引用上,关于对象的引用查阅对象的那篇文章。
弱引用可以保证,只要GC到来,那么就必定回收它。
但是,仅仅是它的key是弱引用,value并不会回收。

ThreadLocal内存分析

image.png
我们假设ThreadLocal完成了自己的使命,与ThreadLocalRef断开了引用关系。此时内存图变成了这样。
image.png
系统GC发生时,由于Heap中的ThreadLocal只有来自key的弱引用,因此ThreadLocal内存会被回收到。
image.png

到这里,value被留在了Heap中,而我们没办法通过引用访问它。value这块内存将会持续到线程结束。
如果不想依赖线程的生命周期,那就调用remove方法来释放value的内存吧。
在面试中,使用完的话,还是要手动的写上remove方法。