Day290&291.ThreadLocal -Juc
Posted 阿昌喜欢吃黄桃
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Day290&291.ThreadLocal -Juc相关的知识,希望对你有一定的参考价值。
ThreadLocal
一、使用场景
ThreadLocal: 当前线程本地可访问的一个副本
1、场景1:每个线程需要一个独享的对象
-
每个Thread类内有
自己
的实例副本,不共享
-
比喻:
教材只有一本,一起做笔记有线程安全问题。复印后就没有问题了
- 案例0: 2个线程分别用直接的SimpleDateFormat
public class ThreadLocalNormalUsage00 {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
ThreadLocalNormalUsage00 t = new ThreadLocalNormalUsage00();
System.out.println(t.date(10));;
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
ThreadLocalNormalUsage00 t = new ThreadLocalNormalUsage00();
System.out.println(t.date(1007));;
}
}).start();
}
public String date(int seconds){
//参数的单位为毫秒
Date date = new Date(1000 * seconds);
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
return format.format(date);
}
}
- 案例1: 10个线程分别用直接的SimpleDateFormat
public class ThreadLocalNormalUsage01 {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 30; i++) {
int num = i;
new Thread(new Runnable() {
@Override
public void run() {
ThreadLocalNormalUsage01 t = new ThreadLocalNormalUsage01();
System.out.println(t.date(num));
;
}
}).start();
Thread.sleep(100);
}
}
public String date(int seconds) {
//参数的单位为毫秒
Date date = new Date(1000 * seconds);
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
return format.format(date);
}
}
- 案例2: 1000个任务,用线程池打印时间
public class ThreadLocalNormalUsage02 {
public static ExecutorService pool = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int num = i;
pool.execute(new Runnable() {
@Override
public void run() {
ThreadLocalNormalUsage02 t = new ThreadLocalNormalUsage02();
System.out.println(t.date(num));
}
});
}
pool.shutdown();
}
public String date(int seconds) {
//参数的单位为毫秒
Date date = new Date(1000 * seconds);
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
return format.format(date);
}
}
这里出现了一个可优化的点
:
每一个线程都要创建一个SimpleDateFormat对象,然后使用结束后再销毁SimpleDateFormat对象,那这里就创建了1000个SimpleDateFormat对象,但是他们用的功能是相同的。我们能不能直接让他们共用一个SimpleDateFormat对象,这样子就减少对象的创建和销毁
案例3: 优化
:1000个任务,用线程池打印时间
那这里就使用静态成员类对象,去让他使用一个 SimpleDateFormat对象,避免重复的创建和销毁
发现打印的结果出现了线程安全问题
!!!,出现了一些打印一样的时间
↓↓每个线程去获取SimpleDateFormat对象并可能同时调用format()方法,又因为没有锁,就会导致获取SimpleDateFormat对象可能出现重复
案例3: 加锁
来解决线程安全问题
能使用volatile来解决问题吗???
原因是不符合volatile的使用场景,使用场景有两种,1:直接赋值场景 ,2:触发器场景
public class ThreadLocalNormalUsage04 {
public static ExecutorService pool = Executors.newFixedThreadPool(10);
public static SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int num = i;
pool.execute(new Runnable() {
@Override
public void run() {
ThreadLocalNormalUsage04 t = new ThreadLocalNormalUsage04();
System.out.println(t.date(num));
}
});
}
pool.shutdown();
}
public String date(int seconds) {
//参数的单位为毫秒
Date date = new Date(1000 * seconds);
String result = null;
synchronized (ThreadLocalNormalUsage04.class) {
result = ThreadLocalNormalUsage04.format.format(date);
}
return result;
}
}
以上的情况是解决了线程安全问题,当时在高并发的情况下,这里就会出现线程排队的问题,性能就不会很好,那还有更好的解决方案吗????
- 案例4: 使用ThreadlLocal
public class ThreadLocalNormalUsage05 {
public static ExecutorService pool = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int num = i;
pool.execute(new Runnable() {
@Override
public void run() {
ThreadLocalNormalUsage05 t = new ThreadLocalNormalUsage05();
System.out.println(t.date(num));
}
});
}
pool.shutdown();
}
public String date(int seconds) {
//参数的单位为毫秒
Date date = new Date(1000 * seconds);
// SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
//获取ThreadLocal中的SimpleDateFormat对象
SimpleDateFormat format = ThreadSafeFormatter.dateFormatThreadLocal.get();
return format.format(date);
}
}
class ThreadSafeFormatter {
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
//初始化
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
}
};
}
//lambta写法
class ThreadSafeFormatter2 {
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal.
withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));
}
2、场景2:每个线程内需要保存全局变量
- 一个比较繁琐的解决方案:
把user作为一个参数层层在方法中传递,但是这样会导致代码冗余且不易维护
现在可以每个线程内需要保存全局变量,可以让不同方法直接使用,避免参数传递的麻烦
- 最佳解决方案: 使用ThreadLocal
/******
@author 阿昌
@create 2021-06-06 19:35
*******
* 演示ThreadLocal用法2: 避免传递参数的麻烦
*/
public class ThreadLocalNormalUsage06 {
//主函数
public static void main(String[] args) {
Service1 service1 = new Service1();
service1.process();
}
}
//模拟业务类1,设置用户信息
class Service1{
public void process(){
User user = new User("阿昌");
//给ThreadLocal设置用户信息
UserHolder.threadLocal.set(user);
new Service2().process();
}
}
//模拟业务类2,获取用户信息
class Service2{
public void process(){
User user = UserHolder.threadLocal.get();
System.out.println("Service2,拿到用户名:"+user.name);
new Service3().process();
}
}
//模拟业务类3,获取用户信息
class Service3{
public void process(){
User user = UserHolder.threadLocal.get();
System.out.println("Service3,拿到用户名:"+user.name);
}
}
//ThreadLocal持有类
class UserHolder{
public static ThreadLocal<User> threadLocal = new ThreadLocal<>();
}
//实体类
class User{
String name;
public User(String name){
this.name = name;
}
}
二、ThreadLocal的作用
- 让某个需要用到的对象在
线程间隔离
(每个线程都有自己的独立的对象)
格子比作线程
- 在任何方法中都可以
轻松获取
到该对象
三、如何选择initValue & set 来保存对象
1、场景1:initValue
初始化时机是由我们控制的
- 在ThreadLocal
第一次get
的时候把对象给初始化出来,对象的初始化时机可以由我们控制
2、场景2:set
初始化时机是不由我们控制的
- 如果需要保存到ThreadLocal里的对象的生成时机
不由我们随意控制
,例如拦截器生成的用户信息,用ThreadLocal.set直接放到我们的ThreadLocal中去,以便后续的使用
四、ThreadLocal的好处
线程安全
不需要加锁
,提高执行效率
- 更高效的
利用内存、节省开销
:
- 更高效的
相比于每个任务都新建一个SimpleDateFormat,显然用ThreadLocal可以节省内存和开销
免去传参
的繁琐:
无论是场景一的工具类,还是场景二的用户名,都可以在任务地方直接通过ThreadLocal拿到,再也不需要在方法的形参中再定义传入相同的参数。ThreadLocal使的代码耦合度更低,更优雅。
五、ThreadLocal原理
讲ThreadLocal原理前,需要先搞清楚 【Thread】、【ThreadLocal】、【ThreadLocalMap】之间的关系
- 每一个Thread类,都
对应一个
ThreadLocalMap
- 一个ThreadLocalMap可以存储很多的ThreadLocal(KV键值对的形式)
- 一个线程可能拥有多个ThreadLocal对象
- ThreadLocal类里面有一个静态类ThreadLocalMap
他的成员变量为Entry,KV键值对存在
六、ThreadLocal的重要方法
1、initValue():初始化
- 该方法没有默认实现,且会返回当前线程对应的"初始值",这个是一个
延迟加载
的方法,只有在调用get
的时候,才会触发
让我们看看get()方法是不是调用了initVaule()
-
当线程
第一次使用get()
方法访问变量时,将调用initValue方法
,除非线程先在此之前调用了set()方法
。在这种情况下,不会为线程调用本initVaule()方法
-
如果使用set设置值
-
-
通常,每个线程
最多调用一次
initValue()方法即可。但如果已经调用了remove()后,再调用get(),则可以再次调用initValue()。 -
如果不重写initValue()方法,那就会返回null。一般使用匿名内部类的方法来
重写initValue()方法
,以便在后续使用汇总可以初始化副本对象
2、set():
为这个线程设置一个新值
- 这个map以及map中的key和value都是
保存在线程中对应的ThreadLocalMap
中的的,而不是保存在单个ThreadLocal中
3、get():
得到这个线程对象的value。如果是首次调用get(),则会调用initialize来得到这个值
- get是先获取到这个线程中的ThreadLocalMap对象,并用this(也就是这个ThreadLocal)作为Key参数传入,获取到这个key对应的Value值
4、remove()
删除对应这个线程的值
不是删除整个ThreadLocalMap对象,而是根据this(也就是当前ThreadLocal对象)来删除对应的threadLocal对象
七、ThreadLocalMap类
- ThreadLocalMap类,也就是Thread.threadLocals
八、ThreadLocal使用注意点
1、*内存泄露问题
- 内存泄露:
某个对象不再有用,但是占用的内存却不能被回收
- Key弱引用:
- Value泄露:
正常情况下,线程运行终止后,后保存在ThreadLocal里的value会被垃圾回收机制回收,因为没有任何强引用了
如果,使用的是线程池的情况,那么这个线程就不会被终止,而是一直被使用,就会出现大量value没被回收的可能性
调用链:
因为线程池的情况,Thread不被线程池终止,一直保存运行,所以他下面的的ThreadLocalMap、Enrty等…都不会被回收,就算key为null,因为key是弱引用,value是强引用,那么如果value不是null,但是key是null,那就会出现key已经被GC回收了,而value还没被回收的情况,出现了
内存的泄露问题
—>导致OOM
JDK考虑到这个问题,所以当key为null时,就将value也置为null
但是这要求,我们调用rehash、remove、set等方法才会去进行if判断设置值
但是一般情况下,当我们设置了这个threadlocal值后就不会在去调用set/remove/rehash等方法进行设置了,一般只会去get获取这个值
2、如何避免内存泄露问题?
每次当使用完threadlocal后就调用remove方法去删除这个对应的entry对象,可以去避免内存泄露问题
所以,当我们使用完ThreadLocal之后,就去调用remove()方法去删除对应的entry对象
3、*空指针异常问题
- 代码演示:
public class ThreadLocalNPE {
ThreadLocal<Long> tl = new ThreadLocal();
public void set(){
tl.set(Thread.currentThread().getId());
}
public long get(){
return tl.get();
}
public static void main(String[] args) {
ThreadLocalNPE item = new ThreadLocalNPE();
System.out.println(item.get());
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
item.set();
System.out.println(item.get());
}
});
thread.start();
}
}
这里的,get()方法出现了NPE异常,那为什么呢?
因为 initValue 返回的值是 T,那么上面的get方法返回的是long基本类型,也就是说他会先返回Long,然后自动拆箱,然后再返回long基本类型;这里就出现了错误,因为返回的是null了,那么再对null进行拆箱返回基本类型,就会出现空指针这异常!!!
通过修改get()方法的返回值 ,从long —> Long,就可以解决问题
当我们第一次去调用threadlocal对象的get()方法, 当然他返回的是null值,但是我们操作返回不当,上面的例子有出现隐式的拆箱环节,那么就可能出现null对象再进行拆箱,出现了NPE异常
4、*共享对象
5、如果可以不使用ThreadLocal解决问题,那么就不要强行使用
九、Spring中使用ThreadLocal的场景
- DateTimeContextHolder:
保存一系列的时间上下文
- RequestContextholder:
保存一系列的请求头,请求参数等…
以上是关于Day290&291.ThreadLocal -Juc的主要内容,如果未能解决你的问题,请参考以下文章
《安富莱嵌入式周报》第290期:开源静电便携测试仪,开源音频功放,CAN高波特率设计,超级铁电产品,小米Vela系统,65W USB PD充电器参考设计