聊聊ThreadLocal

ThreadLocal提供一个保存线程本地变量的方案。每个线程都能保存它自己的变量,线程之间变量独立。

又学习到多线程相关的了。看了下ThreadLocal的作者,又有Doug Lea大神。日常膜拜一下大神(牛逼牛逼牛逼)

强软弱虚四种应用

强引用:比如说

Student stu = new Student();

这个stu就是强引用,只要这个引用指向堆中的Student对象。Student对象就不会被垃圾回收器回收。

软引用:

SoftReference soft = new SoftReference<>(new Student());
Student student = soft.get();

soft里保存了指向Student对象的引用。通过soft.get()可以获取到Student对象。

但是当堆内存空间不足的时候,这个Student对象就会被垃圾回收器回收。用soft.get()就获取不到对象了,返回的是null

弱引用:

WeakReference weak = new WeakReference<>(new Student());
Student student = weak.get();

weak里保存了指向Student对象的引用。通过weak.get()可以获取到Student对象。

但是当垃圾回收器启动的时候,这个Student对象就会被垃圾回收器回收。用weak.get()就获取不到对象了,返回的是null

虚引用:PhantomReference

用于调度回收前的清理工作,管理堆外内存。

内存图

先来看一下内存图,看看这个类是怎么存东西的

public class Test {
    public static void main(String[] args){
        ThreadLocal t = new ThreadLocal<>();
        t.set(new Student());
        t.get();
        t.remove();
    }
}

class Student{}

写了一段代码,往ThreadLocal里存我们自己的一个类Student

ThreadLocal内存图.png

看上面这个图。我们的student对象副本其实保存在线程对象中的,所以能够实现不同线程之间互不影响。

Thread,就是线程对象,里面有一个属性threadLocals,它的类型是ThreadLocal的一个内部类

threadLocals对象里有一个属性table,是一个Entry数组

Entry对象它是继承于WeakReference,它自己有一个属性value,这个引用指向的就是我们要保存的student对象。还有一个继承于父类的属性referent,指向的就是ThreadLocal对象。弱引用的特点是,当垃圾回收器启动时,如果没有其它应用指向这个ThreadLocal对象,仅仅只有弱引用指向该对象,那么该对象就会被回收掉。

一些要点

1.用继承于WeakReference的Entry对象存储key和value,key是弱引用,指向ThreadLocal对象。成员变量value是强引用,指向我们要保存的对象,存储在线程对象中。

2.存储键值对的时候,遇到hash冲突,不会像HashMap一样构建链表,而是往数组后面的空位存。

3.get、set、remove方法中发现key为空都会清理表中的失效项目。

4.数组扩容的时候,会先清理整个数组的失效项目,将size减小一点。如果不超过阈值了,就不需要扩容。如果还是超过阈值,才会将数组扩容2倍

源代码

set方法

    public void set(T value) {
        Thread t = Thread.currentThread();//获取当前线程对象
        ThreadLocalMap map = getMap(t);//获取线程对象里的threadLocals
        if (map != null)//如果map已经实例化了,那么往里放值
            map.set(this, value);
        else//如果还没有实例化,那么就去创建
            createMap(t, value);
    }

getMap(t)

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;//返回线程对象中的属性threadLocals
    }

创建map的方法,也就是创建threadLocals

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

ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];//创建一个初始容量为16的Entry数组
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);//和HashMap一样,也是用hash&(数组长度-1)来计算出数组下标
    table[i] = new Entry(firstKey, firstValue);//创建一个Entry放到数组中
    size = 1;
    setThreshold(INITIAL_CAPACITY);//设置扩容阈值,这里是16*2/3
}

再来看下map.set(this, value);

private void set(ThreadLocal key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);//算出数组下标

    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {//如果有数组该位置有对象的情况,是hash冲突了,遍历数组,往后放
        ThreadLocal k = e.get();

        if (k == key) {//找到key相同的,就替换value
            e.value = value;
            return;
        }

        if (k == null) {//key为空?这是弱引用指向的ThreadLocal对象已经被回收了,代表数组这个位置的Entry已经废弃了,替换掉它
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);//找到了数组中的一个空位置,new一个Entry放进去
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)//这里是清理一些陈旧的项目。什么是陈旧的项目?就是弱引用被回收的项目,就是key已经是null了。那么要将数组这个位置清理掉
        rehash();
}

看下替换陈旧项目的方法replaceStaleEntry(key, value, i);

