java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Java ThreadLocal

Java中关于ThreadLocal的隐式引用详解

作者:程序猿进阶

这篇文章主要介绍了Java中关于ThreadLocal的隐式引用,从线程的角度看,每个线程都保持一个对其线程局部变量副本的隐式引用,只要线程是活动的,ThreadLocal实例就是可访问的,下面我们来具体看看

前言

ThreadLocal 并不是一个Thread,而是 ThreadLocalVariable(线程局部变量)。也许把它命名为 ThreadLocalVar更加合适。线程局部变量就是为每一个使用该变量的线程都提供一个变量值的副本,是 Java中一种较为特殊的线程绑定机制,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。ThreadLocal是除了加锁这种同步方式之外的另一种保证多线程访问出现线程不安全的方式。

从线程的角度看,每个线程都保持一个对其线程局部变量副本的隐式引用,只要线程是活动的,ThreadLocal实例就是可访问的;在线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)。

通过ThreadLocal存取的数据,总是与当前线程相关,也就是说,JVM 为每个运行的线程,绑定了私有的本地实例存取空间,从而为多线程环境常出现的并发访问问题提供了一种隔离机制。在 ThreadLocal类中有一个 Map,用于存储每一个线程变量的副本,Map中元素的键为线程对象,而值为对应线程的变量副本。ThreadLocal采用了 “以空间换时间” 的方式。为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

一、API说明

在程序中一般都重写 initialValue方法,以给定一个特定的初始值。

二、ThreadLocal简单使用

下面的例子中,开启两个线程,在每个线程内部设置了本地变量的值,然后调用 print方法打印当前本地变量的值。如果在打印之后调用本地变量的 remove方法会删除本地内存中的变量,代码如下所示:

package test;
public class ThreadLocalTest {
    static ThreadLocal<String> localVar = new ThreadLocal<>();
    static void print(String str) {
        //打印当前线程中本地内存中本地变量的值
        System.out.println(str + " :" + localVar.get());
        //清除本地内存中的本地变量
        localVar.remove();
    }
    public static void main(String[] args) {
        Thread t1  = new Thread(new Runnable() {
            @Override
            public void run() {
                //设置线程1中本地变量的值
                localVar.set("localVar1");
                //调用打印方法
                print("thread1");
                //打印本地变量
                System.out.println("after remove : " + localVar.get());
            }
        });
        Thread t2  = new Thread(new Runnable() {
            @Override
            public void run() {
                //设置线程1中本地变量的值
                localVar.set("localVar2");
                //调用打印方法
                print("thread2");
                //打印本地变量
                System.out.println("after remove : " + localVar.get());
            }
        });
        t1.start();
        t2.start();
    }
}

下面是运行后的结果:

thread1 :localVar1
after remove : null
thread2 :localVar2
after remove : null

三、ThreadLocal的实现原理

下面是 ThreadLocal的类图结构,从图中可知:Thread类中有两个变量 threadLocals和 inheritableThreadLocals,二者都是 ThreadLocal内部类 ThreadLocalMap类型的变量,我们通过查看内部内 ThreadLocalMap可以发现实际上它类似于一个HashMap。在默认情况下,每个线程中的这两个变量都为null。

ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

只有当线程第一次调用 ThreadLocal的 set或者 get方法的时候才会创建他们(后面我们会查看这两个方法的源码)。除此之外,每个线程的本地变量不是存放在 ThreadLocal实例中,而是放在调用线程的 ThreadLocals变量里面(前面也说过,该变量是Thread类的变量)。也就是说,ThreadLocal类型的本地变量是存放在具体的线程空间上,其本相当于一个装载本地变量的工具壳,通过 set方法将 value添加到调用线程的 threadLocals中,当调用线程调用 get方法时候能够从它的 threadLocals中取出变量。如果调用线程一直不终止,那么这个本地变量将会一直存放在他的 threadLocals中,所以不使用本地变量的时候需要调用 remove方法将 threadLocals中删除不用的本地变量。下面我们通过查看 ThreadLocal的set、get以及remove方法来查看 ThreadLocal具体实怎样工作的。

set方法源码

public void set(T value) {
    //(1)获取当前线程(调用者线程)
    Thread t = Thread.currentThread();
    //(2)以当前线程作为key值,去查找对应的线程变量,找到对应的map
    ThreadLocalMap map = getMap(t);
    //(3)如果map不为null,就直接添加本地变量,key为当前线程,值为添加的本地变量值
    if (map != null)
        map.set(this, value);
    //(4)如果map为null,说明首次添加,需要首先创建出对应的map
    else
        createMap(t, value);
}

在上面的代码中,(2)处调用 getMap方法获得当前线程对应的 threadLocals(参照上面的图示和文字说明),该方法代码如下:

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

