1.threadLocal

ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。
每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用, 其它 Thread 不可访问 , 那就不存在多线程间共享的问题。这也是 ThreadLocal 命名的由来。

  1. public static void main(String[] args) {
  2. //初始化ThreadLocal对象的时候,会重写方法initialValue(),这样就不会发生get方法返回值为null的情况。
  3. ThreadLocal<Integer> value = ThreadLocal.withInitial(() -> {
  4. return 0;
  5. });
  6. new Thread(() -> {
  7. value.set(10);
  8. //Thread-0 10
  9. System.out.println(Thread.currentThread().getName() + " " + value.get());
  10. }).start();
  11. new Thread(() -> {
  12. //Thread-1 0
  13. System.out.println(Thread.currentThread().getName() + " " + value.get());
  14. value.set(3);
  15. //Thread-1 3
  16. System.out.println(Thread.currentThread().getName() + " " + value.get());
  17. }).start();
  18. }

image.png

2.底层原理

ThreadLocal内有ThreadLocalMap内部类。
一个线程内可以存在多个 ThreadLocal 对象,每个线程都有一个ThreadLocalMap,保存了当前自己所在线程的所有“局部变量”。

set方法

public void set(T value) {
        Thread t = Thread.currentThread();//获取当前线程实例
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);//以当前ThreadLocal实例对象为key,传入的value值为value
        else
            createMap(t, value);//
    }

//获取当前线程实例的threadLocals属性,保存了当前自己所在线程的所有局部变量。
ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

// Thread类中声明的threadLocals变量
ThreadLocal.ThreadLocalMap threadLocals = null;

//创建一个ThreadLocalMap实例对象,并以当前ThreadLocal实例对象为key,传入的value值为value
void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

get方法

public T get() {
        Thread t = Thread.currentThread();//获取当前线程
        ThreadLocalMap map = getMap(t);//获取当前线程所维护的ThreadLocalMap实例
        if (map != null) {
            //以当前ThreadLocal对象实例为key从ThreadLocalMap中获取所需要的值
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

/**初始化一个ThreadLocalMap,并以当前ThreadLocal实例对象为key,
null值为value存到这个ThreadLocalMap中,同时返回null。*/
private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }
//此方法可能被重写
protected T initialValue() {
        return null;
    }

remove方法

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

ThreadLocalMap类

ThreadLocalMap构造方法

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];//初始数组,长度INITIAL_CAPACITY=16
            //计算当前第一个元素应该所处的位置,根据ThreadLocal的不可变实例变量 threadLocalHashCode (作为hash值)        
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);//直接放置当前键值对
            size = 1;    //元素个数
            setThreshold(INITIAL_CAPACITY);    //初始化扩容阈值
        }
//设置对table数组进行扩容的阈值。
private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }

getEntry方法

//ThreadLocalMap的方法,根据key(threadlocal实例)获取entry
private Entry getEntry(ThreadLocal<?> key) {
    //根据hash值确定元素所处位置
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];

    //没有hash冲突,并且当前entry没有过期,一次就找到
    if (e != null && e.get() == key)
        return e;
    else
        //hash冲突或则该位置[e.get()==null(表示entry已过期)]
        return getEntryAfterMiss(key, i, e);
}

//参数一:当前ThreadLocal实例
//参数二:当前ThreadLocal实例根据hash值计算出来的位置i
//参数三:位置i上的entry
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    //循环进入条件是e!=null,为什么不考虑hash冲突,e==null,且当前查找的元素在后面?
    //答:因为每次清除过期entry的时候,或则remove的时候都进行了数据修正归位置,将后面的数据移到正确的位置上去
    while (e != null) {

        ThreadLocal<?> k = e.get();

        //是当前要找的entry
        if (k == key)
            return e;

        //遍历到的entry已经过期,则对该位置进行探测式整理
        if (k == null)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);

        //继续迭代
        e = tab[i];
    }
    return null;
}

set方法

private void set(ThreadLocal<?> key, Object value) {

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

    //e!=null, 遍历(可能hash冲突)
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {

        ThreadLocal<?> k = e.get();

        //找到保存的位置,直接替换
        if (k == key) {
            e.value = value;
            return;
        }

        //遍历到的位置过期
        if (k == null) {
            //替换过期的entry为新的键值对entry
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    //没有hash冲突,直接放在位置i
    tab[i] = new Entry(key, value);
    int sz = ++size;

    //清理过期元素,检查容量
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

remove方法

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)]) {

        //找到键值对entry
        if (e.get() == key) {

            //清除弱引用(this.referent = null; 置为过期),及entry.get()会等于null
            e.clear();

            //从当前位置i(当前位置的e.get()==null),开始整理元素(清理过期的,移动未过期的到正确位置)
            expungeStaleEntry(i);
            return;
        }
    }
}

replaceStaleEntry方法