会扫描数组,并且清理找到的陈旧项目。但是由于采用了对数次扫描,不能完全清理整个数组的陈旧项目

private void replaceStaleEntry(ThreadLocal key, Object value,
                               int staleSlot) {//传入的staleSlot是陈旧插槽的数组索引位置
    Entry[] tab = table;
    int len = tab.length;
    Entry e;

    // Back up to check for prior stale entry in current run.
    // We clean out whole runs at a time to avoid continual
    // incremental rehashing due to garbage collector freeing
    // up refs in bunches (i.e., whenever the collector runs).
    int slotToExpunge = staleSlot;//这两个值相等了,后面可能会判断到
    for (int i = prevIndex(staleSlot, len);//往前找key为空的插槽。
         (e = tab[i]) != null;//数组是有阈值控制扩容的,不会存满,会碰到null的情况跳出循环
         i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;//出循环之后slotToExpunge的位置是一个key被回收掉的Entry。后面清理会从这个位置开始往后清理。

    // Find either the key or trailing null slot of run, whichever
    // occurs first
    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;//如果往后找,碰到一个位置没有Entry,就退出循环了
         i = nextIndex(i, len)) {
        ThreadLocal k = e.get();

        // If we find key, then we need to swap it
        // with the stale entry to maintain hash table order.
        // The newly stale slot, or any other stale slot
        // encountered above it, can then be sent to expungeStaleEntry
        // to remove or rehash all of the other entries in run.
        if (k == key) {//往后找到了key相等的,替换
            e.value = value;

            tab[i] = tab[staleSlot];//和失效的槽替换一下。保证数组中的key都是连续的,不为空?
            tab[staleSlot] = e;

            // Start expunge at preceding stale entry if it exists
            if (slotToExpunge == staleSlot)//这里相等,是因为前面往前找没有找到key为空的
                slotToExpunge = i;//刚刚交换了一下,要清理的位置是从i开始了
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);//清理插槽
            return;
        }

        // If we didn't find stale entry on backward scan, the
        // first stale entry seen while scanning for key is the
        // first still present in the run.
        if (k == null && slotToExpunge == staleSlot)//往后找,碰到一个key为空的。
            slotToExpunge = i;//如果slotToExpunge == staleSlot的话,表示531行往前找没有key为空的了。后面的清理插槽就从i的位置开始吧。
    }

    // If key not found, put new entry in stale slot//key没有找到的话,会到这里
    tab[staleSlot].value = null;//回收陈旧项目的value
    tab[staleSlot] = new Entry(key, value);//创建新的键值对放入

    // If there are any other stale entries in run, expunge them
    if (slotToExpunge != staleSlot)//不相等是由于531的循环往前找key为空的键值对
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

对数扫描,清理一些插槽的方法cleanSomeSlots(int i, int n)

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);//对数次扫描。这是在不扫描和线性扫描之间的平衡。后者虽然能清除所有垃圾,但是会造成某些插入操作花费O(n)的时间
    return removed;
}

expungeStaleEntry用于清除传入的位置到下一个数组空位置之间的所有陈旧项目

private int expungeStaleEntry(int staleSlot) {//清除staleSlot和下一个数组中的空位置之间的所有陈旧条目
    Entry[] tab = table;
    int len = tab.length;

    // expunge entry at staleSlot
    tab[staleSlot].value = null;//将value置空了,可以回收掉了
    tab[staleSlot] = null;//Entry也置空了
    size--;

    // Rehash until we encounter null
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;//循环,清理staleSlot和下一个数组中的空位置之间的所有陈旧项目
         i = nextIndex(i, len)) {
        ThreadLocal k = e.get();
        if (k == null) {//碰到key为空的,清一清
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {//不相等,表示存在hash冲突。由于之前的位置可能已经被清理,当前i位置的Entry需要移一下位置
                tab[i] = null;//将数组中的引用置空。e中保存了对象

                // Unlike Knuth 6.4 Algorithm R, we must scan until
                // null because multiple entries could have been stale.
                while (tab[h] != null)//从第一个hash位置开始找起,直到找到数组中的空位置
                    h = nextIndex(h, len);
                tab[h] = e;//将Entry放入数组
            }
        }
    }
    return i;//返回的i的索引是数组中的一个空位置
}

最后看一下存储的条目超过阈值,数组需要扩容的处理

