15-Java多线程、ThreadLocal

ThreadLocal

一、ThreadLocal

  • 线程本地变量。ThreadLocal变量是线程隔离的,多个线程读写ThreadLocal变量是互不影响的。它提供了一种将可变数据通过每个线程有自己的独立副本从而实现线程封闭的机制。比如有一个ThreadLocal变量val,那么在多线程环境下,实际上每一个线程对他的操作都是互不影响的,仿佛是每个线程都有一个val变量一样。ThreadLocal实例通常是类中的private static字段。

二、使用

2.1 示例

public class ThreadLocalTest {

    private static ThreadLocal<Integer> val = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            //初始化值
            return 100;
        }
    };

    private static class MyThread extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                Integer current = val.get();
                Integer ret = current + 1;
                val.set(ret);
            }
            System.out.println(Thread.currentThread().getName() + ":" + val.get());
            val.remove();
            System.out.println("移除后:" + Thread.currentThread().getName() + ":" + val.get());
        }
    }

    public static void main(String[] args) {
        Thread t1 = new MyThread();
        Thread t2 = new MyThread();
        Thread t3 = new MyThread();

        t1.start();
        t2.start();
        t3.start();
    }
}

输出:
Thread-2:110
Thread-1:110
Thread-0:110
移除后:Thread-1:100
移除后:Thread-2:100
移除后:Thread-0:100  

从运行结果可以看出,每个线程初始化都是100,自增10次后得到的都是110,就像前面说的每个线程都有一个自己的变量,互补干扰,remove后又变回了初始值。
这里说明ThreadLocal确实是可以达到线程隔离机制,确保多线程环境下变量的安全性和隔离性。

2.2 关键方法

  • set(T value):设置本线程变量副本值

  • get():获取本线程的变量副本值

  • initialValue():返回本线程变量副本的初始值

  • remove():删除本线程变量副本值

  • 如前面的例子,初始化是100,每个线程自增十次,因此每个线程都是110,然后remove移除之后,每个线程的值就是初始值100了。

三、原理示图

  • 线程类Thread内部包含一个ThreadLocalMap类型的实例变量,它是ThreadLocal类的一个子类,先暂且理解为一个存储k-v对的Map,不同的线程对ThreadLocal变量进行操作时(包括get/set/remove),都会找到本线程对应的Map然后再操作这个map,因为这个map完全就是线程对象的一个成员变量,因此肯定是线程隔离的,线程之间互不影响,简单代码如下所示。
    public T get() {
        //1.获取本线程对象
        Thread t = Thread.currentThread();
        //2.获取本线程对应的ThreadLocalMap,其实就是Thread对象的一个属性
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //3.从本线程对应的ThreadLocalMap中获取变量值
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
    
    //每个Thread包含一个ThreadLocalMap类型的属性,属性里面保存着全部的ThreadLocal变量
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

示意图

  • 这个图是盗取的(非常抱歉-_- , 后面根据自己的理解画了一张图)

image

四、ThreadLocal源码解析

  • ThreadLocalMap是实现ThreadLocal的关键,ThreadLocalMap其内部利用Entry来实现k-v的存储,如下:
static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
  • 代码可以看出Entry的包含一个k-v对,Key是ThreadLocal本身变量,而value就是值。同时Entry继承了WeakReference,所以说Entry所对应key(ThreadLocal实例)的引用为一个弱引用(关于弱引用和k-v对在后面关于内存泄露那一块在详细说,这里先不多说)。

4.1 set

  • set是ThreadLocal的设值方法,其实就是将键值对保存到线程对象里面的ThreadLocalMap里面去
  • ThreadLocal#set
public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
  • ThreadLocal.ThreadLocalMap#set
//ThreadLocal.ThreadLocalMap#set
private void set(ThreadLocal<?> key, Object value) {

    ThreadLocal.ThreadLocalMap.Entry[] tab = table;
    int len = tab.length;

    // 根据 ThreadLocal变量的散列值,查找对应元素在数组中的位置
    int i = key.threadLocalHashCode & (len-1);

    // 采用“线性探测法”,寻找合适位置
    for (ThreadLocal.ThreadLocalMap.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 == null,但是存在值(因为此处的e != null),说明之前的ThreadLocal对象已经被回收了
        if (k == null) {
            // 用新元素替换陈旧的元素
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    // ThreadLocal对应的key实例不存在也没有陈旧元素,new 一个
    tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value);

    int sz = ++size;

    // cleanSomeSlots 清楚陈旧的Entry(key == null)
    // 如果没有清理陈旧的 Entry 并且数组中的元素大于了阈值,则进行 rehash
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}
  • 在HashMap中采用链地址法解决hash冲突,在ThreadLocalMap是采用开放定址法。set()操作除了存储元素外,还有一个很重要的作用,就是replaceStaleEntry()和cleanSomeSlots(),这两个方法可以清除掉key为null 的实例,防止内存泄漏(后面小结专门写关于内存泄露的部分)。

  • threadLocalHashCode变量是key的hash值的,定义如下:

    //final类型意味着ThreadLocal一旦创建其散列值就已经确定了,
    //是调用nextHashCode()生成的
    private final int threadLocalHashCode = nextHashCode();
    
    //nextHashCode表示分配下一个ThreadLocal实例的threadLocalHashCode的值
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
    
    //HASH_INCREMENT表示分配两个ThradLocal实例的threadLocalHashCode的增量
    private static final int HASH_INCREMENT = 0x61c88647;
    
    public final int getAndAdd(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta);
    }

4.2 get

  • getEntry是ThreadLocal获取值的方法,其实就是根据hash值定位到ThreadLocalMap里面的Entry数组,思想上没有太多深奥的东西
  • ThreadLocal#get
//ThreadLocal#get
public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
  • ThreadLocal.ThreadLocalMap#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);
}
  • 由于采用了开放定址法,所以当前key的散列值和元素在数组的索引并不是完全对应的,首先取一个探测数(key的散列值),如果所对应的key就是
    我们所要找的元素,则返回,否则调用getEntryAfterMiss(),如下:
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    while (e != null) {
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        if (k == null)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}
  • 这里有一个重要的地方,当key为null时,调用了expungeStaleEntry()方法,该方法用于处理key为null的键值对,有利于GC回收,能够有效地避免内存泄漏。