private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;

    //从已经确定过期位置向前查找过期元素,以便于扩大清理范围
    int slotToExpunge = staleSlot;
    for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)){
        if (e.get() == null){
            slotToExpunge = i;
        }
    }

    //向后遍历(staleSlot+1开始),查找该元素是否存在
    for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {

        ThreadLocal<?> k = e.get();

        //该键值对已经存在,直接替换value
        if (k == key) {
            e.value = value;

            //将过期元素往后放(i)
            tab[i] = tab[staleSlot];

            //将当前entry往前放(归位)
            tab[staleSlot] = e;

            //除了位置staleSlot位置,未找到其他位置过期,则将开始清理位置改为i(缩小清理范围)
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;

            //该方法会返回清理到的null的位置
            int nullIndex = expungeStaleEntry(slotToExpunge);

            //从nullIndex的下一个位置,简单清理几次(len=2^x,即清理x+1此)
            cleanSomeSlots(nullIndex, len);
            return;
        }

        //位置entry过期,若之前未扫描到其他过期的entry,重置过期扫描起始位置为i
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }

    //走到这里:遍历没有找到当前键值对entry,则将元素放在当前过期的位置(替换)
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

    //进行到这里:先确定遍历有没有扫描到其他过期的entry
    if (slotToExpunge != staleSlot){
        //清理
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
    }
}

cleanSomeSlots方法

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];

        //清理过期entry
        if (e != null && e.get() == null) {
            n = len;
            removed = true;

            //探测式清理
            i = expungeStaleEntry(i);
        }
    } while ( (n >>>= 1) != 0);

    //确定是否有清楚成功
    return removed;
}

expungeStaleEntry方法

该方法从一个过期的元素位置开始,并向后移动扫描,遇到过期元素就清除,遇到还处于正常状态的元素对其进行rehash操作,将正常元素移动到正确的位置上去,直到遇到null位置停止扫描,并返回null位置的索引下标。

//参数:staleSlot 该位置的entry已经过期
//核心方法,探测式整理(清理加归位处理)
//扫描从staleSlot位置开始的entry,直到遇到第一个null为止,[及staleSlot与null之间的元素]
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();

        //过期的entry直接清除
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;

        } else {

            //没过期的entry进行rehash,归位
            int h = k.threadLocalHashCode & (len - 1);

            //元素不在正确的位置上(有hash冲突)
            if (h != i) {
                //置空当前位置i
                tab[i] = null;

                //从正确位置开始查找(新位置可能hash冲突),找到空位
                while (tab[h] != null)
                    h = nextIndex(h, len);

                //新的位置
                tab[h] = e;
            }
        }
    }
    return i;
}
初始情况如图


image.png
假设调用expungeStaleEntry时staleSlot=3
image.png
清除位置i=3处的元素:
image.png
继续向后扫描
image.png
发现i=4处的元素正常,并且 int h = k.threadLocalHashCode & (len - 1);得出h=3,说明加入该元素时有hash冲突,需要对该元素归位到位置3处。
image.png
继续向后扫描到i=5,发现该元素过期,直接清除
image.png
继续向后扫描i=6,发现该元素E6正常,并且 int h = k.threadLocalHashCode & (len - 1); 得出h=3,需要移动该元素,但是i=3处已经有元素,所以需要往3之后放置到i=4处。
image.png
继续向后扫描i=7,发现该元素正常,并且 int h = k.threadLocalHashCode & (len - 1); 得出h=7,不需要移动
image.png
继续向后扫描 i=8,发现该位置为null,退出循环,扫描结束,返回该位置的位置索引
image.png

3.内存泄漏

原因

每一个Thread维护一个ThreadLocalMap映射表,映射表的key是ThreadLocal实例,并且使用的是ThreadLocal的弱引用 ,value是具体需要存储的Object。
image.png
如果ThreadLocal没有外部强引用,当发生垃圾回收时,这个ThreadLocal一定会被回收(弱引用的特点是不管当前内存空间足够与否,GC时都会被回收),这样就会导致ThreadLocalMap中出现key为null的Entry,外部将不能获取这些key为null的Entry的value,并且如果当前线程一直存活,那么就会存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,导致value对应的Object一直无法被回收,产生内存泄露。

解决

每次使用完ThreadLocal都调用它的remove()方法清除数据,或者按照JDK建议将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉。

为什么设计弱引用

假设map中的引用是强引用,那么由于map中依然有这个对象的引用,那么这个对象不能够被GC回收。

子线程中获取主线程的值

主线程中创建InheritableThreadLocal实例对象,ThreadLocal threadLocal = new InheritableThreadLocal()。

原理

InheritableThreadLocal继承了ThreadLocal,并且重写了childValue、getMap和createMap方法。
通过new Thread()方式创建子线程时,进入如下代码。

public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize) {
        init(g, target, name, stackSize, null, true);
    }

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name;

        Thread parent = currentThread();
        SecurityManager security = System.getSecurityManager();
        if (g == null) {
            if (security != null) {
                g = security.getThreadGroup();
            }
            if (g == null) {
                g = parent.getThreadGroup();
            }
        }
        g.checkAccess();
        if (security != null) {
            if (isCCLOverridden(getClass())) {
                security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
            }
        }

        g.addUnstarted();

        this.group = g;
        this.daemon = parent.isDaemon();
        this.priority = parent.getPriority();
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;
        this.inheritedAccessControlContext =
                acc != null ? acc : AccessController.getContext();
        this.target = target;
        setPriority(priority);
    // 如果父线程的inheritableThreadLocals不为null
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
             // 通过createInheritedMap方法将父线程中inheritableThreadLocals的值复制到子线程的inheritableThreadLocals中
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        this.stackSize = stackSize;
        tid = nextThreadID();
    }

使用场景

存储数据库连接

参考

InheritableThreadLocal