private void rehash() {
    expungeStaleEntries();//清除陈旧条目,这个会清理整个数组
    //清理完之后,size会变小
    // Use lower threshold for doubling to avoid hysteresis
    if (size >= threshold - threshold / 4)//清理之后条目还是很多,超过阈值的话,数组就真的要扩容了
        resize();
}

清除整个数组的陈旧条目的方法expungeStaleEntries()

private void expungeStaleEntries() {
    Entry[] tab = table;
    int len = tab.length;
    for (int j = 0; j < len; j++) {//遍历整个数组
        Entry e = tab[j];
        if (e != null && e.get() == null)
            expungeStaleEntry(j);//这个方法?上面见过
    }
}

resize()方法,将数组的容量加倍

private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];
    int count = 0;

    for (int j = 0; j < oldLen; ++j) {//遍历旧数组
        Entry e = oldTab[j];//取出旧数组元素
        if (e != null) {
            ThreadLocal k = e.get();
            if (k == null) {//又碰到陈陈旧项目了?清理掉
                e.value = null; // Help the GC
            } else {
                int h = k.threadLocalHashCode & (newLen - 1);
                while (newTab[h] != null)//hash冲突的时候往后面的位置放
                    h = nextIndex(h, newLen);
                newTab[h] = e;
                count++;
            }
        }
    }

    setThreshold(newLen);//设置新阈值
    size = count;
    table = newTab;
}

get方法

    public T get() {
        Thread t = Thread.currentThread();//获取当前线程
        ThreadLocalMap map = getMap(t);//获取线程对象的threadLocals
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);//用ThreadLocal从map中获取Entry对象
            if (e != null) {//有对象的话,返回Entry中的value,就是我们保存的对象。
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();//设置初始值
    }

看下map.getEntry(this)

private Entry getEntry(ThreadLocal key) {
    int i = key.threadLocalHashCode & (table.length - 1);//计算直接下标
    Entry e = table[i];
    if (e != null && e.get() == key)//如果直接计算出的下标能找到key对应的,就返回
        return e;
    else//为什么会找不到呢?因为存储方式和HashMap不同,这个是往数组后面的空位置存的,而不是构建一个链表
        return getEntryAfterMiss(key, i, e);
}

看下getEntryAfterMiss(key, i, e)

private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
    //往后找,直到碰到数组的位置为空了还没找到,就是真没有了
    while (e != null) {//e不为空的话,遍历数组找key对应的
        ThreadLocal k = e.get();
        if (k == key)//找到key相等的,返回
            return e;
        if (k == null)//key为空,清除陈旧条目
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);//计算出下一个索引
        e = tab[i];
    }
    return null;//如果e为空的话,那就是没有存入对象
}

remove方法

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

看下m.remove(this);

private void remove(ThreadLocal key) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);//计算索引
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {//遍历数组找
        if (e.get() == key) {//找到key相等的了
            e.clear();//将指向key的应用referent置空
            expungeStaleEntry(i);//清除陈旧条目
            return;
        }
    }
}

一些思考

1.我们在使用的时候,用完了对象一定要记得用remove()方法清理一下。

虽然Entry保存的key是弱引用,当我们不用ThreadLocal的时候,对象就会被回收。key是被回收了,但是value是一个强引用,不会被回收,可能就会造成内存泄漏的危险。

2.为什么有了remove()方法做清理了,还要设计成弱引用呢?

我觉得这是为了兜底。假如我们忘记了remove,仅仅把我们用到的ThreadLocal--A置为null了,我们代码中可能还有其它的ThreadLocal--B,调用B的get,set,remove方法的时候,也会对ThreadLocalMap里的数组进行废弃项的清除。那什么是废弃的呢,key是null的就是废弃的,弱引用保证了key可以变成null。这样设计最大程度减少了内存泄漏的可能性。

最可怕的是:我们用了线程池中的线程,用完ThreadLocal之后忘记了remove,只将ThreadLocal回收了。而线程一直存活在线程池中,那个Entry里的value所指向的对象也一直回收不掉。

3.initialValue()方法

在ThreadLocal源代码中,这个方法是返回null的。这个方法需要我们自己创建子类去覆写,return我们自己需要的对象。

    protected T initialValue() {
        return null;
    }

一开始没有设置值,就去get的话,就会调用这个方法,往ThreadLocal里设值。remove之后再调用get方法,也会去调用initialValue方法设值。

你可能感兴趣的:(聊聊ThreadLocal)