4.3 initialValue()

  • initialValue()默认是返回null的,需要在使用的时候重写,我们主要看看它是时候调用的
protected T initialValue() {
        return null;
    }
  • 我们在ThreadLocal#get方法中可以看到,当map为null的时候,会执行setInitialValue方法,因此首次来获取值的时候,会走这个逻辑来对值进行初始化。
  • ThreadLocal#setInitialValue
private T setInitialValue() {
        //initialValue方法就是我们重写的方法,在这个方法里面返回初始值
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

4.4 remove

  • ThreadLocal#remove方法将变量移除,其实是调用ThreadLocalMap的remove方法
public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         //其实是调用了ThreadLocalMap的remove方法
         if (m != null)
             m.remove(this);
     }
  • ThreadLocal.ThreadLocalMap#remove
    private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            //1.定位键值对在Entry数组中的位置
            int i = key.threadLocalHashCode & (len-1);
            //2.如果元素不是要找的元素,就一直向后面遍历,但是并不是直接向后扫描,而是有一个向后扫描的规则
            //每次都会在nextIndex里面计算下一次要去找的下标位置
            for (Entry e = tab[i] ; e != null ; e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }
  • Reference#clear和expungeStaleEntry:clear会将当前的引用置为null,expungeStaleEntry会清理一些null的key,并做长度减一之后的再散列
public void clear() {
        this.referent = null;
}
    
private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            //清理当前的Entry
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            //再散列,直到遇到一个key为null的位置
            Entry e;
            int i;
            //当前的staleSlot是需要清理的位置,先计算这个位置再散列之后的位置i,如果i这个位置不是null(说明有一个键值对占用着),
            //那么就对这个键值对进行处理,处理逻辑就是先检查key是不是null,key是null就把他的置为null,如果不是说明这个键值对
            //是健康的,那么就给他找一个新的下标为h的位置,并把它放在h的位置,并且在计算新位置的时候会将长度减1
            //(也会考虑冲突,或者找的新位置其实没有变化的情况)这个循环处理的过程一直到往后找到一个null的key为止
            //也就是说方法会将staleSlot位置和下一个key为null之间的键值对全部对len-1进行再散列一次
            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;
        }

五、ThreadLocalMap解析

  • ThreadLocal的一个内部类,可以理解为一个Map,但是包含其自身的一些特点。