如果调用 getMap方法返回值不为 null,就直接将 value值设置到 threadLocals中(key为当前线程引用,值为本地变量);如果getMap方法返回 null说明是第一次调用 set方法(前面说到过,threadLocals默认值为null,只有调用 set方法的时候才会创建map),这个时候就需要调用 createMap方法创建 threadLocals,该方法如下所示:createMap方法不仅创建了threadLocals,同时也将要添加的本地变量值添加到了 threadLocals中。

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

get方法源码: 在 get方法的实现中,首先获取当前调用者线程,如果当前线程的 threadLocals不为null,就直接返回当前线程绑定的本地变量值,否则执行 setInitialValue方法初始化 threadLocals变量。在 setInitialValue方法中,类似于 set方法的实现,都是判断当前线程的 threadLocals变量是否为null,是则添加本地变量(这个时候由于是初始化,所以添加的值为null),否则创建 threadLocals变量,同样添加的值为null。

public T get() {
    //(1)获取当前线程
    Thread t = Thread.currentThread();
    //(2)获取当前线程的threadLocals变量
    ThreadLocalMap map = getMap(t);
    //(3)如果threadLocals变量不为null,就可以在map中查找到本地变量的值
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    //(4)执行到此处,threadLocals为null,调用该更改初始化当前线程的threadLocals变量
    return setInitialValue();
}
private T setInitialValue() {
    //protected T initialValue() {return null;}
    T value = initialValue();
    //获取当前线程
    Thread t = Thread.currentThread();
    //以当前线程作为key值,去查找对应的线程变量,找到对应的map
    ThreadLocalMap map = getMap(t);
    //如果map不为null,就直接添加本地变量,key为当前线程,值为添加的本地变量值
    if (map != null)
        map.set(this, value);
    //如果map为null,说明首次添加,需要首先创建出对应的map
    else
        createMap(t, value);
    return value;
}

remove方法的实现: remove方法判断当前线程对应的 threadLocals变量是否为null,不为 null就直接删除当前线程中指定的 threadLocals变量。

public void remove() {
    //获取当前线程绑定的threadLocals
     ThreadLocalMap m = getMap(Thread.currentThread());
     //如果map不为null,就移除当前线程中指定ThreadLocal实例的本地变量
     if (m != null)
         m.remove(this);
}

如下图所示: 每个线程内部有一个名为 threadLocals的成员变量,该变量的类型为 ThreadLocal.ThreadLocalMap类型(类似于一个HashMap),其中的 key为当前定义的 ThreadLocal变量的 this引用,value为我们使用 set方法设置的值。每个线程的本地变量存放在自己的本地内存变量 threadLocals中,如果当前线程一直不消亡,那么这些本地变量就会一直存在(可能会导致内存溢出),因此使用完毕需要将其 remove掉。

四、ThreadLocal不支持继承性

同一个 ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到的。(threadLocals 中为当前调用线程对应的本地变量,所以二者自然是不能共享的)

package test;
public class ThreadLocalTest2 {
    //(1)创建ThreadLocal变量
    public static ThreadLocal<String> threadLocal = new ThreadLocal<>();
    public static void main(String[] args) {
        //在main线程中添加main线程的本地变量
        threadLocal.set("mainVal");
        //新创建一个子线程
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("子线程中的本地变量值:"+threadLocal.get());
            }
        });
        thread.start();
        //输出main线程中的本地变量值
        System.out.println("mainx线程中的本地变量值:"+threadLocal.get());
    }
}

五、InheritableThreadLocal类

在上面说到的 ThreadLocal类是不能提供子线程访问父线程的本地变量的,而 InheritableThreadLocal类则可以做到这个功能,下面是该类的源码

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    protected T childValue(T parentValue) {
        return parentValue;
    }
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

从上面代码可以看出,InheritableThreadLocal类继承了 ThreadLocal类,并重写了childValue、getMap、createMap三个方法。其中 createMap方法在被调用(当前线程调用 set方法时得到的 map为 null的时候需要调用该方法)的时候,创建的是inheritableThreadLocal而不是 threadLocals。同理,getMap方法在当前调用者线程调用 get方法的时候返回的也不是threadLocals 而是 inheritableThreadLocal。下面我们看看重写的 childValue方法在什么时候执行,怎样让子线程访问父线程的本地变量值。我们首先从 Thread类开始说起

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;
    //(1)获取当前线程(父线程)
    Thread parent = currentThread();
    //安全校验
    SecurityManager security = System.getSecurityManager();
    if (g == null) { //g:当前线程组
        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);
    //(2)如果父线程的inheritableThreadLocal不为null
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        //(3)设置子线程中的inheritableThreadLocals为父线程的inheritableThreadLocals
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    this.stackSize = stackSize;
    tid = nextThreadID();
}

在 init方法中,首先(1)处获取了当前线程(父线程),然后(2)处判断当前父线程的 inheritableThreadLocals是否为null,然后调用createInheritedMap将父线程的 inheritableThreadLocals作为构造函数参数创建了一个新的 ThreadLocalMap变量,然后赋值给子线程。下面是 createInheritedMap方法和 ThreadLocalMap的构造方法

