专注于 JetBrains IDEA 全家桶,永久激活,教程
持续更新 PyCharm,IDEA,WebStorm,PhpStorm,DataGrip,RubyMine,CLion,AppCode 永久激活教程

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

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值发生冲突,在数组下面继续找
}

<pre><code class="line-numbers"><br />### 原理总结 ###

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

### 内存泄漏 ###

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

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

</code></pre>

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高并发程序设计》–葛一鸣、郭超

文章永久链接:https://tech.souyunku.com/42509

未经允许不得转载:搜云库技术团队 » 读书笔记-Java高并发程序设计(二)

JetBrains 全家桶,激活、破解、教程

提供 JetBrains 全家桶激活码、注册码、破解补丁下载及详细激活教程,支持 IntelliJ IDEA、PyCharm、WebStorm 等工具的永久激活。无论是破解教程,还是最新激活码,均可免费获得,帮助开发者解决常见激活问题,确保轻松破解并快速使用 JetBrains 软件。获取免费的破解补丁和激活码,快速解决激活难题,全面覆盖 2024/2025 版本!

联系我们联系我们