5.1 特点

  • 这个map真正保存变量是一个Entry数组,初始容量16,阈值是容量的2/3。这里注意容量和可保存的元素个数是2个概念,容量是数组的length,
    但是保存个数一定小与阈值,阈值是容量算出来的,换言之保存的元素达到了容量的2/3就需要再次扩容了。(比如初始容量16,那么阈值是10,
    保存了10个元素就需要扩容了)
    static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

        /**
         * The initial capacity -- MUST be a power of two.
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;
        
        /**
         * Set the resize threshold to maintain at worst a 2/3 load factor.
         */
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }
  • 保存元素的时候,根据元素的threadLocalHashCode值来计算再环中的位置,因此这里是一个取模的操作,和HashMap类似

六、关于ThreadLocal内存泄露

  • 在第三点的图中画出了ThreadLocal在栈和堆中的引用关系,关于为什么会有内存泄露,参考文章[2]写的很不错,推荐阅读,最初我看这个图
    也是有点不理解,后面就自己再画了一个。

image

6.1 引用分析

  • 首先我们回头看前面的示例代码,我们声明了一个ThreadLocal变量,这个变量不是在方法内部申明的(在方法内部使用),因此这个ThreadLocal对象肯
    定是保存在堆中的。然后我们在方法内部使用它,我们看到其实有多个线程使用它,因此在这个若干个线程的方法栈里面是通过val引用了保存在堆中的ThreadLocal
    对象的,并通过这个引用对它进行了相关的get/set等操作,由此我们对上图的link1明白了。

  • 我们回到源代码看看set方法,我们看到保存到map的键值对的key是this,而this谁?this是一个对象,而且就是ThreadLoca对象,也就是说ThreadLocal对象调用
    set保存一个value的时候,他把自己作为key,value作为值一起保存到了当前线程所持有的ThreadLocalMap里面去了,由此我们对上图的link2明白了。但是为什么关
    系2是虚线,我们看后面。

 //ThreadLocal#set
public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
  • 上面一点我们明白了map.set(this, value)的含义,我们再进去看看ThreadLocal.ThreadLocalMap#set方法的tab[i] = new Entry(key, value);
    含义明了就是构造一个Entry对象保存到Entry数组的指定位置,但是我们看看下面的构造方法,他把key传给了super父类,自己就是保存了value,
    这里的父类一直传给了Reference类型,保存到了referent变量。这里的含义是说Entry继承了WeakReference是一个弱引用,引用的是key所指向的
    对象,而key传进来的就是ThreadLocal对象,因此Entry的key是一个指向堆中ThreadLocal对象的若引用。由此明白了link2是虚线。
static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
//Reference#Reference(T)
Reference(T referent) {
        this(referent, null);
    }
Reference(T referent, ReferenceQueue<? super T> queue) {
        this.referent = referent;
        this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
    }
  • 关于强弱引用这里不多述,简单说如果一个对象只有弱引用对它进行关联,那么下一次垃圾回收的时候,这个对象一定会被当做垃圾回收掉。比如对象
    Object,存在一个强引用linkA和弱引用linkB指向它,那么对象Object肯定不是垃圾,是不能被回收的,如果linkA没有了只有linkB,那么下一次对象Object
    会被回收掉。(这里可以参考深入理解Java虚拟机第三章-再谈引用)

6.2 内存泄漏

6.2.1 分析
  • 假设图中的线程1方法内对val的使用已经完毕,(比如方法内将引用置为null),那么link1就没有了,那么此时堆中的ThreadLocal对应就只有一个弱引用指向它,那么他就会被回收掉,而key就是这个ThreadLocal对象,因此key也会被回收掉(弱引用会被自动回收),但是key没有了value还在,因为当前线程的ThreadLocalMap有对Entry的强引用link3,而且key是null以后这个value无法被访问,但是value对应的对象可能保存在堆中另一个地方占据着10MB的内存,由此导致了内存泄漏。
