ThreadLocal原理及使用场景
发布网友
发布时间:2024-09-27 04:25
我来回答
共1个回答
热心网友
时间:2024-09-28 03:47
ThreadLocal,即线程本地变量,它旨在解决多线程并发中共享变量访问的问题。
所谓的共享变量,指的是位于堆中的实例、静态属性以及数组。这些共享数据的访问受到Java内存模型(JMM)的约束。每个线程都拥有自己的本地内存,当线程访问堆中的变量时,这些变量会被复制到线程的本地内存中。当线程修改共享变量后,会通过JMM进行管理,将修改后的值写回到主内存中。
在多线程环境下,当多个线程同时修改共享变量时,就可能出现线程安全问题,即数据不一致的问题。通常,我们会通过加锁(synchronized或Lock)的方式来解决这一问题。然而,这种方法会对性能产生较大影响。为了解决这个问题,JDK1.2中引入了ThreadLocal类,通过修饰共享变量,使每个线程都单独拥有一份共享变量,从而实现线程间的隔离。
ThreadLocal的使用场景与锁的使用场景有所不同。以下是ThreadLocal的使用及原理:
1. 使用:通常将ThreadLocal声明为一个静态字段,并初始化如下:
其中Object即为原本堆中共享变量的数据。
例如,有一个User对象需要在不同线程之间进行隔离访问,可以定义ThreadLocal如下:
常用的方法包括:
set(T value):设置线程本地变量的内容。
get():获取线程本地变量的内容。
remove():移除线程本地变量。
注意:在线程池的线程复用场景中,在线程执行完毕时一定要调用remove,避免在线程被重新放入线程池中时,本地变量的旧状态仍然被保存。
1. 原理:ThreadLocal是如何在每个线程中保存一份单独的本地变量呢?首先,我们需要了解Java中的线程。线程是一个Thread类的实例对象。一个实例对象的实例成员字段内容肯定是这个对象独有的。因此,我们可以将保存ThreadLocal线程本地变量作为一个Thread类的成员字段。这个成员字段是:
它是一个ThreadLocal中定义的Map对象,用于保存该线程中的所有本地变量。ThreadLocalMap中的Entry的定义如下:
ThreadLocalMap和Entry都在ThreadLocal中定义。
1. ThreadLocal设计:在JDK早期的设计中,每个ThreadLocal都有一个map对象,将线程作为map对象的key,要存储的变量作为map的value。然而,在JDK8之后,每个Thread维护一个ThreadLocalMap对象,这个Map的key是ThreadLocal实例本身,value是存储的值要隔离的变量,是泛型。具体过程如下:
每个Thread线程内部都有一个Map(ThreadLocalMap::threadlocals);Map里面存储ThreadLocal对象(key)和线程的变量副本(value);Thread内部的Map由ThreadLocal维护,由ThreadLocal负责向map获取和设置变量值;对于不同的线程,每次获取副本值时,别的线程不能获取当前线程的副本值,就形成了数据之间的隔离。
1. 使用ThreadLocal的好处:保存每个线程绑定的数据,在需要的地方可以直接获取,避免直接传递参数带来的代码耦合问题;各个线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失。
2. ThreadLocal内存泄露问题:内存泄露问题指程序中动态分配的堆内存由于某种原因没有被释放或者无法释放,造成系统内存的浪费,导致程序运行速度减慢或者系统崩溃等严重后果。内存泄露堆积将会导致内存溢出。
ThreadLocal的内存泄露问题一般考虑和Entry对象有关。在Entry定义中可以看出ThreadLocal::Entry被弱引用所修饰。JVM会将弱引用修饰的对象在下次垃圾回收中清除掉。这样就可以实现ThreadLocal的生命周期和线程的生命周期解绑。但实际上,并不是使用了弱引用就一定不会发生内存泄露问题。考虑以下几个过程:
使用强引用:当ThreadLocal Ref被回收了,由于在Entry使用的是强引用,在Current Thread还存在的情况下就存在着到达Entry的引用链,无法清除掉ThreadLocal的内容,同时Entry的value也同样会被保留;也就是说,就算使用了强引用仍然会出现内存泄露问题。
使用弱引用:当ThreadLocal Ref被回收了,由于在Entry使用的是弱引用,因此在下次垃圾回收的时候就会将ThreadLocal对象清除,这个时候Entry中的KEY=null。但是由于ThreadLocalMap中仍然存在Current Thread Ref这个强引用,因此Entry中value的值仍然无法清除。还是存在内存泄露的问题。
由此可以发现,使用ThreadLocal造成内存泄露的问题是因为:ThreadLocalMap的生命周期与Thread一致,如果不手动清除掉Entry对象的话就可能会造成内存泄露问题。因此,需要我们在每次在使用完之后需要手动的remove掉Entry对象。
那么为什么使用弱引用?避免内存泄露的两种方式:使用完ThreadLocal,调用其remove方法删除对应的Entry或者使用完ThreadLocal,当前Thread也随之运行结束。第二种方法在使用线程池技术时是不可以实现的。
所以一般都是自己手动调用remove方法,调用remove方法弱引用和强引用都不会产生内存泄露问题,使用弱引用的原因如下:
在ThreadLocalMap的set/getEntry中,会对key进行判断,如果key为null,那么value也会被设置为null,这样即使在忘记调用了remove方法,当ThreadLocal被销毁时,对应value的内容也会被清空。多一层保障!
总结:存在内存泄露的有两个地方:ThreadLocal和Entry中Value;最保险还是要注意要自己及时调用remove方法!!!
3. ThreadLocal的应用场景:场景一:在重入方法中替代参数的显式传递;场景二:全局存储用户信息;场景三:解决线程安全问题。
4. 总结:ThreadLocal更像是对其他类型变量的一层包装,通过ThreadLocal的包装使得该变量可以在线程之间隔离和当前线程全局共享。线程的隔离性和变量的线程全局共享性得益于在每个Thread类中的threadlocals字段。(从类实例对象的角度抽象的去看Java中的线程!!!)ThreadLocalMap中Entry的Key不管是否使用弱引用都有内存泄露的可能。引起内存泄露主要在于ThreadLocal对象和Entry中的Value对象,因此要确保每次使用完之后都remove掉Entry!