飞道的博客

深度剖析ThreadLocal(应用,构造,问题,与Syn异同)

462人阅读  评论(0)

官方介绍

从Java官方文档中的描述:ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程上下文。

我们可以得知 ThreadLocal 的作用是:提供线程内的局部变量,不同的线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度。

总结:
1.  线程并发: 在多线程并发的场景下
2.  传递数据: 我们可以通过ThreadLocal在同一线程,不同组件中传递公共变量
3.  线程隔离: 每个线程的变量都是独立的,不会相互影响

一. ThreadLocal适合用在哪些实际生产的场景中?

在通常的业务开发中, ThreadLocal有两种典型的使用场景。

  1. ThreadLocal用作 保存每个线程独享的对象, 为每个线程都创建一个副本,这样每个线程都可以修改自己所拥有的副本,而不会影响其他线程的副本,确保了线程安全 , 线程可复用(线程池),规则在每一线程中唯一,格式化传入参数,相对于没有线程复用的情况强
  2. ThreadLocal用作 每个线程内需要独立保存信息,以便供其他方法更方便地获取该信息的场景。 每个线程获取到的信息可能都是不一样的前面执行的方法保存了信息后,后续方法可以 通过 ThreadLocal直接获取到,避免了传参,类似于全局变量的概念。

1.1 格式化

  1. 典型场景
    这种场景通常用于保存线程不安全的工具类,典型的需要使用的类就是SimpleDateFormat。
  2. 场景介绍
    在这种情况下,每个Thread内都有自己的实例副本,且该副本只能由当前Thread 访问到并使用,相当于每个线程内部的本地变量,这也是Threadlocal命名的含义。因为每个线程独享副本,而不是公用的,所以不存在多线程间共享的问题。
    先来看一段代码
	public String date(int seconds){
        Date date = new Date(1000 * seconds);
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
        return simpleDateFormat.format(date);
    }

(1) 1000对象

我们将传入一个int seconds,然后用定义的simpleDateFormat格式化然后返回。
现在,1000 个线程都要用到 SimpleDateFormat:(用来模拟多线程,传入seconds,得到时间)

public class ThreadLocalDemo03 {
    public static ExecutorService threadPool = Executors.newFixedThreadPool(16);  // 1000线程太多,用线程池
    static ConcurrentHashMap<String, Integer> localMap = new ConcurrentHashMap<>();   // 计数
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            threadPool.submit(()->{
                String date = new ThreadLocalDemo03().date(finalI);
                localMap.put(date,0);  // 添加,可以起到去重作用,
            });
        }
        threadPool.shutdown();
        TimeUnit.SECONDS.sleep(2);
        System.out.println(localMap.size());   // 打印出结果,是否是1000
    }
    public String date(int seconds){
        Date date = new Date(1000 * seconds);
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
        return simpleDateFormat.format(date);
    }
}

可以看出,我们刚才所做的就是每个任务都创建了一个 simpleDateFormat 对象,也就是说,1000 个任务对应 1000 个 simpleDateFormat 对象。

(2) 1个对象,加锁

但是这样做是没有必要的,因为这么多对象的创建是有开销的,并且在使用完之后的销毁同样是有开销的,而且这么多对象同时存在在内存中也是一种内存的浪费。最简单的就是使用一个就可以了,
变为static 共享,但是存在线程不安全问题,所以加锁

public class ThreadLocalDemo04 {
    public static ExecutorService threadPool = Executors.newFixedThreadPool(16);
    static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
    static ConcurrentHashMap<String, Integer> localMap = new ConcurrentHashMap<>();
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            threadPool.submit(()->{
                String date = new ThreadLocalDemo04().date(finalI);
                localMap.put(date,0);
            });
        }
        threadPool.shutdown();
        TimeUnit.SECONDS.sleep(2);
//      3. synchronized 关键字,就会陷入一种排队的状态,多个线程不能同时工作
        System.out.println(localMap.size());
    }
    public String date(int seconds){
        Date date = new Date(1000 * seconds);
        String s = null;
        synchronized (ThreadLocalDemo04.class){   // 加锁,安全
            s = simpleDateFormat.format(date);
        }
        return s;
    }
}

但是,加锁相当排队,我们希望达到的效果是既不浪费过多的内存,同时又想保证线程安全。经过思考得出,可以让每个线程都拥有一个自己的 simple Date Format对象来达到这个目的,这样就能两全其美了。