6.2.2 条件
  • 我们先看看value不发生内存泄漏的条件是什么,再来看看如何改进避免。我们现在看到Entry这个对象有2条引用路径,引用路径1由link1和link2组成,线程对ThreadLocal的强引用link1和key对ThreadLocal的弱引用link2,只要link1不存在了,那么link2也会回收,ThreadLocal和key会被同时回收。引用路径2是线程内的ThreadLocalMap对Entry的强引用link3,link3只有在线程死亡的时候才会断掉,也就是说只要线程不死,link3一直存在。
  • 问题 :我们分析了引用路径1和2之后,我们思考假设link1没有了(比如执行:val=null),ThreadLocal和key被回收,value内存泄漏,(刚好就是前面分析的)。如果我们什么都不做又不想发生内存泄漏,就只能等到线程1死亡的时候link3断掉(线程死亡是否好控制,如果是线程池呢?),才能回收掉value。当然我们也可以手动让置Entry为null,remove方法就可以做到。
  • 由此我们知道不发生内存泄漏的条件有下面2个:
1.条件1:link1,link2和link3都被打断。当前这三个被打断之后,Entry就会被回收,但是这比较困难
首先:ThreadLocal变量往往被声明为static类型,这意味着他的生命周期和所在的类一样,即使我们显式打断link1(比如执行:val=null),此时ThreadLocal不一定会被回收,那么link2就存在。其次:即使不是声明为static类型,我们也要考虑多线程环境下需要打断所有线程对同一个ThreadLocal的link2(如图中线程12)。第三:打断link3需要线程死亡,但是很多时候我们并不确定什么时候线程死亡,比如使用线程池的时候。由此我们看到这个条件1是比较苛刻不好控制的)
1.条件1:将Entry中特定的k-v对置为null(回收特定的ThreadLocal键值对,将Entry的k-v都置为null后肯定会被回收,ThreadLocal的remove方法可以做到,
另外ThreadLocal有改进点也能在某些情况下做到,在6.4阐述)
2.条件2:线程死亡。线程死亡,link3被打断,Entry不存在强引用,回收整个ThreadLocalMap里面的键值。

6.3 改进和解决

  • ThreadLocal也考虑到了内存泄漏的缺点,在早期版本的ThreadLocal中ThreadLocalMap就是使用HashMap来充当,现在使用ThreadLocalMap来充当,为了避免内存泄漏主要有以下三点改进。
1.Entry使用ThreadLocal的弱引用作为key,如果没有其他强引用指向ThreadLocal,ThreadLocal就会被回收,弱引用自身也会被回收,此时key会置为null
2.get方法内部会增加数据检查,清除key为null的Entry (通过expungeStaleEntry方法)
3.set方法内部会增加数据检查,清除key为null的Entry (通过expungeStaleEntry方法)
  • 我们根据6.3.2中的条件1,知道将要特定的Entry中k-v对置为null有2个办法,一个是显式调用remove方法,另一个是ThreadLocal自身的一些机制。但是自身的机制需要触发是需要一些条件的,我们来看看触发条件。条件A:ThreadLocal被回收;(ThreadLocal被回收后,key才会变成null),条件B:调用get/set方法;(调用方法会将前面key为null的Entry回收),换言之要满足条件AB才能避免内存泄漏。但是条件AB并不容易满足,对于条件A来说,我们必须将栈中的ThreadLocal引用置为null,对于条件B来说要求我们显式调用一次get或者set方法,而且我调试发现,并不是每次get/set都一定保证会去清除key为null的Entry,有时候并不会触发清理逻辑,以set方法举例,里面会走到ThreadLocalMap#cleanSomeSlots方法来清理key为null的Entry,但是里面的逻辑不是百分百保证执行的,请查看示例7.1。
 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;
                    i = expungeStaleEntry(i);//这里的逻辑不是百分百保证可以执行的。
                }
            } while ( (n >>>= 1) != 0);
            return removed;
        }
  • 既然自身的机制存在一定程度的缺陷,那么我们最好是显式调用remove方法,查看示例7.2和7.3。

  • 在6.3.2中,我们还说道了保证不内存泄漏的条件2是线程死亡,在代码演示的7.1、7.2和7.3,我都让子线程阻塞了,也就是发生内存泄漏的线程没有死亡,
    这对应这我们不确定线程什么时候死亡的情况,比如在使用线程池的时候,这也是内存泄漏的灾区。如果线程执行完毕之后立刻死亡,是不会发生内存泄漏的,
    因为此时Entry不存在来自线程内部的ThreadLocalMap的强引用,整个Entry会被回收。请参考代码演示7.4

  • 不过在这几点不能帮助我们满足7.3.2中分析2个条件之一,对于条件1,即使我们打断了全部的link1,ThreadLocal也不一定就会回收,此时key就不会为null,由此这三点将无能为力,退一步当我们打断全部的link1导致ThreadLocal被回收了,key变成了null,此时也需要我们调用get/set方法才会触发这个清除的过程,因此这些改进有一定的帮助但是还不能一劳永逸;至于7.3.2的条件2更是需要我们手动去显式调用。

  • 简言之,改进点不足以避免内存泄漏,因此我们只能主动控制条件2,显式调用remove方法来将Entry置为null。由此我们得到结论,在使用ThreadLocal的时候,使用完毕之后最好remove移除对应的变量。

