ThreadLocal的原理并结合线程池使用(内存泄露)

一、ThreadLocal的定义

ThreadLocal顾名思义是线程私有的局部变量存储容器,可以理解成每个线程都有自己专属的存储容器,它用来存储线程私有变量,其实它只是一个外壳,内部真正存取是一个Map。每个线程可以通过set()和get()存取变量,多线程间无法访问各自的局部变量,相当于在每个线程间建立了一个隔板。只要线程处于活动状态,它所对应的ThreadLocal实例就是可访问的,线程被终止后,它的所有实例将被垃圾收集。总之记住一句话:ThreadLocal存储的变量属于当前线程。因此,通常对数据库连接(Connection)和事务(Transaction)使用线程本地存储。

二、ThreadLocal应用场景

  • 处理较为复杂的业务时,使用ThreadLocal代替参数的显示传递。
  • ThreadLocal可以用来做数据库连接池保存Connection对象,这样就可以让线程内多次获取到的连接是同一个了(Spring中的DataSource就是使用的ThreadLocal)。
  • 管理Session会话,将Session保存在ThreadLocal中,使线程处理多次处理会话时始终是一个Session。

三、ThreadLocal的原理

1、ThreadLocal存储过程

//  ThreadLocal中set方法
public void set(T value) {
    //  获取当前线程对象
    Thread t = Thread.currentThread();
    //  获取该线程的threadLocals属性(ThreadLocalMap对象)
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

//  Thread类中的threadLocals定义
ThreadLocal.ThreadLocalMap threadLocals = null;

//  ThreadLocal中getMap方法
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

//  ThreadLocal中createMap方法
//  为线程创建ThreadLocalMap对象并赋值给threadLocals
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

从源码中看到在set方法里面就是把值存入ThreadLocalMap类中,这个类是属于ThreadLocal的内部类,但是在Thread类中也有定义threadLocals变量,get方法的操作对象也是ThreadLocalMap,也就是说关键的存储和获取实质上在于ThreadLocalMap类。其中是以ThreadLocal类为key,存入的值为value,而ThreadLocal又是定义在每个线程的属性中,这也就实现了“ThreadLocal线程私有化”的功能,每次都是先从当前线程获取到threadLocals属性,也就是获得ThreadLocalMap对象,以ThreadLocal对象作为key存取对应的值

2、ThreadLocal对象的回收

//  ThreadLocal中的remove方法
public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

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

Entry是一个弱引用,是因为它不会影响到ThreadLocal的GC行为,如果是强引用的话,在线程运行过程中,我们不再使用ThreadLocal了,将ThreadLocal置为null,但ThreadLocal在线程的ThreadLocalMap里还有引用,导致其无法被GC回收。而Entry声明为WeakReference,ThreadLocal置为null后,线程的ThreadLocalMap就不算强引用了,ThreadLocal就可以被GC回收了。

四、ThreadLocal的内存泄露问题

ThreadLocalMap的生命周期是与线程一样的,但是ThreadLocal却不一定,可能ThreadLocal使用完了就想要被回收,但是此时线程可能不会立即终止,还会继续运行(比如线程池中线程重复利用),如果ThreadLocal对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。

如果ThreadLocal没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来ThreadLocalMap中使用这个ThreadLocal的key也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现key为null的value值。

在ThreadLocalMap中,调用 set()、get()、remove()方法的时候,会清理掉key为null的记录。在ThreadLocal设置为null之后,ThreadLocalMap中存在key为null的值,那么就可能发生内存泄漏,只有手动调用remove()方法来避免。

所以我们在使用完ThreadLocal变量时,尽量用threadLocal.remove()来清除,避免threadLocal=null的操作。remove方法是彻底地回收该对象,而通过threadLocal=null只是释放掉了ThreadLocal的引用,但是在ThreadLocalMap中却还存在其Entry,后续还需处理。

五、ThreadLocal与线程池结合使用需要注意的地方

线程池中的线程在任务执行完成后会被复用,所以在线程执行完成时,要对 ThreadLocal 进行清理(清除掉与本线程相关联的 value 对象)。不然,被复用的线程去执行新的任务时会使用被上一个线程操作过的 value 对象,从而产生不符合预期的结果。

先不做ThreadLocal清除,代码演示:

package com.calvin.currency.ThreadLocal;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadLocalVariableHolder {

    private static ThreadLocal<Integer> variableHolder = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };

    private static int getValue() {
        return variableHolder.get();
    }

    private static void increment() {
        variableHolder.set(variableHolder.get() + 1);
    }

    public static void main(String[] args) {
        ExecutorService threadPool = new ThreadPoolExecutor(2, 5,
                3, TimeUnit.SECONDS, new LinkedBlockingQueue<>(3),
                Executors.defaultThreadFactory(), new ThreadPoolExecutor.DiscardOldestPolicy());
        for (int i = 0; i < 10; i++) {
            threadPool.execute(() -> {
                try {
                    long threadId = Thread.currentThread().getId();
                    int before = getValue();
                    increment();
                    int after = getValue();
                    System.out.println("threadId: " + threadId + ", before: " + before + ", after: " + after);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }
        threadPool.shutdown();
    }
}

控制台结果输出:
在这里插入图片描述
可以看到,线程是被复用的。既然是为每个线程都提供一个副本,为什么会出现 before 不为 0 的情况呢?

那么,我们接着将ThreadLocal清除(remove方法),代码演示:

package com.calvin.currency.ThreadLocal;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadLocalVariableHolder {

    private static ThreadLocal<Integer> variableHolder = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };

    private static void remove() {
        variableHolder.remove();
    }

    private static int getValue() {
        return variableHolder.get();
    }

    private static void increment() {
        variableHolder.set(variableHolder.get() + 1);
    }

    public static void main(String[] args) {
        ExecutorService threadPool = new ThreadPoolExecutor(2, 5,
                3, TimeUnit.SECONDS, new LinkedBlockingQueue<>(3),
                Executors.defaultThreadFactory(), new ThreadPoolExecutor.DiscardOldestPolicy());
        for (int i = 0; i < 10; i++) {
            threadPool.execute(() -> {
                try {
                    long threadId = Thread.currentThread().getId();
                    int before = getValue();
                    increment();
                    int after = getValue();
                    System.out.println("threadId: " + threadId + ", before: " + before + ", after: " + after);
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    // 清理线程本地存储
                    remove();
                }
            });
        }
        threadPool.shutdown();
    }
}

控制台结果输出:
在这里插入图片描述
通过输出结果,我们可以分析得出,当线程复用,我们也能得到一个全新属于各自的副本。因此,我们在使用线程池结合ThreadLocal的场景下,需要注意在线程执行完成时,要对 ThreadLocal 进行清理。

参考文档:
https://cloud.tencent.com/developer/article/1469598
https://www.cnblogs.com/qifenghao/p/8977378.html

发布了14 篇原创文章 · 获赞 12 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/haogexiang9700/article/details/104603742