欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

[Interview系列 知识储备回顾] 线程篇[2]

程序员文章站 2022-05-05 18:43:28
...

ThreadLocal

上下文是贯穿整个系统或者阶段生命周期的对象, 包含了系统全局的一些信息.
自从JDK1.2起, Java就提供了java.lang.ThreadLocal
为每一个使用该变量的线程都提供了独立的副本, 可以做到线程间的数据隔离, 每一个线程都可以访问各自内部的副本变量.

ThreadLocal的使用场景及注意事项

  • 在进行对象跨层传递的时候, 可以考虑ThreadLocal, 避免方法多次传递, 打破层次间的约束.(MethodA->MethodB->MethodC->MethodD)
  • 线程间数据隔离
  • 进行事务操作, 用于存储线程间的事务信息

ThreadLocal并不是解决多线程共享只有的技术, 一般情况下, 每一个线程的ThreadLocal存储的都是一个新的对象, 如果ThreadLocal存储的是一个对象的引用, 还是会出现数据不一致等并发问题.

ThreadLocal 源码详解

[Interview系列 知识储备回顾] 线程篇[2]

由官方javadoc给出exmaple, 可以了解到其两个API(initialValuie和get)

[Interview系列 知识储备回顾] 线程篇[2]
initialValue方法为ThreadLocal要保存的数据类型指定了一个初始化值.

[Interview系列 知识储备回顾] 线程篇[2]
get方法用于返回当前线程在ThreadLocal中的数据备份, 当前线程的数据都存放在一个ThreadLocalMap的数据结构中.
get具体流程

  1. 获取当前线程
  2. 根据当前线程获取ThreadLocalMap, 这里每个Thread类中都有个属性是跟ThreadLocalMap关联的.
  3. 如果map不为null, 则以当前的ThreadLocal为key获取对应的Entry
  4. 如果Entry不为null, 则直接返回Entry的value值, 反之进入(5)
  5. 如果在(2)获取不到对应的ThreadLocalMap, 需要执行setInitialValue方法
  6. setInitialValue方法中先通过initialValue方法获取初始值
  7. 根据当前线程Thread获取对应的ThreadLocalMap
  8. 如果ThreadLocalMap不为null, 则为map指定initialValue所获取的初始值.实际上在map.set(this, value)方法中new了一个Entry对象
  9. 如果ThreadLocalMap为null, 则创建一个ThreadLocalMap, 并且与Thread对象的threadLocals属性相关联.
  10. 返回initialValue方法的结果

[Interview系列 知识储备回顾] 线程篇[2]
set方法主要是为ThreadLocal指定将要存储的数据.
其实跟get方法中的setinitialValue是一样的. 只是少了获取initialValue并返回这个操作而已.
过程就不赘述了.

ThreadLocalMap

无论是get方法还是set方法都不可避免地要和ThreadLocalMapEntry打交道.在ThreadLocalMap中用于存储数据的是Entry, 它是一个WeakReference类型的子类, 之所以设计为WeakReference是为了能够在JVM发生垃圾回收事件时, 能够自动回收防止内存溢出的情况出现.

[Interview系列 知识储备回顾] 线程篇[2]

ThreadLocal 内存泄漏问题分析

对于内存泄漏我们都不陌生.
举个例子, 在某个线程结束了生命周期后, Thread的实例和所存储的数据还存在contexts中, 随着运行时间的增加, 在contexts中会残留很多thread实例和数据.

  • ThreadLocalMap#Entry设计为WeakReference类型, 是保证在JVM中触发GC(young gcfull gc)时都会导致Entry的回收.
  • 在get、set数据时增加检查, 清除已经被垃圾回收的Entry.

以下API会检查(key为null的Entry进行清除)

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
    // 查找key为null的Entry
    while (e != null) {
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        if (k == null)
            // 将key为null的Entry删除
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}
private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {
        i = nextIndex(i, len);
        Entry e = tab[i];
        if (e != null && e.get() == null) {
            n = len;
            removed = true;
            // 将key为null的Entry删除
            i = expungeStaleEntry(i);
        }
    } while ( (n >>>= 1) != 0);
    return removed;
}
 // 执行Entry在ThreadLocalMap中的删除操作
 private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // expunge entry at staleSlot
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // Rehash until we encounter null
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                tab[i] = null;

                // Unlike Knuth 6.4 Algorithm R, we must scan until
                // null because multiple entries could have been stale.
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}

因此, ThreadLocal可以在一定程序上保证不发生内存泄漏.

借用<Java高并发详解 - 汪文君老师>书中一个例子.

ThreadLocal<byte[]> threadLocal = new ThreadLocal<>();
TimeUnit.SECONDS.sleep(30);
threadLocal.set(new byte[1024 * 1024 * 100]); //100MB
threadLocal.set(new byte[1024 * 1024 * 100]); //100MB
threadLocal.set(new byte[1024 * 1024 * 100]); //100MB
threadLocal = null;
currentThread().join();

程序运行时, 借用VisualVM工具对JVM进程进行监控.
[Interview系列 知识储备回顾] 线程篇[2]
最后的100MB堆内存一直没有得到释放.
[Interview系列 知识储备回顾] 线程篇[2]
threadLocal显式指定为null之后, 执行gc操作, 堆内存中的threadLocal被回收, 同时ThreadLocalMap中的Entry.key也变为null, 但是value不会被释放. 除非当前线程结束了生命周期, thread引用被回收.

内存泄漏内存溢出是有区别的, 内存泄漏是导致内存溢出的原因之一, 内存泄漏更多的是程序中不再持有某个对象的引用, 导致该对象无法被垃圾回收, 原因是因为该对象到根Root的链路是可达的, 比如 ThreadRefEntry.value的引用链路.