ThreadLocal使用注意:线程不安全,可能会发生内存泄漏

先说可能会发生内存泄漏:

前言

ThreadLocal 的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内 多个函数或者组件之间一些公共变量的传递的复杂度。但是如果 滥用ThreadLocal,就可能会导致内存泄漏。下面,我们将围绕三个方面来分析ThreadLocal 内存泄漏的问题

  • ThreadLocal 实现原理
  • ThreadLocal为什么会内存泄漏
  • ThreadLocal 最佳实践


ThreadLocal 实现原理

ThreadLocal的实现是这样的:每个Thread 维护一个 ThreadLocalMap 映射表,这个映射表的 key 是 ThreadLocal实例的 软引用,value 是真正需要存储的 Object。

也就是说 ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。值得注意的是 ThreadLocalMap 是使用 ThreadLocal 的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收。

 

ThreadLocal为什么会内存泄漏

ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value。导致value永远无法回收,造成内存泄漏。

其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除 线程ThreadLocalMap里所有key为null的value

 

但是这些被动的预防措施 并不能保证 不会内存泄漏

  • 使用static的ThreadLocal,延长了ThreadLocal的生命周期,可能导致的内存泄漏(参考ThreadLocal 内存泄露的实例分析)。
  • 分配使用了ThreadLocal而不再调用get(),set(),remove()方法,那么就会导致内存泄漏。

 

首先区分强、软、弱、虚四种引用:

强引用:任何时候都不会被回收,即使报了OOM错误。清除方法需要手动将对象置null,如调用List的clear方法等

软引用:soft,内存不够时,为了避免oom问题,会回收软引用对应的对象。

弱引用:weak,无论内存是否足够,只要发生了垃圾回收,就会进行回收

虚引用:phantom,任意时候都会被回收

 

引用类型转化方式:

 String str=new String("abc");    // 强引用

// 软引用 ,注意此时str还是强引用,softRef被垃圾回收时,但是 str还在内存中
 SoftReference<String> softRef=new SoftReference<String>(str);    

具体的可参考:https://blog.csdn.net/mazhimazh/article/details/19752475

 

为什么使用弱引用

从表面上看内存泄漏的根源在于使用了弱引用。网上的文章大多着重分析ThreadLocal使用了弱引用会导致内存泄漏,但是另一个问题也同样值得思考:为什么使用弱引用而不是强引用?

我们先来看看官方文档的说法:

To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.
为了应对非常大和长时间的使用,哈希表使用弱引用的 key

下面我们分两种情况讨论:

key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用。如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。


key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用。即使没有手动删除,ThreadLocal也会被回收。key为null的value 在下一次ThreadLocalMap调用set,get,remove的时候会被清除。


比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。

因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
 

比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期 跟Thread一样长,如果没有手动删除对应key都会导致内存泄漏,但是 使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,key为null的value 在下一次ThreadLocalMap调用set,get,remove的时候会被清除。

因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key 就会导致内存泄漏,而不是因为弱引用。
 

 

ThreadLocal 最佳实践

综合上面的分析,我们可以理解ThreadLocal内存泄漏的前因后果,那么怎么避免内存泄漏呢?

  • 每次使用完ThreadLocal,都调用它的remove()方法,清除数据。

在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal,需要用完就清理。(就跟加锁之后 需要解锁一样)

 

线程不安全:

大家通常知道,ThreadLocal类可以帮助我们实现线程的安全性,这个类能使 线程中的某个值保存值的对象关联起来。ThreadLocal提供了get与set等访问接口或方法,这些方法为 每个使用该变量的线程 都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时 设置的最新值。从概念上看,我们把ThreadLocal<T>理解成一个包含了Map<Thread,T>的对象,其中Map的key 用来标识 不同的线程而Map的value存放了相应线程的某个值。但是ThreadLocal的实现并非如此,我们以这样的理解方式去使用ThreadLocal也并不能实现真正的线程安全。

  下面我们举一个例子进行说明,Number是拥有一个int型成员变量的类:

