读书笔记-Java高并发程序设计(二)

193 阅读4分钟

ThreadLocal

从字面上就可以看出,ThreadLocal是属于线程的私有变量,只有当前线程才可以访问。其次,ThreadLocal主要是对线程Thread的ThreadLocalMap进行操作,以ThreadLocal为键值向其中保存数据。

用法

ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("test-value");
String str = threadLocal.get();
System.out.println(str);

实现原理

ThreadLocal的使用方法比较简单,主要是set()和get()方法,下面我们去ThreadLocal的源码中寻找答案。

set

public void set(T value) {
    // 获取当前线程引用
    Thread t = Thread.currentThread();
    // 获取当前线程的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);		// set值
    else
    	createMap(t, value);
}
/**
 * 通过当前线程获取map
 */
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

我们发现set操作很大程度上都与ThreadLocalMap有关,接下来我们需要了解ThreadLocalMap的实现原理,ThreadLocalMap是ThreadLocal中的一个静态内部类:

static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    
    private Entry[] table;		// Entry数组,用来存放键值对
  
    /**
     * 构造方法
     */
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        table = new Entry[INITIAL_CAPACITY];	//初始化Entry数组
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);	//通过key的hashcode计算在数组中的位置
        table[i] = new Entry(firstKey, firstValue);		//保存到数组中
        size = 1;
        setThreshold(INITIAL_CAPACITY);
     }
  
      /**
     * Set the value associated with key.
     */
    private void set(ThreadLocal<?> key, Object value) {
        Entry[] tab = table;
        int len = tab.length;
        int i = key.threadLocalHashCode & (len-1);
        // 因为Entry中没有next指针,所以不能用链表的方式解决key冲突,这里使用nextIndex(i, len)来解决键值的冲突,如果出现冲突,向后移固定长度再次检查key
        for (Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            ThreadLocal<?> k = e.get();
            // 找到key,重置数据
            if (k == key) {
                e.value = value;
                return;
            }
            // 如果key不存在,用新的Entry对象填充
            if (k == null) {
                replaceStaleEntry(key, value, i);
                return;
            }
        }
    
        tab[i] = new Entry(key, value);
        int sz = ++size;
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
    }
}

上面是ThreadLocalMap的一些细节,ThreadLocalMap类中又包含了实体类Entry,Entry中主要包含ThreadLocal类型的键值和Object类型的数据,而ThreadLocalMap中维护了一个数组:Entry[],用于保存键值对,但是Entry中没有HashMap中的next指针,所以无法使用链表的形式解决冲突,这里ThreadLocalMap通过轮询数组中的元素进行安插键值对。

get

public T get() {
    // 通过当前线程获取ThreadLocalMap
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    // 通过ThreadLocalMap和ThreadLocal对象获取数据
    if (map != null) {
    ThreadLocalMap.Entry e = map.getEntry(this);
    if (e != null) {
        @SuppressWarnings("unchecked")
        T result = (T)e.value;
        return result;
    }
    }
    return setInitialValue();
}

我们再看一下get()方法中获取Entry的map.getEntry(this)方法,ThreadLocalMap.getEntry(ThreadLocal<?> key):

private Entry getEntry(ThreadLocal<?> key) {
    // 根据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);		//key值发生冲突,在数组下面继续找
}

原理总结

由上诉分析我们知道,每个线程都维护了一个ThreadLocalMap对象,ThreadLocalMap中又维护了一个元素为Entry的数组,Entry为键值对的实体对象,其中key为ThreadLocal对象,值为Object类型数据。但是与HashMap不同的是数组中的元素没有next指针,不是通过链表来解决冲突的。所以我们也可以得知一个ThreadLocal对象可以类比HashMap中的一个Entry对象,她只能保存一个键值对,其中键值就是ThreadLocal对象本身。

内存泄漏

如果我们使用线程池维护线程,线程中创建了大对象到ThreadLocal中,但是在线程完成工作后线程未必会退出,而可能仍然维护在线程池中等待下一次调用,如果没有显式地使用ThreadLocal.remove()方法将其移除,那么ThreadLocal仍然可以保存下来,但是此时这个变量已经没有任何用途,就可能使系统发生内存泄漏。

然后我们考虑另外一种情况:因为ThreadLocalMap.Entry中的key是“弱引用”,即

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

Entry中的key是外部ThreadLocal对象的弱引用,而虚拟机GC时一发现弱引用就会立即回收,如果ThreadLocal对象的外部强引用被回收时(此时只剩下Entry中key对其的弱引用),在下次发生GC时就会回收被弱引用的key,ThreadLocalMap的key就会变成null,但是key对应的Entry中的value仍保存在ThreadLocalMap中,一直得不到回收,造成内存泄漏。

Entry中的key为什么使用弱引用

我们知道Entry是ThreadLocalMap中的元素,Entry的key被设置为弱引用,但是为什么不能是强引用呢?

试想当我们想要清除ThreadLocalMap中的一个ThreadLocal对象,如果仅仅将ThreadLocal对象的强引用设为null时,在GC时仍无法回收ThreadLocal对象的内存,因为在ThreadLocalMap.Entry中还保留着对ThreadLocal对象的强引用,所以,Entry需要将ThreadLocal的引用设置为弱引用,当外部强引用消除后只剩下弱引用,在后续的GC中可以立即被回收。

参考

《Java高并发程序设计》--葛一鸣、郭超