天天看点

二刷Java多线程:ThreadLocal详解

1、ThreadLocal简介

ThreadLocal是一个以ThreadLocal对象为键、任意对象为值的存储结构,提供了线程本地变量,也就是如果创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量时,实际操作的是自己本地内存里面的变量,从而避免了线程安全问题。创建一个ThreadLocal变量后,每个线程都会复制一个变量到自己的本地内存

二刷Java多线程:ThreadLocal详解

ThreadLocal的内部结构图如下:

二刷Java多线程:ThreadLocal详解

2、ThreadLocal使用示例

public class ThreadLocalTest {
    static void print(String str) {
        System.out.println(str + ":" + localVariable.get());
        localVariable.remove();
    }

    static ThreadLocal<String> localVariable = new ThreadLocal<String>();

    public static void main(String[] args) {
        new Thread(new Runnable() {
            public void run() {
                localVariable.set("threadOne local variable");
                print("threadOne");
                System.out.println("threadOne remove after :" + localVariable.get());
            }
        }).start();

        new Thread(new Runnable() {
            public void run() {
                localVariable.set("threadTwo local variable");
                print("threadTwo");
                System.out.println("threadTwo remove after :" + localVariable.get());
            }
        }).start();
    }
}
           

运行结果:

threadOne:threadOne local variable
threadOne remove after :null
threadTwo:threadTwo local variable
threadTwo remove after :null
           

3、ThreadLocal的实现原理

二刷Java多线程:ThreadLocal详解

Thread类中有一个threadLocals和一个inheritableThreadLocals,它们都是ThreadLocalMap类型的变量,而ThreadLocalMap是一个定制化的HashMap。在默认情况下,每个线程中的这两个变量都为null,只有当前线程第一次调用ThreadLocal的set()或者get()方法时才会创建它们。其实每个线程的本地变量不是存放在ThreadLocal实例里面,而是存放在调用线程的threadLocals变量里面。也就是说,ThreadLocal类型的本地变量存放在具体的线程内存空间中。ThreadLocal就是一个工具壳,它通过set()方法把value值放入调用线程的threadLocals里面并存放起来,当调用线程调用它的get()方法时,再从当前线程的threadLocals变量里面将其拿出来使用。如果调用线程一直不终止,那么这个本地变量会一直存放在调用线程的threadLocals变量里面,所以当不需要使用本地变量时可以通过调用ThreadLocal变量的remove()方法,从当前线程的threadLocals里面删除该本地变量

1)、void set(T value)

public void set(T value) {
        //获取当前线程
        Thread t = Thread.currentThread();
        //找到当前线程对应的threadLocals变量
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            //第一次调用就创建当前线程对应的threadLocals变量
            createMap(t, value);
    }

    ThreadLocalMap getMap(Thread t) {
        //获取线程自己的变量threadLocals,threadLocals变量被绑定到了线程的成员变量上
        return t.threadLocals;
    }

    void createMap(Thread t, T firstValue) {
        //创建当前线程的threadLocals变量
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
           

2)、T get()

public T get() {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取当前线程的threadLocals变量
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //如果threadLocals不为null,则返回对应本地变量的值
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //threadLocals为空则初始化当前线程的threadLocals成员变量
        return setInitialValue();
    }

    private T setInitialValue() {
        //初始化为null
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        //如果当前线程的threadLocals变量不为null
        if (map != null)
            map.set(this, value);
        //如果当前线程的threadLocals变量为null
        else
            createMap(t, value);
        return value;
    }

    protected T initialValue() {
        return null;
    }
           

3)、void remove()

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

如果当前线程的threadLocals变量不为空,则删除当前线程中指定ThreadLocal实例的本地变量

4)、ThreadLocalMap

ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也独立实现的

二刷Java多线程:ThreadLocal详解

在ThreadLocalMap中,也是用Entry来保存K-V结构数据的,但是Entry中key只能是ThreadLocal对象

static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
           

Entry继承自WeakReference(弱引用,生命周期只能存活到下次GC前),但只有key是弱引用类型的,value并非弱引用

private static final int INITIAL_CAPACITY = 16;

		ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }
           

从ThreadLocalMap的构造函数可以得知,ThreadLocalMap初始化容量为16,负载因子为2/3

和HashMap的最大的不同在于,ThreadLocalMap结构非常简单,没有next引用,也就是说ThreadLocalMap中解决Hash冲突的方式并非链表的方式,而是采用线性探测的方式。所谓线性探测,就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置

private void set(ThreadLocal<?> key, Object value) {
            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)]) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

		private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }
           

在插入过程中,根据ThreadLocal对象的hash值,定位到table中的位置i,过程如下

  • 如果当前位置是空的,那么正好,就初始化一个Entry对象放在位置i上
  • 位置i已有对象,如果这个Entry对象的key正好是即将设置的key,那么覆盖value
  • 位置i的对象,和即将设置的key没关系,那么只能找下一个空位置

5)、ThreadLocalMap内存泄露问题

二刷Java多线程:ThreadLocal详解