public class Number {
    
    private int num;
 
    public int getNum() {
        return num;
    }
 
    public void setNum(int num) {
        this.num = num;
    }
 
    @Override
    public String toString() {
        return "Number [num=" + num + "]";
    }
    
}

 NotSafeThread是一个实现了Runable接口的类,其中我们创建了一个ThreadLocal<Number>类型的变量value,用来存放不同线程的num值。接着我们用线程池的方式启动了5个线程,我们希望使用ThreadLocal类 为5个不同的线程 都存放一个Number类型的副本,根除 对变量的共享,并且在调用ThreadLocal类的get()方法时,返回与线程关联的Number对象,而这些Number对象我们希望它们都能跟踪自己的计数值:
 

public class NotSafeThread implements Runnable {
 
    public static Number number = new Number();

    public static int i = 0;
 
    public void run() {
        //每个线程计数加一
        number.setNum(i++);
     //将其存储到ThreadLocal中
        value.set(number);//我们希望使用ThreadLocal类,为5个不同的线程 都存放一个Number类型的副本
        //输出num值
        System.out.println(value.get().getNum());
    }
 
    public static ThreadLocal<Number> value = new ThreadLocal<Number>() {
    };
 
    public static void main(String[] args) {
        ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++) {
            newCachedThreadPool.execute(new NotSafeThread());
        }
    }
 
}

 启动程序:输出结果

0
1
2
3
4

看起来一切正常,每个线程好像都有自己关于Number的存储空间,但是我们简单的在输出前 加一个延时

public class NotSafeThread implements Runnable {
 
    public static Number number = new Number();
 
    public static int i = 0;
 
    public void run() {
        //每个线程计数加一
        number.setNum(i++);
        //将其存储到ThreadLocal中
        value.set(number);//不同线程,保存的是同一个number对象的引用,而不是每一个线程保存一个num对象。因此当number对象的值被其他的线程修改时,会导致当前线程中保存的值,也会发生变化
        //延时2秒
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
        }
        //输出num值
        System.out.println(value.get().getNum());
    }
 
    public static ThreadLocal<Number> value = new ThreadLocal<Number>() {
    };
 
    public static void main(String[] args) {
        ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++) {
            newCachedThreadPool.execute(new NotSafeThread());
        }
    }
 
}

运行程序,输出:

4
4
4
4
4

 

为什么每个线程都输出4?难道他们没有独自保存自己的Number副本吗?为什么其他线程还是能够修改这个值?我们看一下ThreadLocal的源码

    public void set(Object obj)
    {
        Thread thread = Thread.currentThread();//获取当前线程
        ThreadLocalMap threadlocalmap = getMap(thread); //这些特定于线程的值是保存在当前的Thread对象中,并非保存在ThreadLocal对象中
        if(threadlocalmap != null)
            threadlocalmap.set(this, obj); //Thread对象中保存的是Object对象的一个引用。当有其他线程 对这个引用指向的对象 做修改时,当前线程Thread对象中保存的值也会发生变化
        else
            createMap(thread, obj);
    }

  其中getMap方法:

    ThreadLocal.ThreadLocalMap getMap(Thread thread)
    {
        return thread.inheritableThreadLocals;//返回的是thread的成员变量
    }

可以看到,这些特定于线程的值是保存在当前的Thread对象中,并非保存在ThreadLocal对象中。并且我们发现Thread对象中保存的是Object对象的一个引用这样的话,当有其他线程 对这个引用指向的对象 做修改时当前线程Thread对象中保存的值也会发生变化。这也就是为什么上面的程序为什么会输出一样的结果:5个线程中保存的是 同一Number对象的引用,在线程睡眠2s的时候,其他线程 将num变量进行了修改,因此它们最终输出的结果是相同的。