(3) 等线程的对象,ThreadLocal

	public class ThreadLocalDemo06 {
    public static ExecutorService threadPool = Executors.newFixedThreadPool(16);
    static ConcurrentHashMap<Integer, Integer> localMap = new ConcurrentHashMap<>();  // 确定返回数目
   	static ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();    // SimpleDateFormat 对象数目
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            threadPool.submit(()->{
                String date = new ThreadLocalDemo06().date(finalI);
                map.put(date,0);
            });
        }
        threadPool.shutdown();
        TimeUnit.SECONDS.sleep(2);
        System.out.println("----------------------------"+map.size());      // 1000
        System.out.println("----------------------------"+localMap.size()); //  16
    }
    public String date(int seconds){
        Date date = new Date(1000 * seconds);
//        get()--->setInitialValue()--->initialValue()
        SimpleDateFormat dateFormat = dateFormatThreadLocal.get();
        int i = System.identityHashCode(dateFormat);
        localMap.put(i,0);  // 验证dateFormat对象是不唯一的
        return dateFormat.format(date);
    }
//    ThreadLocal
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>(){
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("mm:ss");
        }
    };
//  public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =
//                            ThreadLocal.withInitial(() -> new SimpleDateFormat("mm:ss"));
}

在这段代码中,我们使用了ThreadLocal帮每个线程去生成它自己的simpleDateFormat对象,
对于每个线程而言,这个对象是独享的。但与此同时,这个对象就不会创造过多,一共只有16个

1.2 避免传参

  1. 每个线程内需要保存类似于全局变量的信息(例如在拦截器中获取的用户信息),可以让不同方法直接使用,避免参数传递的麻烦却不想被多线程共享(因为不同线程获取到的用户信息不一样)
  2. 例如**,用 ThreadLocal保存一些业务内容**(用户权限信息、从用户系统获取到的用户名、用户ID等),这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不相同的。
  3. 在线程生命周期内,都通过 这个静态 ThreadLocal实例的get()方法取得自己set()过的那个对象,避免了将这个对象(如user对象)作为参数传递的麻烦。

(1) 将User作为参数一路传递

比如说 service1 会创建一个user对象,在后面 service 2/3/4 都需要这个对象,代码太过冗余

(2) Map存储

可以用一个静态map来存放User,但是Web服务器一般是多线程的,所以应该线程安全,使用手动加锁,或ConcurrentHashMap来存放
,但是,这都是有性能消耗的

(3) TreadLocal


同样是多个线程同时去执行,但是这些线程同时去访问这个ThreadLocal并且能利用ThreadLocal拿到只属于自己的独享对象。这样的话,就无需任何额外的措施,保证了线程安全,因为每个线程是独享user对象的.

/**
 * 在这个代码中我们可以看出,
 * 我们有一个UserContextHolder,
 * 里面保存了一个ThreadLocal,
 * 在调用Service1的方法的时候,
 * 就往里面存入了user对象,
 * 而在后面去调用的时候,
 * 直接从里面用 get 方法取出来就可以了。
 * 没有参数层层传递的过程,非常的优雅、方便。
 */
public class ThreadLocalDemo07 {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            int fin = i;
            new Thread(()-> new Service1().service1l(fin+"")).start();
        }
    }
}
class Service1{
    public void service1l(String str){
        User user = new User(str);
        UserContextHolder.holder.set(user);// 放入
        new Service2().service2();
    }
}

class Service2{
    public void service2(){
        User user = UserContextHolder.holder.get();  // 取出
        System.out.println(Thread.currentThread().getName()+"  Service2拿到用户名: "+user.name);
        new Service3().service3();
    }
}

class Service3{
    public void service3(){
        User user = UserContextHolder.holder.get();  // 取出
        System.out.println(Thread.currentThread().getName()+"  Service3拿到用户名: "+user.name);
        UserContextHolder.holder.remove();
    }
}

class UserContextHolder{
    public static ThreadLocal<User> holder = new ThreadLocal<>();
}

class User{
    String name;
    public User(String name) {
        this.name = name;
    }
}

Thread-3  Service2拿到用户名: 3
Thread-4  Service2拿到用户名: 4
Thread-4  Service3拿到用户名: 4
Thread-1  Service2拿到用户名: 1
Thread-1  Service3拿到用户名: 1
Thread-0  Service2拿到用户名: 0
Thread-0  Service3拿到用户名: 0
Thread-2  Service2拿到用户名: 2
Thread-2  Service3拿到用户名: 2
Thread-3  Service3拿到用户名: 3