1.显式调用remove方法。在我们不需要使用ThreadLocal变量的时候,在线程中调用remove方法,释放对应的Entry。

七、代码演示

  • 这一小节我们通过代码和垃圾回收简单看看,不同的场景下垃圾回收的效果。
  • 打印垃圾回收日志的启动参数:-verbose:gc

7.1 set/get的清理逻辑不能保证一定执行

  • 我们看到很多资料写到ThreadLocal的get和set里面会有逻辑去清理掉那些key为null的Entry来避免内存泄漏,不幸的是这不是一个百分百的保证,下面的例子
    中我定义了3个ThreadLocal,第1个和第2个各保存500MB数据,然后将第2个置为null,用第3个另一个调用一次set,我们查看结果看能否将第二个的500MB内存回收:
public class ThreadLocalTest2 {

    private static class MyThread extends Thread {
        private  ThreadLocal<byte[]> val = new ThreadLocal<>();
        private  ThreadLocal<byte[]> val1 = new ThreadLocal<>();
        private  ThreadLocal<String> val2 = new ThreadLocal<>();
        private  ThreadLocal<String> val3 = new ThreadLocal<>();

        @Override
        public void run() {
            //分配500MB内存
            val.set(new byte[500 * 1024 * 1024]);
            val1.set(new byte[500 * 1024 * 1024]);
            try {
                Thread.sleep(3 * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            val1 = null; //断掉link1  --- code line A
            try {
                Thread.sleep(2 * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            val2.set("123"); //调用set,触发一次对key为null的Entry的清理  --- code line A
            //val3.set("123"); //调用set,触发一次对key为null的Entry的清理  --- code line B

            System.out.println("Thread" + Thread.currentThread().getName() + " finished");
            try {
                Thread.sleep(100 * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new MyThread();
        t1.start();
        Thread.sleep(2 * 1000);
        //连续看3次垃圾回收的状态
        for (int i = 0; i < 5; i++) {
            Thread.sleep(2 * 1000);
            System.gc();
        }
        Thread.sleep(1000 * 1000);
    }
}
  • 输出:
[Full GC (System.gc())  1024736K->1024636K(1148928K), 0.0173612 secs]
ThreadThread-0 finished
[GC (System.gc())  1025291K->1024700K(1148928K), 0.0133910 secs]
[Full GC (System.gc())  1024700K->1024632K(1148928K), 0.0166479 secs]
[GC (System.gc())  1025287K->1024632K(1148928K), 0.0091974 secs]
[Full GC (System.gc())  1024632K->1024632K(1148928K), 0.0101045 secs]
[GC (System.gc())  1024632K->1024632K(1148928K), 0.0128461 secs]
[Full GC (System.gc())  1024632K->1024632K(1148928K), 0.0115059 secs]
[GC (System.gc())  1024632K->1024632K(1148928K), 0.0165419 secs]
[Full GC (System.gc())  1024632K->1024632K(1148928K), 0.0068197 secs]
  • 从结果来看,很不幸,我们一直未能回收那500MB的内存,这说明了set方法的清理逻辑不是百分百保证执行的。
  • 再次尝试,将code line B放开,相当于多调用了一次set,我们看到这一次回收了500MB的内存,输出如下:
    (其实get方法也是一样,不能保证一定会清理那些key为null的Entry,因此利用这点来避免内存泄漏还是有一点风险的),
[GC (System.gc())  1027945K->1024736K(1148928K), 0.0106728 secs]
[Full GC (System.gc())  1024736K->1024636K(1148928K), 0.0252458 secs]
ThreadThread-0 finished
[GC (System.gc())  1025291K->1024668K(1148928K), 0.0194063 secs]
[Full GC (System.gc())  1024668K->512632K(1148928K), 0.0092766 secs]
[GC (System.gc())  513287K->512632K(1148928K), 0.0048769 secs]
[Full GC (System.gc())  512632K->512632K(1148928K), 0.0084692 secs]
[GC (System.gc())  512632K->512632K(1148928K), 0.0041700 secs]
[Full GC (System.gc())  512632K->512632K(1148928K), 0.0052558 secs]
[GC (System.gc())  512632K->512632K(1148928K), 0.0061889 secs]
[Full GC (System.gc())  512632K->512632K(1148928K), 0.0082883 secs]

7.2 ThreadLocal置为null不能避免内存泄漏

  • 将ThreadLocal置为null,我们看后面内存是否会泄漏。下面例子中,子线程在ThreadLocal中保存500MB数据,
    然后将引用置为null,再看垃圾回收是否能够回收这500MB空间,结果显式不能,说明内存泄漏了
public class ThreadLocalTest1 {

    private static ThreadLocal<byte[]> val = new ThreadLocal<>();

    private static class MyThread extends Thread {
        @Override
        public void run() {
            //分配500MB内存
            val.set(new byte[500 * 1024 * 1024]);
            try {
                Thread.sleep(3 * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            //val.remove();//移除 --- code line A
            val = null; //断掉link1  --- code line B
            System.out.println("Thread" + Thread.currentThread().getName() + " finished-2");
            //让子线程不退出,对比效果
            try {
                Thread.sleep(100 * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new MyThread();
        t1.start();
        Thread.sleep(2 * 1000);
        //连续看3次垃圾回收的状态
        for (int i = 0; i < 5; i++) {
            System.gc();
            Thread.sleep(2 * 1000);
        }
        Thread.sleep(1000 * 1000);
    }
}
  • 输出(结果说明将ThreadLocal置为null不能避免内存泄漏):
[GC (System.gc())  515945K->512704K(636416K), 0.0023362 secs]
[Full GC (System.gc())  512704K->512636K(636416K), 0.0111372 secs]
ThreadThread-0 finished-2
[GC (System.gc())  513291K->512668K(636416K), 0.0096473 secs]
[Full GC (System.gc())  512668K->512632K(636416K), 0.0137339 secs]
[GC (System.gc())  513287K->512632K(636416K), 0.0088515 secs]
[Full GC (System.gc())  512632K->512632K(636416K), 0.0148090 secs]
[GC (System.gc())  512632K->512632K(636416K), 0.0115807 secs]
[Full GC (System.gc())  512632K->512632K(636416K), 0.0120289 secs]
[GC (System.gc())  512632K->512632K(636416K), 0.0305692 secs]
[Full GC (System.gc())  512632K->512632K(636416K), 0.0059499 secs]
  • 下图是将code line B放开,将code line A注释后的输出,我们看到垃圾回收的时候,不能回收500MB的内存,说明分配的字节数组内存泄漏。

7.3 remove

  • 将上面7.2的代码code line A放开,将code line B注释后的输出如下:我们看到GC回收了500MB的内存,说明内存没有泄漏
[GC (System.gc())  515945K->512720K(636416K), 0.0072187 secs]
[Full GC (System.gc())  512720K->512636K(636416K), 0.0195568 secs]
ThreadThread-0 finished-2
[GC (System.gc())  513291K->512668K(636416K), 0.0064866 secs]
[Full GC (System.gc())  512668K->632K(636416K), 0.0096974 secs]
[GC (System.gc())  1287K->632K(636416K), 0.0018273 secs]
[Full GC (System.gc())  632K->632K(636416K), 0.0079401 secs]
[GC (System.gc())  632K->632K(636416K), 0.0018090 secs]
[Full GC (System.gc())  632K->632K(636416K), 0.0082545 secs]
[GC (System.gc())  632K->632K(636416K), 0.0051946 secs]
[Full GC (System.gc())  632K->632K(636416K), 0.0065772 secs]

7.4 线程死亡不会内存泄漏

  • 在前面几个例子中,我们将子线程sleep的目的是保证子线程不退出,子线程不退出,因此才能看到对比的效果(如同线程池),如果子线程退出了,
    那么link3没有了,没有任何强引用指向Entry,因此内存是会被回收的。将7.2的代码基础上(lineA和B都可以注释掉,说明都不做),将MyThread最
    后的sleep注释掉,让现在执行完后就死亡,输出如下(说明内存被回收了,没有泄漏):
[GC (System.gc())  515945K->512752K(636416K), 0.0039485 secs]
[Full GC (System.gc())  512752K->512636K(636416K), 0.0117133 secs]
ThreadThread-0 finished-2
[GC (System.gc())  513291K->512668K(636416K), 0.0070228 secs]
[Full GC (System.gc())  512668K->632K(636416K), 0.0127007 secs]
[GC (System.gc())  1287K->632K(636416K), 0.0011209 secs]
[Full GC (System.gc())  632K->631K(636416K), 0.0189230 secs]
[GC (System.gc())  631K->631K(636416K), 0.0009909 secs]
[Full GC (System.gc())  631K->631K(636416K), 0.0092727 secs]
[GC (System.gc())  631K->631K(636416K), 0.0003186 secs]
[Full GC (System.gc())  631K->631K(636416K), 0.0037359 secs]

八、小结

8.1 设计

  • ThreadLocal使用上,主要就是initialValue初始化方法和get/set、remove方法。
  • ThreadLocal提供了线程本地变量,每个线程操作的都是自己专有的那一份变量,互不干扰。
  • 所有的ThreadLocal变量保存在一个ThreadLocalMap里面,这个Map以一个成员变量的形式保存在当前线程对象里面,因此可以做到线程隔离。
  • ThreadLocalMap里面使用了开放地址发解决Hash冲突,冲突的时候按照既定算法往后面再找一个位置。

8.1 安全

  • 关于内存泄漏,前面总结了很多,一个原则就是:使用完ThreadLocal变量之后,记得调用remove显式移除,帮助垃圾回收

8.2 threadLocalHashCode

    • threadLocalHashCode:哈希散列值,通过该值在哈希数组中定位每一个ThreadLocal变量的数组下标,每个threadLocalHashCode之间的增量是0x61c88647,原因后面解释。
  • 我们通常是创建一个static类型的ThreadLocal类型的变量,但是我们也可以创建很多个,如果创建多个,那么每一个ThreadLocal实例的hashcode是有联系的,她们从0开始,每次自增一个常数t(0会不要,即第一个实例的hash值是t,第二个是t+t,第三个是t+t+t,不过这里是使用整型表示,因此超过int的最大表示之后会按照int的表示方法呈现结果)如下所示:
    //threadLocalHashCode是在每一次创建实例的时候计算出来的
    private final int threadLocalHashCode = nextHashCode();
    
    //nextHashCode是static类型的,因此对于所有的ThreadLocal实例而言都是一致且可见的,
    //每次增加0x61c88647之后,下一次又会以上次计算的结果再次加一个0x61c88647
    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    private static final int HASH_INCREMENT = 0x61c88647;

    //计算就是以nextHashCode为基数,每次加一个常数0x61c88647
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
  • 如下我创建了几个实例,他们的threadLocalHashCode就是按照这样的规律,注意0x61c88647的十进制值是1640531527,下面的数字之间的差值就是1640531527

image

  • 代码如下:(注意自己创建的第一个ThreadLocal并不一定真的是第一个实例,可能系统也创建了,因此自己的实例的threadLocalHashCode值并不是从1640531527开始)
        int i1 = -387276957;
         int i2 = 1640531527 + i1;
         int i3 = 1640531527 + i2;
         int i4 = 1640531527 + i3;
         int i5 = 1640531527 + i4;

        System.out.println("int 1 : " + i1);
        System.out.println("int 2 : " + i2);
        System.out.println("int 3 : " + i3);
        System.out.println("int 4 : " + i4);
        System.out.println("int 5 : " + i5);
        
输出:
int 1 : -387276957
int 2 : 1253254570
int 3 : -1401181199
int 4 : 239350328
int 5 : 1879881855
  • 作用
从源码注释来看,增加这个值是为了避免hash冲突,这里没有简单的使用自增或者hashcode的方法。这使得threadLocalHashCode总是1640531527的倍数,
它能够让threadLocalHashCode很均匀的分布在2的N次方的范围里面。至于原因,这个数字是2的31次方乘以(根号5-1),具体应该和数学原理有关。

九、参考

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值