难度
初级
学习时间
30分钟
适合人群
零基础
开发语言
Java
开发环境
- JDK v11
- IntelliJIDEA v2018.3
友情提示
- 本教学属于系列教学,内容具有连贯性,本章使用到的内容之前教学中都有详细讲解。
- 本章内容针对零基础或基础较差的同学比较友好,可能对于有基础的同学来说很简单,希望大家可以根据自己的实际情况选择继续看完或等待看下一篇文章。谢谢大家的谅解!
1.温故知新
前面在《“全栈2019”Java多线程第四十八章:读写锁实战高并发容器》一章中介绍了如何使用读写锁实战简易版高并发容器。
在《“全栈2019”Java多线程第四十九章:LockSupport简单介绍》一章中介绍了LockSupport的阻塞线程park()方法和唤醒线程unpark()方法。
在《“全栈2019”Java多线程第五十章:设置/获取LockSupport同步对象》一章中介绍了LockSupport的设置同步对象park(Object blocker)方法和获取同步对象getBlocker(Thread t)方法。
在《“全栈2019”Java多线程第五十一章:LockSupport线程等待时间》一章中介绍了LockSupport的超时自动唤醒阻塞线程parkNanos(long nanos)方法和可设置同步对象的超时自动唤醒阻塞线程parkNanos(Object blocker, long nanos)方法。
在《“全栈2019”Java多线程第五十二章:LockSupport等待截止时间》一章中介绍了LockSupport的超过截止时间时自动唤醒阻塞线程parkUntil(long deadline)方法和可设置同步对象的超过截止时间时自动唤醒阻塞线程parkUntil(Object blocker, long deadline)方法。
现在介绍线程本地变量ThreadLocal。
2.一个问题认识到ThreadLocal重要性
在介绍线程本地变量ThreadLocal之前,先来看看共享变量。
什么是共享变量?
就是被多个线程所共享的变量。
为什么先看看共享变量?
因为共享变量线程间会相互干扰,而线程各自拥有的变量不会受到干扰,所以先来看看共享变量。
创建一个共享变量(只要是能被多个线程访问的变量,定义在哪都可以),这里就创建一个closed变量,作用是记录链接是否关闭,这个链接大家怎么理解都成,你可以理解成网络链接或者是数据库链接或者是你和男/女朋友正在聊天的链接:
然后,创建一个线程并重写run()方法:
接着,在run()方法中不停地输出(输出什么都可以,反正就是打印语句),因为是不停地输出,所以这里我们采用while循环,循环条件是当链接未关闭时执行循环体:
为了避免打印太快,这里我们在每次输出完一句话之后使当前线程睡1秒钟:
run()方法书写完毕。
最后,启动线程:
运行程序,执行结果:
从运行结果来看,符合预期。程序在不停地运行。
接下来,发生了一件不幸的事情,就是你正和你的男/女朋友聊的正Hi时,另一个人把路由器关了,链接中断,你聊不了。
这就是链接受到了干扰,即共享变量被另一个线程所更改。
依上所述,修改例子。
再创建一个线程并重写run()方法:
然后,在run()方法中将closed的值设置为true:
run()方法书写完毕。
最后,启动线程:
只不过不马上启动thread2线程,要等3秒钟之后再启动它,目的就是为了演示当一个线程正在执行任务时,一段时间后另一个线程突然对其进行干扰:
例子改写完毕。
运行程序,执行结果:
从运行结果来看,符合预期。thread2线程在3秒钟之后将closed值改为true,thread1线程随即执行完毕。
以上情形就是线程间相互干扰造成的。
解决这个问题办法有很多种,说其中一种吧,就是让两个线程各自持有一个closed变量,你操作你的closed,我操作我的closed,互不干涉。
如上所述,更改例子。
先将之前的closed变量移除掉:
然后,在thread1线程内部定义一个closed变量:
接着,在thread2线程内部也定义一个closed变量:
例子改写完毕。
运行程序,执行结果:
从运行结果来看,符合预期。thread1线程并没有被thread2线程影响。
针对上述情况,Java也为我们提供了相应的解决方案:将变量存在线程中。
这样一来,存在线程里面的变量就是原变量的副本。
什么是副本?
就是大家平时用的复制-粘贴,一下就出现跟原来一摸一样的东西。在程序中,就是创建好变量之后有多处保存它的内存地址,除了原变量保存之外,其他的都是该变量的副本。
如果有多个线程,变量它怎么知道该存在哪个线程?
哪个线程执行存储方法,那么变量就存在哪个线程里面。
你比如说,我们在主线程中执行存储变量的方法(以下代码是伪代码,只作演示用):
那么这个10就存在主线程中。
究竟怎么存呢?
我们下面来看看线程本地变量ThreadLocal。
3.什么是线程本地变量ThreadLocal?
线程本地变量分为三部分来看:
- 线程
- 本地
- 变量
其中,线程指的是操作ThreadLocal对象的线程;
本地指的是将变量存在线程中。
变量指的是变量的副本。
综上所述,ThreadLocal的作用就是为变量在线程中创建一份副本。
还不理解“将变量存在线程中”?
简单的不能再简单,就是这样:
当然了,实际操作肯定不是这样,只不过这夸张了点,但大体意思就是这个意思。线程里面也是有一个变量来记录我们要存储的变量。
ThreadLocal是一个类:
类中有三个常用方法:
- T get()
- void set(T value)
- void remove()
顾名思义,get()方法就是获取ThreadLocal在当前线程中保存的变量副本。
set(T value)方法就是设置ThreadLocal在当前线程中保存的变量副本。
remove()方法就是删除ThreadLocal在当前线程中保存的变量副本。
下面,我们演示一下这几个方法。
4.设置ThreadLocal在当前线程中保存的变量副本set(T value)方法
我们可以调用ThreadLocal类的set(T value)方法来设置ThreadLocal在当前线程中保存的变量副本。
set(T value)方法在ThreadLocal类中的源码:
注释全文:
设置ThreadLocal在当前线程中保存的变量副本。
去掉注释版:
set(T value)方法作用是设置ThreadLocal在当前线程中保存的变量副本。
访问权限
public:set(T value)方法是公开的。
void:set(T value)方法无返回值。
set(T value)方法只能被对象调用。
参数
T:ThreadLocal在当前线程中保存的变量副本。
抛出的异常
无
应用
先来解释一下什么叫“设置ThreadLocal在当前线程中保存的变量副本。”?
ThreadLocal?
因为存储变量副本的数据结构是键值对,而键的类型就是ThreadLocal,所以这里的ThreadLocal指的就是把ThreadLocal对象作为key。
当前线程?
就是执行set(T value)方法的线程。
综上所述,就是以ThreadLocal对象为key,以set(T value)方法中的实际参数为value的形式存入线程中。
下面,我们就来试试set(T value)方法。
还是上一小节例子,我们先创建出ThreadLocal对象,也就是key:
然后,调用ThreadLocal对象的set(T value)方法为变量在当前线程中创建一个副本:
现在我们先不着急获取,也不着急运行程序,等我们下面把get()方法介绍完再一起进行。
5.获取ThreadLocal在当前线程中保存的变量副本get()方法
我们可以调用ThreadLocal类的get()方法来获取ThreadLocal在当前线程中保存的变量副本。
get()方法在ThreadLocal类中的源码:
注释全文:
获取ThreadLocal在当前线程中保存的变量副本。
去掉注释版:
get()方法作用是获取ThreadLocal在当前线程中保存的变量副本。
访问权限
public:get()方法是公开的。
T:get()方法无返回ThreadLocal在当前线程中保存的变量副本。
get()方法只能被对象调用。
参数
无
抛出的异常
无
应用
下面,我们就来试试get()方法。
还是上一小节例子,既然有了ThreadLocal,那就把各个线程里面的closed变量删掉:
thread1线程中,将while循环条件中的“!closed”变量替换为“!closedKey.get()”:
thread2线程中,因为只是设置closed变量为true,所以我们只需调用closedKey.set(T value)方法即可:
例子改写完毕。
运行程序,执行结果:
静图:
文字版:
Exception in thread "Thread-0" java.lang.NullPointerException
at main.Main$1.run(Main.java:20)
从运行结果来看,不符合预期。程序出了问题。
为什么会产生NullPointerException异常?
NullPointerException异常产生在Main.java的第20行代码处:
即“closedKey.get()”这句代码上。
为什么closedKey.get()执行结果会为null呢?
因为执行closedKey.get()线程上根本就没有一个key为closedKey的ThreadLocal对象,更别说所对应的value,所以closedKey.get()执行结果为null。
解决办法就是我们必须让thread1线程执行closedKey.set(T value)方法,这样value的值才会存到thread1线程中,获取时才有值。
改写例子,将“closedKey.set(true)”移至thread1线程run()方法中即可:
例子改写完毕。
运行程序,执行结果:
从运行结果来看,符合预期。在thread1线程里面设置/获取closed变量副本,在thread2线程里面设置closed变量副本,两个线程相互不干扰,各自拥有一个变量副本。
6.删除ThreadLocal在当前线程中保存的变量副本remove()方法
我们可以调用ThreadLocal类的remove()方法来删除ThreadLocal在当前线程中保存的变量副本。
remove()方法在ThreadLocal类中的源码:
注释全文:
删除ThreadLocal在当前线程中保存的变量副本。
去掉注释版:
remove()方法作用是删除ThreadLocal在当前线程中保存的变量副本。
访问权限
public:remove()方法是公开的。
void:remove()方法无返回值。
remove()方法只能被对象调用。
参数
无
抛出的异常
无
应用
下面,我们来试试remove()方法。
还是上一小节例子,在while循环的结尾处调用set(T value)方法将closed变量设置为true:
然后,在while循环下面调用remove()方法将closedKey变量副本从当前线程移删掉:
最后,我们再获取一次closedKey试试:
例子改写完毕。
运行程序,执行结果:
从运行结果来看,符合预期。当我们调用remove()方法删除ThreadLocal在当前线程中保存的变量副本后,再去获取到变量副本就为null。
7.ThreadLocal没有处理并发的能力
这里大家需要注意一点:ThreadLocal没有处理并发的能力。
因为并发问题最起码是多个线程操作同一个或多个数据,但ThreadLocal是为每个线程保存各自的变量副本,每个线程都玩自己的变量,线程间互不干涉,所以ThreadLocal没有处理并发的能力。
8.ThreadLocal应用场景
因为ThreadLocal有为每个变量在线程中场景副本的能力,所以隔离线程间的操作数据就可以交给ThreadLocal来处理。
比如,数据库链接,每次可直接保存在线程变量中。
一是可以防止我正在使用链接时,其他线程来把我的链接给关闭了;
二是线程随时可获取,例如线程在执行A()方法时需要用到数据库链接,后面在执行B()方法时也需要用到数据库链接,这个时候我们用变量副本来传递数据库链接再合适不过了。
总之,线程在执行期间经常需要用到的值可以使用ThreadLocal来解决。
最后,希望大家可以把这个例子照着写一遍,然后再自己默写一遍,方便以后碰到类似的面试题可以轻松应对。
祝大家编码愉快!
GitHub
本章程序GitHub地址:https://github.com/gorhaf/Java2019/tree/master/Thread/ThreadLocal
总结
- get()方法作用是获取ThreadLocal在当前线程中保存的变量副本。
- set(T value)方法作用是设置ThreadLocal在当前线程中保存的变量副本。
- remove()方法作用是删除ThreadLocal在当前线程中保存的变量副本。
至此,Java中ThreadLocal相关内容讲解先告一段落,更多内容请持续关注。
答疑
如果大家有问题或想了解更多前沿技术,请在下方留言或评论,我会为大家解答。
上一章
“全栈2019”Java多线程第五十二章:LockSupport等待截止时间
下一章
- 本章为《“全栈2019”Java多线程》系列最后一章,接下来是《“全栈2019”Java原子操作》系列。
- 《“全栈2019”Java多线程》系列全部文章都在《“全栈2019”53篇Java多线程学习资料及总结》一文中。
学习小组
加入同步学习小组,共同交流与进步。
- 方式一:关注头条号Gorhaf,私信“Java学习小组”。
- 方式二:关注公众号Gorhaf,回复“Java学习小组”。
全栈工程师学习计划
关注我们,加入“全栈工程师学习计划”。
版权声明
原创不易,未经允许不得转载!