Java并发编程:深入剖析ThreadLocal

ThreadLocal,线程本地变量,或者线程本地存储。ThreadLocal为变量在每个线程中都创建了一个副本,每个线程可以访问自己内部的副本变量。

ThreadLocal是如何为每个线程创建变量的副本的:

  首先,在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。

  初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。

  然后在当前线程里面,就可以通过ThreadLocal get方法在threadLocals里面查找副本变量。

 

1. ThreadLocal有什么缺陷?如果是线程池里的线程用ThreadLocal会有什么问题?

下图为ThreadLocal的内部结构图

 

从上面的结构图,ThreadLocal的核心机制

每个Thread线程内部都有一个Map。

Map里面存储线程本地对象(key)和线程的变量副本(value)

Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。

所以对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。

 

ThreadLocalMap

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

 

和HashMap的最大的不同在于,ThreadLocalMap结构非常简单,没有next引用,也就是说ThreadLocalMap中解决Hash冲突的方式并非链表的方式,而是采用线性探测的方式。(ThreadLocalMap如何解决冲突?)

 

在ThreadLocalMap中,也是用Entry来保存K-V结构数据的。但是Entry中key只能是ThreadLocal对象,这点被Entry的构造方法已经限定死了。

static class Entry extends WeakReference<ThreadLocal> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal k, Object v) { super(k); value = v; } }

 

注意了!!

Entry继承自WeakReference(弱引用,生命周期只能存活到下次GC前),但只有Key是弱引用类型的,Value并非弱引用。(问题马上就来了)

 

由于ThreadLocalMap的key是弱引用,而Value是强引用。这就导致了一个问题,ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key会被回收,而Value不会回收。

 

当线程没有结束,但是ThreadLocal已经被回收,则可能导致线程中存在ThreadLocalMap<null, Object>的键值对,造成内存泄露。(ThreadLocal被回收,ThreadLocal关联的线程共享变量还存在)。

 

如何避免泄漏

1、使用完线程共享变量后,显示调用ThreadLocalMap.remove方法清除线程共享变量;

既然Key是弱引用,那么我们要做的事,就是在调用ThreadLocal的get()、set()方法时完成后再调用remove方法,将Entry节点和Map的引用关系移除,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。

 

2、JDK建议ThreadLocal定义为private static,线程存活期间,ThreadLocal的实例一直有强引用存在,那么ThreadLocalMap里的弱引用虽然也存在,但是GC运行时,检查到这个实例除了弱引用还有强引用存在,这个时候GC并不会回收该实例,所以当前线程无法通过ThreadLocalMap的防护措施清除ThreadLocal的key,自然也无法清除它对应的value。这样ThreadLocal的弱引用问题则不存在了。

ThreadLocal用于保存某个线程共享变量:对于同一个static ThreadLocal,不同线程只能从中get,set,remove自己的变量,而不会影响其他线程的变量。

让对象共享也许很容易做到,用静态变量引用对象就能很简单的做到对象共享。

但是并不是所有静态变量都是拿出去共享的,有时我们希望一些静态变量不被共享,要求它在不同的线程中有不同的表现,并且变量自始至终都只能让相同的线程访问,即让每个线程都拥有独立的静态变量。

这就是“对象的线程本地化”

ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。

  在threadlocal的生命周期中,都存在这些引用. 看下图: 实线代表强引用,虚线代表弱引用.

每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个threadlocal实例. 这个Map的确使用了弱引用,不过弱引用只是针对key. 每个key都弱引用指向threadlocal. 当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收. 但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用. 只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收.

  比如使用线程池的时候,线程结束是不会销毁的,会再次使用的。就可能出现内存泄露。  

  PS.Java为了最小化减少内存泄露的可能性和影响,在ThreadLocal的get,set的时候都会清除线程Map里所有key为null的value。所以最怕的情况就是,threadLocal对象设null了,开始发生“内存泄露”,然后使用线程池,这个线程结束,线程放回线程池中不销毁,这个线程一直不被使用,或者分配使用了又不再调用get,set方法,那么这个期间就会发生真正的内存泄露。

内存泄露是因为WeakReference Key的问题,当然,Java的各位大佬肯定早就想到这个问题了,注释里面是这么说的,大意就是如果key==null的时候,就可以认为这个值无效了,可以调用expunged进行清理:

而这个expungeStaleEntry方法在get、set时都会有间接的调用,而且remove方法中也会显示的调用,这也就是为什么有的文章中说通过在线程调用完成之后,通过调用remove方法能有效的杜绝该泄露问题的原因。

当然简单来说理解到这里就基本明了内存泄露的原因,但是其实再深入一点来说,如果泄露的原因是Key被释放,而Value没有释放,那么是否一定会有泄露呢?

答案当然是否定的,因为如果是一般的线程场景中,除了会调用expungeStaleEntry来进行清理,最差,在线程结束之时,自然也就消除了引用从而使得Value得以GC回收。

所以,会不会有线程一直不结束的场景呢?

当然答案是肯定的,最简单来说线程只要一直在wait就不会结束了,不过这种场景下其实和泄露也没啥关系的感觉。

其实最常用的线程一直不结束的场景,自然就是线程池了。因为这种情况下,线程是一直在不断的重复运行的,从而也就造成了value可能造成累积的情况。具体的模拟可以参考: 深入理解ThreadLocal的"内存溢出"

最后来做个总结吧,可能泄露的场景仅且仅在:

1.线程run方法结束后没有显示的调用remove进行清理

2.线程在线程池的模式下,一直重复运行

 

静态变量的引用存放在堆中,但是引用指向的静态变量存放在方法区里面。static变量也称作静态变量,静态变量和非静态变量的区别是:静态变量被所有的对象所共享,在内存中只有一个副本【存放在方法区】,它当且仅当在类初次加载时会被初始化【加final和不加final的static变量初始化的位置不一样】。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。也就是说,在一个线程内,没有被static修饰的ThreadLocal变量实例,会随着所在的类多次创建而被多次实例化,虽然ThreadLocal限制了变量的作用域,但这样频繁的创建变量实例是没有必要的。ThreadLocal变量为什么用static修饰

 

答案是,为了避免重复创建TSO(thread specific object,即与线程相关的变量。),我们知道,一个ThreadLocal实例对应当前线程中的一个TSO实例。因此,如果把ThreadLocal声明为某个类的实例变量(而不是静态变量),那么每创建一个该类的实例就会导致一个新的TSO实例被创建。显然,这些被创建的TSO实例是同一个类的实例。于是,同一个线程可能会访问到同一个TSO(指类)的不同实例,这即便不会导致错误,也会导致浪费(重复创建等同的对象)!因此,一般我们将ThreadLocal使用static修饰即可。

对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

静态变量:线程非安全。

 

静态变量即类变量,位于方法区,为所有对象共享,共享一份内存,一旦静态变量被修改,其他对象均对修改可见,故线程非安全。

 

实例变量:单例模式(只有一个对象实例存在)线程非安全,非单例线程安全。

 

实例变量为对象实例私有,在虚拟机的堆中分配,若在系统中只存在一个此对象的实例,在多线程环境下,“犹如”静态变量那样,被某个线程修改后,其他线程对修改均可见,故线程非安全;如果每个线程执行都是在不同的对象中,那对象与对象之间的实例变量的修改将互不影响,故线程安全。

 

局部变量:线程安全。

 

每个线程执行时将会把局部变量放在各自栈帧的工作内存中,线程间不共享,故不存在线程安全问题。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值