【Java并发编程】——ThreadLocal的深入解析与使用,以及内存泄漏问题

目录

什么是ThreadLocal?

如何使用ThreadLocal?

内存泄漏

为什么会导致内存泄漏?

如何避免内存泄漏?

源码解析:

void set()方法

T get()方法

void remove()方法

InheritableThreadLocal类


什么是ThreadLocal?

ThreadLocal,意为线程本地变量

ThreadLocal是JDK包提供的,它提供了线程本地变量,也就是如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量时,实际操作的是自己本地内存里面的变量,从而避免了线程安全问题。

如何使用ThreadLocal?

我们先看下面这段代码

public class ThreadTest{

    // 创建ThreadLocal变量
    static ThreadLocal<String> localVariable = new ThreadLocal<>();

    static void print(String str) {
        // 打印当前线程副本变量的值
        System.out.println(str + ":" + localVariable.get());
       
    }
    public static void main(String[] args) {
        // 创建线程1
        Thread threadOne = new Thread(new Runnable() {
            @Override
            public void run() {
                localVariable.set("this is threadOne local variable");
                print("threadOne");
                System.out.println("threadOne remove after:" + localVariable.get());
            }
        });
        // 创建线程2
        Thread threadTwo = new Thread(new Runnable() {
            @Override
            public void run() {
                localVariable.set("this is threadTwo local variable");
                print("threadTwo");
                System.out.println("threadTwo remove after:" + localVariable.get());
            }
        });
        // 开启线程
        threadOne.start();
        threadTwo.start();
    }
}

 

通过运行结果,我们可以看出,两个线程通过调用print方法都打印出了当前线程副本变量的值,然后又在run()方法中获取到了当前线程的副本变量的值。

接下来我们在print()方法中添加一条语句,也就是输出完之后就将副本变量删除掉

 // 清除当前线程副本变量
localVariable.remove();

 然后再来看一下运行结果

 

我们可以发现,在run()方法中再次获取localVariable变量的值时,已经获取不到了,因为那个副本变量已经在当前线程中删除掉了。

 

内存泄漏

为什么会导致内存泄漏?

ThreadLocalMap 使用 ThreadLocal 的弱引用作为key,如果一个 ThreadLocal 没有外部强引用来引用它,那么系统 GC 的时候,这个 ThreadLocal 就会被回收,由此一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry,也就没有办法再去访问对应的 value,如果当前线程再迟迟不结束的话,这些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value 永远无法回收,然后下次再使用的时候则会继续创建新的,越来越多,最终造成内存泄漏。

如何避免内存泄漏?

为了避免内存泄漏,当不需要使用本地变量时可以通过调用ThreadLocal变量的remove方法,从当前线程的threadLocals里面删除该本地变量,如果一直不删除,最终会导致内存泄漏。 

源码解析:

Thread类中有一个threadLocals和inheritableThreadLocals,它们都是ThreadLocalMap类型的变量,而ThreadLocalMap是一个特殊的HashMap

void set()方法

(在当前线程中,设置一个变量的副本)

先获取当前线程,然后根据当前线程作为key,获取到对应线程的变量,如果map不为空就为这个map赋值,否则就创建一个新的变量,并为其赋值

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

getMap()源码如下:

就是获取对应线程自己的变量threadLocals

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

createMap()源码如下:

创建一个新的threadLocals变量

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

T get()方法

(获取当前线程中副本变量的值)

首先获取当前线程,然后根据当前线程作为key,获取到对应线程的ThreadLocals变量,如果变量不为空,则返回当前线程绑定的本地变量,否则执行setInitialValue()

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

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

setInitialValue()源码如下:

先设置一个value,初始化为null,然后获取当前线程,在获取当前线程的threadLocals变量,如果变量不为空,则设置变量的值为value(也就是null),否则就新建一个threadLocals变量。

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

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

InheritableThreadLocal类

ThreadLocal不支持继承性:也就是说,同一个ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到的,那么怎样才能在子线程也能获取到呢?这时就需要使用InheritableThreadLocal类了。

  • InheritableThreadLocal继承自ThreadLocal,其提供了一个特性,就是让子线程可以访问在父线程中设置的本地变量。
  • 当父线程创建子线程时,构造函数会把父线程中inheritableThreadLocals变量里面的本地变量复制一份保存到子线程的inheritableThreadLocals变量里面。

 例:

public class ThreadTest{

    // 创建ThreadLocal变量
    static ThreadLocal<String> localVariable = new ThreadLocal<>();

    public static void main(String[] args) {
        // 在主线程中设置变量的值
        localVariable.set("Hello Java");
        // 创建线程1
        Thread threadOne = new Thread(new Runnable() {
            @Override
            public void run() {
                // 子线程输出变量的值
                System.out.println("threadOne:" + localVariable.get());
            }
        });
        // 开启线程
        threadOne.start();
        // 主线程输出变量的值
        System.out.println("main:" + localVariable.get());
    }
}

输出结果如下 :

main:Hello Java
threadOne:null

我们可以看到,主线程中设置了变量的值,在子线程中是获取不到的

然后我们修改上面代码中创建变量的方法:

// 创建ThreadLocal变量
static ThreadLocal<String> localVariable = new InheritableThreadLocal<>();

接下来再运行一遍,看一下结果:

main:Hello Java
threadOne:Hello Java

可以看到,子线程中也获取到了变量的值

因为在父线程创建子线程时,构造函数会把父线程中inheritableThreadLocals变量里面的副本变量复制一份保存到子线程的inheritableThreadLocals变量里面。

注意:ThreadLocal使用完后,一定要删除,否则会造成内存泄漏,平时的时候可能察觉不到,但当数据量很大的时候,就会出现内存泄漏的情况,而且也会影响业务逻辑。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章