1.3 总结

  1. ThreadLocal用作保存每个线程独享的对象,为每个线程都创建一个副本,每个线程都只能修改自己所拥有的副本,而不会影响其他线程的副本,这样就让原本在并发情况下,线程不安全的情况变成了线程安全的情况。
  2. ThreadLocal 用作每个线程内需要独立保存信息的场景,供其他方法更方便得获取该信息,每个线程获取到的信息都可能是不一样的,前面执行的方法设置了信息后,后续方法可以通过ThreadLocal 直接获取到,避免了传参。

二. ThreadLocal 是用来解决共享资源的多线程访问的问题吗?与Syn的异同

这道题的答案很明确——不是,

Threadlocal并不是用来解决共享资源问题的。虽然 Threadlocal确 实可以用于解决多线程情况下的线程安全问题,但其资源并不是共享的,而是每个线程独享的。所以 这道题其实是有一定陷阱成分在内的,

比如之前的SimpleDateFormat,每个线程都有一个对象,互不影响。如果给SimpleDateFormat改为static,它就不安全。

public class ThreadLocalStatic {
    public static ExecutorService threadPool = Executors.newFixedThreadPool(16);
    static SimpleDateFormat dateFormat = new SimpleDateFormat("mm:ss");
    public static void main(String[] args) throws InterruptedException {
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            threadPool.submit(()->{
                String date = new ThreadLocalStatic().date(finalI);
                map.put(date,0);
            });
        }
        threadPool.shutdown();
        TimeUnit.SECONDS.sleep(2);
        System.out.println("----------------------------"+map.size());
    }
    public String date(int seconds){
        Date date = new Date(1000 * seconds);
        SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
        return dateFormat.format(date);
    }
}
class ThreadSafeFormatter{
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>(){
        @Override
        protected SimpleDateFormat initialValue() {
            return ThreadLocalStatic.dateFormat;
        }
    };
}

结果不是1000,解决多线程共同访问问题,只能加锁

2.1 ThreadLocal 与Synchronized的关系

当ThreadLocal用于解决线程安全问题的时候,也就是把一个对象给每个线程都生成一份独享的副本的,在这种场景下,ThreadLocal和synchronized都可以理解为是用来保证线程安全的,

例如,在1.1 SimpleDateFormat 的例子中,我们既使用了 synchronized 来达到目的,也使用了 ThreadLocal 作为实现方案。但是效果和实现原理不同:

  1. synchronized 主要用于临界资源的分配,在同一时刻限制最多只有一个线程能访问资源
  2. ThreadLocal 通过让每一个线程独享自己的副本,避免了资源的竞争。
ThreadLocal synchronized
时空性 空间换时间 时间换空间
侧重点 多线程中让每一个线程之间的数据相互隔离 多个线程之间访问资源的同步性

但是ThreadLocal ,用于让多个类能更方便的拿到我们希望给每个线程独立保存这个信息的场景下(比如每个线程都会对应一个用户信息,也就是user对象),在这种场景下,ThreadLocal侧重的是避免传参,所以此时ThreadLocal和synchronized是两个不同维度的工具

三.多个 ThreadLocal 在 Thread 中的 threadlocals 里是怎么存储的

3.1 ThreadLocal ,Thread,ThreadLocalMap的关系(jdk1.8)

JDK8 ThreadLocal的设计是:每个Thread维护一个ThreadLocalMap哈希表,这个哈希表的keyThreadLocal实例本身,value才是真正要存储的值Object

​ (1) 每个Thread线程内部都有一个Map (ThreadLocalMap)
​ (2) Map里面存储ThreadLocal对象(key)和线程的变量副本(value)
​ (3)Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。
​ (4)对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。

3.2 ThreadLocal 的设计演变

每个ThreadLocal类都创建一个Map,然后用线程的ID threadID作为Mapkey,要存储的局部变量作为Mapvalue,这样就能达到各个线程的局部变量隔离的效果。这是最简单的设计方法,JDK最早期的ThreadLocal就是这样设计的:

但是JDK后面优化了设计方案,到JDK8时,就是这样的设计:

3.3 这样设计的好处

四.ThreadLocal的核心方法源码