上图中,实线代表强引用,虚线代表的是弱引用,如果threadLocal外部强引用被置为null(

threadLocalInstance==null

)的话,threadLocal实例就没有一条引用链路可达,很显然在GC(垃圾回收)的时候势必会被回收,因此entry就存在key为null的情况,无法通过一个Key为null去访问到该entry的value。同时,就存在了这样一条引用链:

threadRef->currentThread->threadLocalMap->entry->valueRef->valueMemory

,导致在垃圾回收的时候进行可达性分析的时候,value可达从而不会被回收掉,但是该value永远不能被访问到,这样就存在了内存泄漏。当然,如果线程执行结束后,threadLocal和threadRef会断掉,因此threadLocal、threadLocalMap、entry都会被回收掉。可是,在实际使用中我们都是会用线程池去维护我们的线程,比如在

Executors.newFixedThreadPool()

时创建线程的时候,为了复用线程是不会结束的,所以threadLocal内存泄漏就值得我们关注

ThreadLocalMap的设计中已经做出了哪些改进?

ThreadLocalMap中的get()和set()方法都针对内存泄露问题做了相应的处理,下文为了叙述,针对key为null的entry,源码注释为stale entry,就称之为“脏entry”

ThreadLocalMap的set()方法:

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

            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)]) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }
           

在该方法中针对脏entry做了这样的处理:

  • 如果当前table[i]不为空的话说明hash冲突就需要向后环形查找,若在查找过程中遇到脏entry就通过replaceStaleEntry()进行处理
  • 如果当前table[i]为空的话说明新的entry可以直接插入,但是插入后会调用cleanSomeSlots()方法检测并清除脏entry

当我们调用threadLocal的get()方法时,当table[i]不是和所要找的key相同的话,会继续通过threadLocalMap的getEntryAfterMiss()方法向后环形去找

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);
        }

        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的时候,即遇到脏entry也会调用expungeStleEntry()对脏entry进行清理

当我们调用threadLocal.remove()方法时候,实际上会调用threadLocalMap的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)]) {
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }
           

同样的可以看出,当遇到了key为null的脏entry的时候,也会调用expungeStaleEntry()清理掉脏entry

从以上set()、get()、remove()方法看出,在ThreadLocal的生命周期里,针对ThreadLocal存在的内存泄漏的问题,都会通过expungeStaleEntry()、cleanSomeSlots()、replaceStaleEntry()这三个方法清理掉key为null的脏entry

想要更加深入学习ThreadLocal内存泄漏问题可以查看这篇文章

小结:

二刷Java多线程:ThreadLocal详解

在每个线程内部都有一个名为threadLocals的成员变量,该变量的类型为HashMap,其中key为我们定义的ThreadLocal变量的this引用,value则为我们使用set方法设置的值。每个线程的本地变量存放在线程自己的内存变量threadLocals中,如果当前线程一直不消亡,那么这些本地变量会一直存在,所以可能会造成内存溢岀,因 此使用完毕后要记得调用ThreadLocal的remove()方法删除对应线程的threadLocals中的本地变量

4、InheritableThreadLocal类

同一个ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到的。而子类InheritableThreadLocal提供了一个特性,就是让子线程可以访问在父线程中设置的本地变量

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    //(1)
    protected T childValue(T parentValue) {
        return parentValue;
    }

    //(2)
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }
    
	//(3)
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}
           

从上面InheritableThreadLocal的源码中可知,InheritableThreadLocal继承了ThreadLocal,并重写了三个方法。由代码(3)可知,InheritableThreadLocal重写了createMap()方法,那么现在当第一次调用set()方法时,创建的是当前线程的inheritableThreadLocals变量的实例而不再是threadLocals。由代码(2)可知,当调用get()方法获取当前线程内部的map变量时,获取的是inheritableThreadLocals而不再是threadLocals

综上可知,在InheritableThreadLocal的世界里,变量inheritableThreadLocals替代了threadLocals

下面我们看一下重写的代码(1)何时执行,以及如何让子线程可以访问父线程的本地变量。这要从创建Thread的代码说起,打开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)
			//设置子线程中的inheritableThreadLocals变量
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        
        this.stackSize = stackSize;

        tid = nextThreadID();
    }

    static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
        return new ThreadLocalMap(parentMap);
    }
           

在createlnheritedMap内部使用父线程的inheritableThreadLocals变量作为构造函数创建了一个新的ThreadLocalMap变量,然后赋值给了子线程的inheritableThreadLocals变量

小结:

InheritableThreadLocal类通过重写代码(2)和(3)让本地变量保存到了具体线程的inheritableThreadLocals变量里面,那么线程在通过InheritableThreadLocal类实例的set()或者get()方法设置变量时,就会创建当前线程的inheritableThreadLocals变量。当父线程创建子线程时,构造函数会把父线程中inheritableThreadLocals变量里面的本地变量复制一份保存到子线程的inheritableThreadLocals变量里面

参考:

https://www.jianshu.com/p/98b68c97df9b

https://blog.csdn.net/wsm0712syb/article/details/51025111

https://www.jianshu.com/p/dde92ec37bd1

继续阅读