1. 基本概念
Thread Local提供线程内部的局部变量,这种变量可以在多线程环境下访问(get(), set())时内保证各个现成的变量相对独立于
其他线程内部的局部变量。ThreadLocal基本都是private static类型的
总结:
1.线程并发: 再多线程并发场景下使用;
2.传递数据:可以使用ThreadLocal在同一线程,不同组件中传递公共变量
3.线程隔离:每个线程的变量都是独立的,不会相互影响。
2 基本使用
方法申明 | 描述 |
---|---|
ThreadLocal() | 创建ThreadLocal对象 |
public void set(T value) | 设置当前线程的局部变量 |
public T get() | 获取当前线程的局部变量 |
public void remove() | 移除当前线程的局部变量 |
使用需求:线程隔离
再多线程并发的情况下,使用ThreadLocal线程中变量相互独立
线程A : 设置(变量1), 获取(变量1)
线程B : 设置(变量2), 获取(变量2)
不适用ThreadLocal,在并发时将会出现资源抢占的情况。
3. ThreadLocal类与synchronized关键字
sychronized关键字我们经常用来进行线程同步,那使用他的结果跟ThreadLocal有什么区别呢?
synchronized确实可以解决线程抢占的问题,因为确实可以使线程同步。但问题的关键在于,sychronized会使得线程锁定,多个线程只能在串行下执行,所以会导致效率低下。
synchronized | ThreadLocal | |
---|---|---|
原理 | 同步机制采用“以时间换空间”的方式,只提供一个变量,让不同线程排队访问。 | 采用“用空间换时间“的方式,为每一个线程提供了一个变量的副本,从而实现同时访问但又互相不干扰 |
侧重点 | 多个资源之间访问资源的同步 | 多线程中让每个线程资源之间相互隔离 |
ThreadLocal的好处:
1. 传递数据: 每个线程绑定的数据,在需要的地方可以直接获取, 避免参数直接传递带来的耦合问题
2. 线程隔离:各线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失。
4. ThreadLocal的内部结构
JDK8设计如下:
1. 每个Thread线程内部有一个Map(ThreadLocalMap);
2. Map里存储ThreadLocal对象(key)和线程的变量副本(value);
3. Thread内的Map是由ThreadLocal维护的,又ThreadLocal负责向mao获取和设置线程的变量值;
4. 对于不同的线程,每次获取副本值,别的线程并不能获取到当前线程的副本值,形成了副本的隔离;
图:Thread、ThreadLocalMap和ThreadLocal的关系
get()源码:
此种设计方案的优势:
1.每个Map存储的Entry数量变少;
2.当Thead销毁时,ThreadLocalMap也会随之销毁,减少了内存的使用;
5. ThreadLocalMap的弱引用设计
先回顾几个相关定义:
5.1 内存泄漏相关概念
Memory Overflow:内存溢出, 没有足够的内存提供申请者使用;
Memory Leak: 内存泄露,指程序中已经动态分配的堆内存由于某种原因程序未释放或则和无法释放,造成系统的浪费,导致了陈旭运行速度减慢甚至系统崩溃等严重后果。内存泄露的堆积将导致内存溢出。
5.2 强软弱虚引用概念
1. 强引用(只要引用存在GC时就不会删):
强引用可以直接访问目标对象;
只要有引用变量存在,垃圾回收器永远不会回收。JVM即使抛出OOM异常,也不会回收强引用所指向的对象;
强引用可能导致内存泄漏问题;
2. 软引用(类似缓存,内存足够则不删,内存不够则删):
一个持有软引用的对象,不会被JVM很快回收,JVM会根据当前堆的使用情况来判断何时回收。
当堆使用率临近阈值时,才会去回收软引用的对象。因此,软引用可以用于实现对内存敏感的高速缓存。
SoftReference的特点是它的一个实例保存对一个Java对象的软引用,该软引用的存在不妨碍垃圾收集线程
对该Java对象的回收。也就是说,一旦SoftReference保存了对一个Java对象的软引用后,在垃圾线程对 这个Java对象回收前,
SoftReference类所提供的get()方法返回Java对象的强引用。一旦垃圾线程回收该Java对象之后,get()方法将返回null。
3. 弱引用(只要发现弱引用,不管系统堆空间是否足够,都会将对象进行回收):
4. 虚引用:
一个持有虚引用的对象和没有引用几乎是一样的,随时可能被垃圾回收器回收,当试图通过虚引用的get()方法取得强引用时,
总是会失败。并且虚引用必须和引用队列一起使用,它的作用在于检测对象是否已经从内存中删除,跟踪垃圾回收过程。
当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在垃圾回收后,销毁这个对象,将这个虚引用加入引用队列。
程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。
如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动.
5.3 ThreadLocalMap中的Key为什么使用弱引用
1. 假设使用强引用会出现内存泄漏么?
图:强引用结构示意图
(1)假设业务代码中使用完ThreadLocal,ThreadLocal Ref被回收了
(2)ThreadLocalMap中的Key强引用了ThreadLocal,造成ThreadLocal被强引用而无法回收
(3) 在没有手动删除次Entry一级CurrentThread一直运行的情况下,强引用链Thread Ref -> currentThread->ThreadLocalMap->Entry会一直存在,Entry就不会被回收但Entry又不会再被用到。(Entry中包括ThreadLocal实例和value)所以Entry内存泄漏
2. 使用弱引用就不会内存泄漏了么?
图:弱引用结构示意图
(1)同样是使用完ThreadLcoal,ThreadLocal Ref被回收;
(2)由于ThreadLocalMap支持有ThreadLocal的弱引用,没有任何强引用指向ThreadLcoal,所以ThreadLocal可以被顺利GC,即Entry中的Key = NULL;
(3)但在没有删除Entry以及CurrentThread继续运行的情况下,也存在强引用链ThreadRef -> currentThread -> ThreadLocalMap -> Entry -> value, value不会被回收,而次value不会再被访问,所以导致value内存泄漏。
3. 总结:
从上述两个情况可以发现,内存是否泄露跟强弱引用没有必然关系,而是跟
(1)是否手动删除了Entry;(2)CurrentThread有没被关闭; 这两点有关。
4. 解决方案:
(1)使用完ThreadLocal,调用其remove方法删除对应的Entry,就能避免内存泄漏
(2)由于ThreadLocalMap是Thread的一个属性,被当前线程所引用,所以ThreadLocalMap的生命周期跟Thread一样长。所以,在使用完ThreadLcoal后当前Thread也随之结束,ThreadLocalMap也就自然会被GC回收,避免了内存泄漏;
5. 使用弱引用的原因
我们通过上面的描述知道了:无论ThreadLocalMap中的key使用什么引用方式,都无法完全避免泄漏。避免泄漏主要靠两种方式:
(1)使用完ThreadLocal,remove()掉对应的Entry;
(2)使用完ThreadLocal,让对应Thread也一起结束;
相对于第一种方式,第二种方式更不好控制。比如使用线程池时,线程使用完不会直接销毁。
那既然强弱引用都是同样的结果,为什么还是用弱引用呢?
事实上,在ThreadLocalMap中的set/getEntry中,会对Key = NULL(即ThreadLocal = NULL)的进行判定,如果为NULL,将会把value也置为NULL。
这就意味着:使用完Thread Local,CurrentThread仍然运行的情况下,就算忘记remove(),弱引用也会比强引用多一层保障。