方法声明 描述
protected T initialValue() 返回当前线程局部变量的初始值
public void set( T value) 设置当前线程绑定的局部变量
public T get() 获取当前线程绑定的局部变量
public void remove() 移除当前线程绑定的局部变量

它们得逻辑是相似的

4.1 get()

(1) 源码,注释

  /**
     * 返回当前线程中保存ThreadLocal的值
     * 如果当前线程没有此ThreadLocal变量,
     * 则它会通过调用{@link #initialValue} 方法进行初始化值
     * @return 返回当前线程对应此ThreadLocal的值
     */
    public T get() {
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        // 如果此map存在
        if (map != null) {
            // 以当前的ThreadLocal 为 key,调用getEntry获取对应的存储实体e
            ThreadLocalMap.Entry e = map.getEntry(this);
            // 找到对应的存储实体 e 
            if (e != null) {
                @SuppressWarnings("unchecked")
                // 获取存储实体 e 对应的 value值
                // 即为我们想要的当前线程对应此ThreadLocal的值
                T result = (T)e.value;
                return result;
            }
        }
        // 如果map不存在,则证明此线程没有维护的ThreadLocalMap对象
        // 调用setInitialValue进行初始化
        return setInitialValue();
    }
    /**
     * set的变样实现,用于初始化值initialValue,
     * 用于代替防止用户重写set()方法
     * @return the initial value 初始化后的值
     */
    private T setInitialValue() {
        // 调用initialValue获取初始化的值
        T value = initialValue();
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        // 如果此map存在
        if (map != null)
            // 存在则调用map.set设置此实体entry
            map.set(this, value);
        else
            // 1)当前线程Thread 不存在ThreadLocalMap对象
            // 2)则调用createMap进行ThreadLocalMap对象的初始化
            // 3)并将此实体entry作为第一个值存放至ThreadLocalMap中
            createMap(t, value);
        // 返回设置的值value
        return value;
    }


    /**
     * 获取当前线程Thread对应维护的ThreadLocalMap 
     * 
     * @param  t the current thread 当前线程
     * @return the map 对应维护的ThreadLocalMap 
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
	
/**
     *创建当前线程Thread对应维护的ThreadLocalMap 
     *
     * @param t 当前线程
     * @param firstValue 存放到map中第一个entry的值
     */
	void createMap(Thread t, T firstValue) {
        //这里的this是调用此方法的threadLocal
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

(2)代码流程

A. 首先获取当前线程

​ B. 根据当前线程获取一个Map

​ C. 如果获取的Map不为空,则在Map中以ThreadLocal的引用作为key来在Map中获取对应的value e,否则转到E

​ D. 如果e不为null,则返回e.value,否则转到E

​ E. Map为空或者e为空,则通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的Map

总结: 先获取当前线程的 ThreadLocalMap 变量,如果存在则返回值,不存在则创建并返回初始值。

4.1 set()

(1 ) 源码和对应的中文注释

     * 设置当前线程对应的ThreadLocal的值
     *
     * @param value 将要保存在当前线程对应的ThreadLocal的值
     */
    public void set(T value) {
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        // 如果此map存在
        if (map != null)
            // 存在则调用map.set设置此实体entry
            map.set(this, value);
        else
            // 1)当前线程Thread 不存在ThreadLocalMap对象
            // 2)则调用createMap进行ThreadLocalMap对象的初始化
            // 3)并将此实体entry作为第一个值存放至ThreadLocalMap中
            createMap(t, value);
    }

(2) 代码执行流程

​ A. 首先获取当前线程,并根据当前线程获取一个Map
​ B. 如果获取的Map不为空,则将参数设置到Map中(当前ThreadLocal的引用作为key)
​ C. 如果Map为空,则给该线程创建 Map,并设置初始值

4.3 remove方法

(1 ) 源码和对应的中文注释

 /**
     * 删除当前线程中保存的ThreadLocal对应的实体entry
     */
     public void remove() {
        // 获取当前线程对象中维护的ThreadLocalMap对象
         ThreadLocalMap m = getMap(Thread.currentThread());
        // 如果此map存在
         if (m != null)
            // 存在则调用map.remove
            // 以当前ThreadLocal为key删除对应的实体entry
             m.remove(this);
     }

(2) 代码执行流程

A. 首先获取当前线程,并根据当前线程获取一个Map
B. 如果获取的Map不为空,则移除当前ThreadLocal对象对应的entry

4.4 initialValue方法

/**
  * 返回当前线程对应的ThreadLocal的初始值
  
  * 此方法的第一次调用发生在,当线程通过{@link #get}方法访问此线程的ThreadLocal值时
  * 除非线程先调用了 {@link #set}方法,在这种情况下,
  * {@code initialValue} 才不会被这个线程调用。
  * 通常情况下,每个线程最多调用一次这个方法。
  *
  * <p>这个方法仅仅简单的返回null {@code null};
  * 如果程序员想ThreadLocal线程局部变量有一个除null以外的初始值,
  * 必须通过子类继承{@code ThreadLocal} 的方式去重写此方法
  * 通常, 可以通过匿名内部类的方式实现
  *
  * @return 当前ThreadLocal的初始值
  */
protected T initialValue() {
    return null;
}

此方法的作用是 返回该线程局部变量的初始值。

  1. 这是一个延迟调用方法(懒加载):只有在set方法还未调用,而调用了get方法时才执行,并且在你以后不直接调用initialValue方法情况下,只执行这一次
    get()-->setInitialValue()-->initialValue()
  2. 这个方法缺少实现直接返回一个null
  3. 如果想要一个除null之外的初始值,可以重写此方法。(备注: 该方法是一个protected的方法,显然是为了让子类覆盖而设计的)(我们在开始的例子里便使用过此方法)

五. ThreadLocalMap 类,也就是 Thread.threadLocals

(1)基本结构

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

static class ThreadLocalMap {
	/**
	 * 在ThreadLocalMap中,也是用Entry来保存K-V结构数据的。
	 * 但是Entry中key只能是ThreadLocal对象,这点被Entry的构造方法已经限定死了
	 * 另外,Entry继承WeakReference,使用弱引用,
	 * 可以将ThreadLocal对象的生命周期和线程生命周期解绑,持有对ThreadLocal的弱引用,
	 * 可以使得ThreadLocal在没有其他强引用的时候被回收掉,
	 * 这样可以避免因为线程得不到销毁导致ThreadLocal对象无法被回收
	 * /
	static class Entry extends WeakReference<ThreadLocal> {
	    /** The value associated with this ThreadLocal. */
	    Object value;
	
	    Entry(ThreadLocal k, Object v) {
	        super(k);
	        value = v;
	    }
	
        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;
//.....

它在Thread中threadLocals

public
class Thread implements Runnable {
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
//.....

(2) 不同点——线性探测法

​ ThreadLocalMap使用开发地址-线性探测法来解决哈希冲突,线性探测法的地址增量di = 1, 2, … 其中,i为探测次数。该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。假设当前table长度为16,也就是说如果计算出来key的hash值为14,如果table[14]上已经有值,并且其key与当前key不一致,那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,这个时候如果还是冲突会回到0,取table[0],以此类推,直到可以插入。

按照上面的描述,可以把table看成一个环形数组

先看一下线性探测相关的代码,从中也可以看出来table实际是一个环:

	/* 获取环形数组的下一个索引*/
    private static int nextIndex(int i, int len) {
        return ((i + 1 < len) ? i + 1 : 0);
    }
    /**获取环形数组的上一个索引*/
    private static int prevIndex(int i, int len) {
        return ((i - 1 >= 0) ? i - 1 : len - 1);
    }

ThreadLocalMap的set()代码如下:

private void set(ThreadLocal<?> key, Object value) {
        ThreadLocal.ThreadLocalMap.Entry[] tab = table;
        int len = tab.length;
        //计算索引,
        int i = key.threadLocalHashCode & (len-1);
        /**
         * 根据获取到的索引进行循环,如果当前索引上的table[i]不为空,在没有return的情况下,
         * 就使用nextIndex()获取下一个(上面提到到线性探测法)。
         */
        for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            ThreadLocal<?> k = e.get();
            //table[i]上key不为空,并且和当前key相同,更新value
            if (k == key) {
                e.value = value;
                return;
            }
            /**
             * table[i]上的key为空,说明被回收了
             * 这个时候说明改table[i]可以重新使用,用新的key-value将其替换,并删除其他无效的entry
             */
            if (k == null) {
                replaceStaleEntry(key, value, i);
                return;
            }
        }

六. 内存泄漏与弱引用,和remove()必要

(1)内存泄漏相关概念

  • Memory overflow:内存溢出,没有足够的内存提供申请者使用
  • Memory leak:内存泄漏是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃,OOM等严重后果。内存泄漏的堆积终将导致内存溢出

(2)弱引用相关概念

  • 强引用(Strong Reference):就是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾回收器就不会回收这种对象。
  • 弱引用(Weak Reference):垃圾回收器一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
类型 回收时间 应用场景
强引用 一直存活,除非GC Roots 不可达 所有程序的场景,基本对象,自定义对象等
软引用 内存不足时会被回收 一般用在对内存非常敏感的资源上,用作缓存的场景比较多,例如:网页缓存,图片缓存
弱引用 只能存活到下一次GC前 生命周期很短的对象,例如ThreadLocal 中的Key
虚引用 随时会被回收,创建了可能很快就会被回收 可能被JVM团队内部用来跟踪JVM的垃圾回收活动

(3) Key的泄漏

3.1 假设Key是强引用

我们可能会在业务代码中执行了ThreadLocal instance=null操作,想清理掉这个ThreadLocal实例,但是假设我们在ThreadLocalMap的Entry 中强引用了ThreadLocal实例,那么,虽然在业务代码中把ThreadLocal 实例置为了null,但是在Thread类中依然有这个引用链的存在。

GC在垃圾回收的时候会进行可达性分析,它会发现这个ThreadLocal对象依然是可达的,所以对于这个ThreadLocal对象不会进行垃圾回收,这样的话就造成了内存泄漏的情况。

3.2 所以Key是弱引用

JDK开发者考虑到了这一点,所以 ThreadLocalMap中的Enty继承了 WeakReference弱引用,代码如下所示
关于Java中的WeakReference

	 static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

可以看到,这个Entry是extends WeakReference
弱引用的特点是,如果这个对象只被弱引用关联,而没有任何强引用关联,那么这个对象就可以被回收,所以弱引用不会阻止GC。因此,这个弱引用的机制就避免了ThreadLocal的内存泄露问题
这就是为什么Entry的key 要使用弱引用的原因。

3.3 Value的泄漏

可是,如果我们继续研究的话会发现,虽然 ThreadLocalMap的每个 Entry都是一个对key的弱引 用,但是这个Enty包含了一个对vaue的强引用,还是刚才那段代码:

	 static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

value = v;就代表强引用的发生。
正常情况下,当线程终止,key所对应的value是可以被正常垃圾回收的,因为没有任何强引用存在了。但是有时线程的生命周期是很长的,如果线程迟迟不会终止,那么可能ThreadLocal以及它所对应的value早就不再有用了。在这种情况下,我们应该保证它们都能够被正常的回收。

  1. 左侧是引用栈,栈里面有一个ThreadLocal的引用和一个线程的引用,右侧是堆,在堆中是对象的实例。

  2. 链路:Thread Ref→Current Thread →ThreadLocalMap→Entry一Value→可能泄漏的value实例。

  3. 这条链路是随着线程的存在而一直存在的,如果线程执行耗时任务而不停止,那么当垃圾回收进行可达性分析的时候,这个Value 就是可达的,所以不会被回收。但是与此同时可能我们已经完成了业务逻辑处理,不再需要这个Value了,此时也就发生了内存泄漏问题

  4. JDK同样也考虑到了这个问题,在执行ThreadLocal的set、remove、rehash等方法时,它都会扫描key为null 的Entry,如果发现某个Entry的key为null,则代表它所对应的value 也没有作用了,所以它就会把对应的value置为null,这样,value对象就可以被正常回收了。

  5. 但是假设 ThreadLocal已经不被使用了,那么实际上set、 remove rehash方法也不会被调用,与此同时,如果这个线程又一直存活、不终止的话,那么刚才的那个调用链就一直存在,也就导致了 value的内存泄漏。

3.4 如何避免内存泄漏–remove()

调用 ThreadLocal的 remove方法。调用这个方法就可以删除对应的 value对象,可以避免内存泄漏。

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

它是先获取到ThreadLocalMap这个引用的,并且调用了它的remove方法。这里的remove方法可以把key所对应的value给清理掉,这样一来,value就可以被GC回收了

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

综上, ThreadLocal内存泄漏的根源是:由于 ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应value就会导致内存泄漏


转载:https://blog.csdn.net/qq_43542795/article/details/105563638
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场