简介
从名称看, ThreadLocal 也就是 thread 和 local 的组合, 也就是一个 thread 有一个 local 的变量副本
ThreadLocal 提供了线程的本地副本, 也就是说每个线程将会拥有一个自己独立的变量副本
方法简洁干练, 类信息以及方法列表如下
示例
在测试类中定义了一个 ThreadLocal 变量, 用于保存 String 类型数据
创建了两个线程, 分别设置值, 读取值, 移除后再次读取
- package test2;
- /**
- * Created by noteless on 2019/1/30. Description:
- */
- public class T21 {
- // 定义 ThreadLocal 变量
- static ThreadLocal<String> threadLocal = new ThreadLocal<>();
- public static void main(String[] args) {
- Thread thread1 = new Thread(() -> {
- //thread1 中设置值
- threadLocal.set("this is thread1's local");
- // 获取值
- System.out.println(Thread.currentThread().getName()+": threadLocal value:" + threadLocal.get());
- // 移除值
- threadLocal.remove();
- // 再次获取
- System.out.println(Thread.currentThread().getName()+": after remove threadLocal value:" + threadLocal.get());
- }, "thread1");
- Thread thread2 = new Thread(() -> {
- //thread2 中设置值
- threadLocal.set("this is thread2's local");
- // 获取值
- System.out.println(Thread.currentThread().getName()+": threadLocal value:" + threadLocal.get());
- // 移除值
- threadLocal.remove();
- // 再次获取
- System.out.println(Thread.currentThread().getName()+": after remove threadLocal value:" + threadLocal.get());
- }, "thread2");
- thread1.start();
- thread2.start();
- }
- }
执行结果
从结果可以看得到, 每个线程中可以有自己独有的一份数据, 互相没有影响
remove 之后, 数据被清空
从上面示例也可以看出来一个情况:
如果两个线程同时对一个变量进行操作, 互相之间是没有影响的, 换句话说, 这很显然并不是用来解决共享变量的一些并发问题, 比如多线程的协作
因为 ThreadLocal 的设计理念就是共享变私有, 都已经私有了, 还谈啥共享?
比如之前的消息队列, 生产者消费者的示例中
final LinkedList<Message> messageQueue = new LinkedList<>();
如果这个 LinkedList 是 ThreadLocal 的, 生产者使用一个, 消费者使用一个, 还协作什么呢?
但是共享变私有, 如同并发变串行, 或许适合解决一些场景的线程安全问题, 因为看起来就如同没有共享变量了, 不共享即安全, 但是他并不是为了解决线程安全问题而存在的
实现分析
在 Thread 中有一个 threadLocals 变量, 类型为 ThreadLocal.ThreadLocalMap
而 ThreadLocalMap 则是 ThreadLocal 的静态内部类, 他是一个设计用来保存 thread local 变量的自定义的 hash map
所有的操作方法都是私有的, 也就是不对外暴露任何操作方法, 也就是只能在 ThreadLocal 中使用了
此处我们不深入, 就简单理解为是一个 hash map, 用于保存键值对
也就是说 Thread 中有一个 "hashMap" 可以用来保存键值对
set 方法
看一下 ThreadLocal 的 set 方法
在这个方法中, 接受参数, 类型为 T 的 value
首先获取当前线程, 然后调用 getMap(t)
这个方法也很简单, 就是直接返回 Thread 内部的那个 "hashMap"(threadLocals 是默认的访问权限)
继续回到 set 方法, 如果这个 map 不为空, 那么以 this 为 key,value 为值, 也就是 ThreadLocal 变量作为 key
如果 map 为空, 那么进行给这个线程创建一个 map , 并且将第一组值设置进去, key 仍旧是这个 ThreadLocal 变量
简言之:
调用一个 ThreadLocal 的 set 方法, 会将: 以这个 ThreadLocal 类型的变量为 key, 参数为 value 的这一个键值对, 保存在 Thread 内部的一个 "hashMap" 中
get 方法
在 get 方法内部仍旧是获取当前线程的内部的这个 "hashMap", 然后以当前对象 this(ThreadLocal) 作为 key, 进行值的获取
我们对这两个方法换一个思路理解:
每个线程可能运行过程中, 可能会操作很多的 ThreadLocal 变量, 怎么区分各自?
直观的理解就是, 我们想要获取某个线程的某个 ThreadLocal 变量的值
一个很好的解决方法就是借助于 Map 进行保存, ThreadLocal 变量作为 key,local 值作为 value
假设这个 map 名为: threadLocalsMap, 可以提供 setter 和 getter 方法进行设置和读取, 内部为
- threadLocalsMap.set(ThreadLocal key,T value)
- threadLocalsMap.get(ThreadLocal key)
这样就是可以达到 thread --- local 的效果, 但是是否存在一些使用不便? 我们内部定义的是 ThreadLocal 变量, 但是只是用来作为 key 的? 是否直接通过 ThreadLocal 进行值的获取更加方便呢?
怎么能够做到数据读取的倒置? 因为毕竟值的确是保存在 Thread 中的
其实也很简单, 只需要内部进行转换就好了, 对于下面两个方法, 我们都需要 ThreadLocal key
- threadLocalsMap.set(ThreadLocal key,T value)
- threadLocalsMap.get(ThreadLocal key)
而这个 key 不就是这个 ThreadLocal, 不就是 this 么
所以:
ThreadLocal.set(T value) 就内部调用 threadLocalsMap.set(this,T value)
ThreadLocal.get() 就内部调用 threadLocalsMap.get(this)
所以总结下就是:
每个 Thread 内部保存了一个 "hashMap",key 为 ThreadLocal, 这个线程操作了多少个 ThreadLocal, 就有多少个 key
你想获取一个 ThreadLocal 变量的值, 就是 ThreadLocal.get(), 内部就是 hashMap.get(this);
你想设置一个 ThreadLocal 变量的值, 就是 ThreadLocal.set(T value), 内部就是 hashMap.set(this,value);
关键只是在于内部的这个 "hashMap",ThreadLocal 只是读写倒置的 "壳", 可以更简洁易用的通过这个壳进行变量的读写
"倒置" 的纽带, 就是 getMap(t) 方法
remove 方法
remove 方法也是简单, 当前线程, 获取当前线程的 hashMap,remove
初始值
再次回头看下 get 方法, 如果第一次调用时, 指定线程并没有 threadLocals, 或者根本都没有进行过 set
会发生什么?
如下图所示, 会调用 setInitialValue 方法
在 setInitialValue 方法中, 会调用 initialValue 方法获取初始值, 如果该线程没有 threadLocals 那么会创建, 如果有, 会使用这个初始值构造这个 ThreadLocal 的键值对
简单说, 如果没有 set 过 (或者压根内部的这个 threadLocals 就是 null 的), 那么她返回的值就是初始值
这个内部的 initialValue 方法默认的返回 null, 所以一个 ThreadLocal 如果没有进行 set 操作, 那么初始值为 null
如何进行初始值的设定?
可以看得出来, 这是一个 protected 方法, 所以返回一个覆盖了这个方法的子类不就好了? 在子类中实现初始值的设置
在 ThreadLocal 中提供了一个内部类 SuppliedThreadLocal, 这个内部类接受一个函数式接口 Supplier 作为参数, 通过 Supplier 的 get 方法获取初始值
Supplier 是一个典型的内置函数式接口, 无入参, 返回类型 T, 既然是函数式接口也就是可以直接使用 Lambda 表达式构造初始值了!!!
如何构造这个内部类, 然后进而进行初始化参数的设置呢?
提供了 withInitial 方法, 这个方法的参数就是 Supplier 类型, 可以看到, 这个方法将入参, 透传给 SuppliedThreadLocal 的构造方法, 直接返回一个 SuppliedThreadLocal
换句话说, 我们不是希望能够借助于 ThreadLocal 的子类, 覆盖 initialValue() 方法, 提供初始值吗?
这个 withInitial 就是能够达成目标的一个方法!
使用 withInitial 方法, 创建具有初始值的 ThreadLocal 类型的变量, 从结果可以看得出来, 我们没有任何的设置, 可以获取到值
稍作改动, 增加了一次 set 和 remove, 从打印结果看得出来, set 后, 使用的值就是我们新设置的
而一旦 remove 之后, 那么仍旧会使用初始值
注意:
对于 initialValue 方法的覆盖, 其实即使没有提供这个子类以及这个方法也都是可以的, 因为本质是要返回一个子类, 并且覆盖了这个方法
我们可以自己做, 也可以直接匿名类, 如下所示: 创建了一个 ThreadLocal 的子类, 覆盖了 initialValue 方法
ThreadLocal < 类型 > threadLocalHolder =new ThreadLocal < 类型 > () {
public 类型 initialValue() {
- return XXX;
- }
- };
但是很显然, 提供了子类和方法之后, 我们就可以借助于 Lambda 表达式进行操作, 更加简介
总结:
通过 set 方法可以进行值的设定
通过 get 方法可以进行值的读取, 如果没有进行过设置, 那么将会返回 null; 如果使用了 withInitial 方法提供了初始值, 将会返回初始值
通过 remove 方法将会移除对该值的写入, 再次调用 get 方法, 如果使用了 withInitial 方法提供了初始值, 将会返回初始值, 否则返回 null
对于 get 方法, 很显然如果没有提供初始值, 返回值为 null, 在使用时是需要注意不要引起 NPE 异常
ThreadLocal,thread local, 每个线程一份, 到底是什么意思?
他的意思是对于一个 ThreadLocal 类型变量, 每个线程有一个对应的值, 这个值的名字就是 ThreadLocal 类型变量的名字, 值是我们 set 进去的变量
但是如果 set 设置的是共享变量, 那么 ThreadLocal 其实本质上还是同一个对象不是么?
这句话如果有疑问的话, 可以这么理解
对于同一个 ThreadLocal 变量 a, 每个线程有一个 map,map 中都有一个键值对, key 为 a, 值为你保存的值
但是这个值, 到底每个线程都是全新的? 还是使用的同一个? 这是你自己的问题了!!!
ThreadLocal 可以做到每个线程有一个独立的一份值, 但是你非得使用共享变量将他们设置成一个, 那 ThreadLocal 是不会保障的
这就好比一个对象, 有很多引用指向他, 每个线程有一个独立的引用, 但是对象根本还是只有一个
所以, 从这个角度更容易理解, 为什么说 ThreadLocal 并不是为了解决线程安全问题而设计的, 因为他并不会为线程安全做什么保障, 他的能力是持有多个引用, 这多个引用是否能保障是多个不同的对象, 你来决策
所以我们最开始说的, ThreadLocal 会为每个线程创建一个变量副本的说法是不严谨的
是他有这个能力做到这件事情, 但是到底是什么对象, 还是要看你 set 的是什么, set 本身不会对你的值进行干涉
不过我们通常就是在合适的场景下通过 new 对象创建, 该对象在线程内使用, 也不需要被别的线程访问
如下图所示, 你放进去的是一个共享变量, 他们就是同一个对象
应用场景
前面说过, 对于之前生产者消费者的示例中, 就不适合使用 ThreadLocal, 因为问题模型就是要多线程之间协作, 而不是为了线程安全就将共享变量私有化
比如, 银行账户的存款和取款, 如果借助于 ThreadLocal 创建了两个账户对象, 就会有问题的, 初始值 500, 明明又存进来 1000 块, 可支配的总额还是 500
那 ThreadLocal 适合什么场景呢?
既然是每个线程一个, 自然是适合那种希望每个线程拥有一个的那种场景 (好像是废话)
一个线程中一个, 也就是线程隔离, 既然是一个线程一个, 那么同一个线程中调用的方法也就是共享了, 所以说, 有时, ThreadLocal 会被用来作为参数传递的工具
因为它能够保障同一个线程中的值是唯一的, 那么他就共享于所有的方法中, 对于所有的方法来说, 相当于一个全局变量了!
所以可以用来同一个线程内全局参数传递
不过要慎用, 因为 "全局变量" 的使用对于维护性, 易读性都是挑战, 尤其是 ThreadLocal 这种线程隔离, 但是方法共享的 "全局变量"
如何保障必然是独立的一个私有变量?
对于 ThreadLocal 无初始化设置的变量, 返回值为 null
所以可以进行判断, 如果返回值为 null, 可以进行对象的创建, 这样就可以保障每个线程有一个独立的, 唯一的, 特有的变量
示例
对于 Javaweb 项目, 大家都了解过 Session
ps: 此处不对 session 展开介绍, 打开浏览器输入网址, 这就会建立一个 session, 关闭浏览器, session 就失效了
在这一个时间段内, 一个用户的多个请求中, 共享同一个 session
Session 保存了很多信息, 有的需要通过 Session 获取信息, 有些又需要修改 Session 的信息
每个线程需要独立的 session, 而且很多地方都需要操作 Session, 存在多方法共享 Session 的需求, 所以 session 对象需要在多个方法中共享
如果不使用 ThreadLocal, 可以在每个线程内创建一个 Session 对象, 然后在多个方法中将他作为参数进行传递
很显然, 如果每次都显式的传递参数, 繁琐易错
这种场景就适合使用 ThreadLocal
下面的示例就模拟了多方法共享同一个 session, 但是线程间 session 隔离的示例
- public class T24 {
- /**
- * session 变量定义
- */
- static ThreadLocal<Session> sessionThreadLocal = new ThreadLocal<>();
- /**
- * 获取 session
- */
- static Session getSession() {
- if (null == sessionThreadLocal.get()) {
- sessionThreadLocal.set(new Session());
- }
- return sessionThreadLocal.get();
- }
- /**
- * 移除 session
- */
- static void closeSession() {
- sessionThreadLocal.remove();
- }
- /**
- * 模拟一个调用 session 的方法
- */
- static void fun1(Session session) {
- }
- /**
- * 模拟一个调用 session 的方法
- */
- static void fun2(Session session) {
- }
- public static void main(String[] args) {
- new Thread(() -> {
- fun1(getSession());
- fun2(getSession());
- closeSession();
- }).start();
- }
- /**
- * 模拟一个 session
- */
- static class Session {
- }
- }
所以, ThreadLocal 最根本的使用场景应该是:
在每个线程希望有一个独有的变量时 (这个变量还很可能需要在同一个线程内共享)
避免每个线程还需要主动地去创建这个对象 (如果还需要共享, 也一并解决了参数来回传递的问题)
换句话说就是,"如何优雅的解决: 线程间隔离与线程内共享的问题", 而不是说用来解决乱七八糟的线程安全问题
所以说如果有些场景你需要线程隔离, 那么考虑 ThreadLocal, 而不是你有了什么线程安全问题需要解决, 然后求助于 ThreadLocal, 这不是一回事
既然能够线程内共享, 自然的确是可以用来线程内全局传参, 但是不要滥用
再次注意:
ThreadLocal 只是具有这样的能力, 是你能够做到每个线程一个独有变量, 但是如果你 set 时, 不是传递的 new 出来的新变量, 也就只是理解成 "每个线程不同的引用", 对象还是那个对象 (有点像参数传递时的值传递, 对于对象传递的就是引用)
内存泄漏
ThreadLocal 很好地解决了线程数据隔离的问题, 但是很显然, 也引入了另一个空间问题
如果线程数量很多, 如果 ThreadLocal 类型的变量很多, 将会占用非常大的空间
而对于 ThreadLocal 本身来说, 他只是作为 key, 数据并不会存储在它的内部, 所以对于 ThreadLocal
ThreadLocalMap 内部的这个 Entity 的 key 是弱引用
如下图所示, 实线表示强引用, 虚线表示弱引用
对于真实的值是保存在 Thread 里面的 ThreadLocal.ThreadLocalMap threadLocals 中的
借助于内部的这个 map, 通过 "壳"ThreadLocal 变量的 get, 可以获取到这个 map 的真正的值, 也就是说, 当前线程中持有对真实值 value 的强引用
而对于 ThreadLocal 变量本身, 如下代码所示, 栈中的变量与堆空间中的这个对象, 也是强引用的
static ThreadLocal<String> threadLocal = new ThreadLocal<>();
不过对于 Entity 来说, key 是弱引用
当一系列的执行结束之后, ThreadLocal 的强引用也会消亡, 也就是堆与栈之间的从 ThreadLocal Ref 到 ThreadLocal 的箭头会断开
由于 Entity 中, 对于 key 是弱引用, 所以 ThreadLocal 变量会被回收 (GC 时会回收弱引用)
而对于线程来说, 如果迟迟不结束, 那么就会一直存在: Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value 的强引用, 所以 value 迟迟得不到回收, 就会可能导致内存泄漏
ThreadLocalMap 的设计中已经考虑到这种情况, 所以 ThreadLocal 的 get(),set(),remove() 的时候都会清除线程 ThreadLocalMap 里所有 key 为 null 的 value
以 get 方法为例
一旦将 value 设置为 null 之后, 就斩断了引用于真实内存之间的引用, 就能够真正的释放空间, 防止内存泄漏
但是这只是一种被动的方式, 如果这些方法都没有被调用怎么办?
而且现在对于多线程来说, 都是使用线程池, 那个线程很可能是与应用程序共生死的, 怎么办?
那你就每次使用完 ThreadLocal 变量之后, 执行 remove 方法!!!!
从以上分析也看得出来, 由于 ThreadLocalMap 的生命周期跟 Thread 一样长, 所以很可能导致内存泄漏, 弱引用是相当于增加了一种防护手段
通过 key 的弱引用, 以及 remove 方法等内置逻辑, 通过合理的处理, 减少了内存泄漏的可能, 如果不规范, 就仍旧会导致内存泄漏
总结
ThreadLocal 可以用来优雅的解决线程间隔离的对象, 必须主动创建的问题, 借助于 ThreadLocal 无需在线程中显式的创建对象, 解决方案很优雅
ThreadLocal 中的 set 方法并不会保障的确是每个线程会获得不同的对象, 你需要对逻辑进行一定的处理 (比如上面的示例中的 getSession 方法, 如果 ThreadLocal 变量的 get 为 null, 那么 new 对象)
是否真的能够做到线程隔离, 还要看你自己的编码实现, 不过如果是共享变量, 你还放到 ThreadLocal 中干嘛?
所以通常都是线程独有的对象, 通过 new 创建
来源: https://www.cnblogs.com/noteless/p/10373044.html