static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
    return new ThreadLocalMap(parentMap);
}
private ThreadLocalMap(ThreadLocalMap parentMap) {
    Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    setThreshold(len);
    table = new Entry[len];
    for (int j = 0; j < len; j++) {
        Entry e = parentTable[j];
        if (e != null) {
            @SuppressWarnings("unchecked")
            ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
            if (key != null) {
                //调用重写的方法
                Object value = key.childValue(e.value);
                Entry c = new Entry(key, value);
                int h = key.threadLocalHashCode & (len - 1);
                while (table[h] != null)
                    h = nextIndex(h, len);
                table[h] = c;
                size++;
            }
        }
    }
}

在构造函数中将父线程的 inheritableThreadLocals成员变量的值赋值到新的 ThreadLocalMap对象中。返回之后赋值给子线程的inheritableThreadLocals。总之,InheritableThreadLocals类通过重写getMap和createMap两个方法将本地变量保存到了具体线程的inheritableThreadLocals变量中,当线程通过InheritableThreadLocals实例的set或者get方法设置变量的时候,就会创建当前线程的inheritableThreadLocals变量。而父线程创建子线程的时候,ThreadLocalMap中的构造函数会将父线程的inheritableThreadLocals中的变量复制一份到子线程的inheritableThreadLocals变量中。

六、从ThreadLocalMap看ThreadLocal使用不当的内存泄漏问题

基础概念 : 首先我们先看看 ThreadLocalMap的类图,在前面的介绍中,我们知道 ThreadLocal只是一个工具类,他为用户提供get、set、remove接口操作实际存放本地变量的 threadLocals(调用线程的成员变量),也知道 threadLocals是一个ThreadLocalMap类型的变量,下面我们来看看 ThreadLocalMap这个类。在此之前,我们回忆一下 Java中的四种引用类型链接

分析 ThreadLocalMap内部实现: 上面我们知道 ThreadLocalMap内部实际上是一个 Entry数组 private Entry[] table ​,我们先看看 Entry的这个内部类

/**
 * 是继承自WeakReference的一个类,该类中实际存放的key是指向ThreadLocal的弱引用和与之对应的value值(该value值
 * 就是通过ThreadLocal的set方法传递过来的值)由于是弱引用,当get方法返回null的时候意味着回收引用
 */
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** value就是和ThreadLocal绑定的 */
    Object value;
    //k:ThreadLocal的引用,被传递给WeakReference的构造方法
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}
//WeakReference构造方法(public class WeakReference<T> extends Reference<T> )
public WeakReference(T referent) {
    super(referent); //referent:ThreadLocal的引用
}
//Reference构造方法
Reference(T referent) {
    this(referent, null);//referent:ThreadLocal的引用
}
Reference(T referent, ReferenceQueue<? super T> queue) {
    this.referent = referent;
    this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}

在上面的代码中,我们可以看出,当前 ThreadLocal的引用 k被传递给 WeakReference的构造函数,所以 ThreadLocalMap中的key为 ThreadLocal的弱引用。当一个线程调用ThreadLocal的 set方法设置变量的时候,当前线程的 ThreadLocalMap就会存放一个记录,这个记录的 key值为 ThreadLocal的弱引用,value就是通过 set设置的值。如果当前线程一直存在且没有调用该ThreadLocal的 remove方法,如果这个时候别的地方还有对 ThreadLocal的引用,那么当前线程中的 ThreadLocalMap中会存在对ThreadLocal变量的引用和 value对象的引用,是不会释放的,就会造成内存泄漏。

考虑这个 ThreadLocal变量没有其他强依赖,如果当前线程还存在,由于线程的 ThreadLocalMap里面的 key是弱引用,所以当前线程的 ThreadLocalMap里面的 ThreadLocal变量的弱引用在 gc的时候就被回收,但是对应的 value还是存在的这就可能造成内存泄漏(因为这个时候 ThreadLocalMap会存在key为 null但是 value不为 null的 entry项 )。

ThreadLocalMap 中的 Entry的 key使用的是 ThreadLocal对象的弱引用,在没有其他地方对 ThreadLoca依赖,ThreadLocalMap中的 ThreadLocal对象就会被回收掉,但是对应的 value不会被回收,这个时候 Map中就可能存在 key为null但是 value不为null的项,这需要实际使用的时候使用完毕及时调用 remove方法避免内存泄漏。

如果使用线程池,由于线程可能并不是真正的关闭(比如newFixedThreadPool会保持线程一只存活)。因此,如果将一些大对象存放到 ThreadLocalMap中,可能会造成内存泄漏。因为线程没有关闭,无法回收,但是这些对象不会再被使用了。如果希望及时回收对象,则可以使用Thread.remove()方法将变量移除。

以上就是Java中关于ThreadLocal的隐式引用详解的详细内容,更多关于Java ThreadLocal的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文