ThreadLocal 并不是 每个使用该变量的线程 都存有一份独立的副本这些特定于线程的值是保存在当前的Thread对象中,并非保存在ThreadLocal对象中。并且我们发现Thread对象中保存的是Object对象的一个引用这样的话,当有其他线程 对这个引用指向的对象 做修改时当前线程Thread对象中保存的值也会发生变化。这也就是为什么上面的程序为什么会输出一样的结果:5个线程中保存的是 同一Number对象的引用

 

  那么,ThreadLocal的 “为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值” 。这句话中的“独立的副本”,也就是我们理解的“线程本地存储” 只能是: 每个线程所独有的对象 并且不与其他线程进行共享,大概是这样的情况:

    public static ThreadLocal<Number> value = new ThreadLocal<Number>() {
        public Number initialValue(){//为每个线程保存的值进行初始化操作
            return new Number();
        }
    };

或者

    public void run() {
        value.set(new Number());
    }

 好吧...这个时候估计你会说:那这个ThreadLocal有什么用嘛。每个线程都自己new一个对象使用,只有它自己使用这个对象而不进行共享,那么程序肯定是线程安全的咯。这样看起来我不使用ThreadLocal,在需要用某个对象的时候,直接new一个给本线程使用不就好咯。

  确实,ThreadLocal的使用场景  不是为了 让多个线程 ,能共同使用某一对象而是我有一个线程A,其中 我需要用到某个对象o这个对象o 在这个线程A之内 会被多处调用,而我不希望将这个对象o 当作参数 在多个方法之间传递于是,我将这个对象o 放到TheadLocal中。这样,在这个线程A之内的任何地方只要线程A之中的方法 不修改 这个对象o,我都能取到同样的这个变量o

ThreadLocal 并不是 每个使用该变量的线程 都存有一份独立的副本

ThreadLocal的使用场景是:我们在一个线程A的多个方法中,都要使用同一个对象object。但我不希望将这个object在多个方法中传递。所以,我将这个object放到ThreadLoacl中,这样我就能在线程A中的任何地方,获取到同一个object对象(只要 线程A的的方法 不修改这个objcet)。

  再举一个在实际中应用的例子,例如,我们有一个银行的BankDAO类和一个个人账户的PeopleDAO类,现在需要个人向银行进行转账,在PeopleDAO类中有一个账户减少的方法,BankDAO类中有一个账户增加的方法,那么这两个方法在调用的时候必须使用同一个Connection数据库连接对象

  • 如果他们使用 个Connection对象,则会开启 两段事务,可能出现个人账户减少银行账户未增加的现象。
  • 使用同一个Connection对象的话,在应用程序中可能会设置为一个全局的数据库连接对象,从而避免 在调用每个方法时 都传递一个Connection对象

问题是当我们把Connection对象 设置为全局变量时,你不能保证 是否有其他线程会将这个Connection对象关闭,这样就会出现线程安全问题

解决办法就是在进行 转账操作这个线程中,从ThreadLocal中  获取 Connection对象。这样,在调用个人账户减少和银行账户增加的线程中,就能从ThreadLocal中取到 同一个Connection对象,并且这个Connection对象为转账操作这个线程独有,不会被其他线程影响,保证了线程安全性。


代码如下:

public class ConnectionHolder {
    
    public static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
    };
    
    public static Connection getConnection(){
        Connection connection = connectionHolder.get();
        if(null == connection){
            connection = DriverManager.getConnection(DB_URL);
            connectionHolder.set(connection);
        }
        return connection;
    }
 
}

 在框架中,我们需要将一个事务上下文(Transaction  Context)某个执行中的线程关联起来。通过将事务上下文 保存在 静态的ThreaLocal对象中(这个上下文肯定是不与其他线程共享的),可以很容易地实现这个功能:当框架代码 需要判断当前运行的是哪一个事务时只需从这个ThreadLocal对象中读取事务上下文,避免了在调用每个方法时都需要传递执行上下文信息

需要判断当前线程对应的是哪一个用户时,只需要从ThreadLocal对象中读取User对象,从而避免了在调用每个方法时,都传递User对象。


--------------------- 
版权声明:本文为CSDN博主「深山猿」的原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/h2604396739/article/details/83033302

 

 

 

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值