满怀忧思,不如先干再说!做干净纯粹的技术分享!欢迎评论区或私信交流!
[酷][酷][酷]ThreadLocal在工作和面试中都非常常见,本篇文章收录于《Java并发编程》合集,通过2-3篇文章介绍清楚ThreadLocal的应用,原理,案例,达到实战会用,面试会说的效果。通过本篇你可以得到[奸笑]:
- ThreadLocal的正确解释和作用
- ThreadLocal的API以及三种初始化方式的对比
- 通过ThreadLocal实现春节红包案例
- 通过线程池引出ThreadLocal隐患和解决方案
- 总结ThreadLocal特点和应用场景
喜欢的话记得动动手指点关注,升职加薪不迷路,长期稳定输出干货技术文章
ThreadLocal
对于ThreadLocal的翻译有很多,如:线程本地,本地线程,如果根据他的作用来说翻译成【线程局部变量】我认为是比较合适的
ThreadLocal作用
ThreadLocal是java.lang包中的一个泛型类,可以实现为线程创建独有变量,这个变量对于其他线程是隔离的,也就是线程本地的值,这也是ThreadLocal名字的来源
每个使用该变量的线程都要初始化一个完全独立的实例副本,不存在多线程间共享的问题
ThreadLocal方法
ThreadLocal用来存储当前线程的独有数据,相关API就是存值,取值,清空值的简单操作
- withInitial:创建一个ThreadLocal实例,并给定初始值【JDK8推出的新方法,一般都是用该方法初始化ThreadLocal】
- get:返回当前线程ThreadLocal的值,如果没有设置值返回null
- set:设置当前线程ThreadLocal的值
- remove:删除当前线程ThreadLocal的值
- initialValue:此方法默认返回null, 通过ThreadLocal构造方法初始化时一般重写此方法,来设置初始值,在JDK8之后通过withInitial方法初始化
初始化ThreadLocal
初始化ThreadLocal有三种方式:
- 直接通过构造方法创建,此时初始值为null
- 通过构造方法同时重写initialValue方法给定初始值
- 通过JDK8的withInitial()静态方法创建,可以通过Lambda直接给初始值【推荐使用】
通过构造方法
@Test
public void test1() {
// 通过构造方法,初始化ThreadLocal
ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
// 未给定初始值,通过get方法获取值为null
System.out.println(threadLocal.get());
}
通过get方法获取结果为null
如果想要设置值,则需要通过set方法
@Test
public void test1() {
// 初始化ThreadLocal
ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
System.out.println("设置前===》" + threadLocal.get());
// 设置初始值
threadLocal.set(1);
System.out.println("设置后===》" + threadLocal.get());
}
此时获取的值就是1
重写initialValue方法
该方法的主要作用是返回当前线程ThreadLocal的初始值,但是在ThreadLocal中默认实现返回为null
这个值和具体的泛型类型有关,通常需要根据实际需求重写此方法,定义初始值
@Test
public void test2() {
// 初始化ThreadLocal,重写initialValue方法设置默认值
ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
// 设置,初始值为1024
return 1024;
}
};
// 启动5个线程
for (int i = 0; i < 5; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + ":threadLocal初始值--->" + threadLocal.get());
},"线程:" + i).start();
}
}
启动5个线程每个线程的初始值都为1024
withInitial静态方法
上方两种方案对创建ThreadLocal时给定初始值都稍显繁琐,在JDK8中新增了withInitial静态方法接收Supplier供给型函数接口设置初始值
@Test
public void test3() {
// 通过withInitial方法设置初始值
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> {
return 100;
});
// 启动5个线程
for (int i = 0; i < 5; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + ":threadLocal初始值--->" + threadLocal.get());
},"线程:" + i).start();
}
}
启动5个线程之后初始值为100,强烈推荐此种方式来实现ThreadLocal的初始化
推荐通过withInitial静态方法实现ThreadLocal的初始化
如果线程内需要修改值则可以使用set方法,如果需要获取值则使用get方法
结合下图搞懂ThreadLocal数据存储:一个ThreadLocal实例,在每个线程中都有独自的初始化副本,接下来每个线程对ThradLocal的操作都在线程内,对其他线程隔离
[吐舌]ThreadLocal的数据结构下一篇介绍,知道的小伙伴不妨写在评论区
父母保管孩子春节红包案例[白眼]
春节期间孩子收到的红包都会被父母暂时保管,答应长大后归还,比如,小翠有三个孩子小明,晓明和小茗,为了将来分账,需要单独记录孩子们收到的红包金额
@Test
public void test5() {
// 通过withInitial方法设置初始红包为0
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
// 设置随机数
Random random = new Random();
// 通过map集合映射线程名字
Map<Integer, String> map = new HashMap<>();
map.put(0,"小明");
map.put(1,"晓明");
map.put(2,"小茗");
// 启动3个线程,分别为3个孩子
for (int i = 0; i < 3; i++) {
new Thread(() -> {
// 去七大姑八大姨家拜年,比如5家亲戚吧
for (int j = 0; j < 5; j++) {
// 每家亲戚给随机的200以内的红包
int yasuiqian = random.nextInt(200);
// 红包金额加1
threadLocal.set(yasuiqian + threadLocal.get());
}
System.out.println(Thread.currentThread().getName() + "共收到:" + threadLocal.get() + "元红包");
},map.get(i)).start();
}
}
三个孩子去5个亲戚家,收到的红包金额实现独自记录
线程池实现保管红包
项目开发过程中对于多线程的场景都推荐使用线程池实现,可以避免线程频繁创建和销毁的资源浪费,使用线程池是一定要记得在finally代码块中关闭线程池
如下:开启一个有三个核心线程的线程池来处理3个孩子的拜年领红包任务
@Test
public void test6() {
// 通过withInitial方法设置初始值
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
// 设置随机数
Random random = new Random();
Map<Integer, String> map = new HashMap<>();
map.put(0,"小明");
map.put(1,"晓明");
map.put(2,"小茗");
// 开启有3个核心线程的线程池
ExecutorService threadPool = Executors.newFixedThreadPool(3);
try {
// 3个孩子
for (int i = 0; i < 3; i++) {
// 记录当前循环值,映射对应的孩子名字
int index = i;
threadPool.submit(() -> {
// 设置线程名:Thread.currentThread().getName()为默认的线程池给的名字,方便查看是哪一个线程执行的此任务
Thread.currentThread().setName(Thread.currentThread().getName() + map.get(index));
// 去七大姑八大姨家拜年,比如5家亲戚吧
for (int j = 0; j < 5; j++) {
// 每家亲戚给随机的200以内的红包
int yasuiqian = random.nextInt(200);
// 红包金额加1
threadLocal.set(yasuiqian + threadLocal.get());
}
System.out.println(Thread.currentThread().getName() + "共收到:" + threadLocal.get() + "元红包");
});
}
}catch (Exception e) {
e.printStackTrace();
}finally {
// 关闭线程池
threadPool.shutdown();
}
}
运行程序的电脑为8核,可以通过线程池中的3个核心线程,同时执行3条任务,此时的结果没有任何问题
线程池中线程可复用引发的问题
如果此时又多出来一个孩子,或者核心线程数变为2,即任务数大于核心线程数,会复用线程处理其他任务,即一个线程需要处理多个任务,这里减少核心线程数为例演示:
@Test
public void test6() {
......
// 修改线程池核心线程数为2,其他不变
ExecutorService threadPool = Executors.newFixedThreadPool(2);
......
}
此时发现小明和小茗都是通过1号线程执行的任务,每个亲戚最多发200红包,5个亲戚,最大值应该为1000,但是小茗收到了1172元的红包,这肯定是不对的
原因在于:1号线程处理完小明之后,发现小茗任务没有执行,此时1号线程处理两个任务,ThreadLocal也还是同一个,即处理小茗任务时,ThreadLocal的值为小明任务处理后的值,并不是初始值0
在阿里Java开发规范中强制要求回收自定义的ThreadLocal变量
[机智]如果有需要《阿里开发规范手册》的评论区留言或者私信免费获取
通过try块将任务逻辑包裹,在finally中通过remove方法回收该任务执行后的ThreadLocal值
@Test
public void test7() {
......
try {
......
try {
for (int j = 0; j < 5; j++) {
// 每家亲戚给随机的200以内的红包
int yasuiqian = random.nextInt(200);
// 红包金额加1
threadLocal.set(yasuiqian + threadLocal.get());
}
System.out.println(Thread.currentThread().getName() + "共收到:" + threadLocal.get() + "元红包");
} catch (Exception e) {
e.printStackTrace();
} finally {
// 回收数据
threadLocal.remove();
}
}catch (Exception e) {
e.printStackTrace();
}finally {
// 关闭线程池
threadPool.shutdown();
}
}
回收ThreadLocal变量后,复用该线程也不会对后续程序造成影响
ThreadLocal特点
- 统一设置初始值,每个线程可以通过set方法设置值,也可以通过get方法获取当前值
- ThreadLocal被每一个线程单独持有副本,相互独立,只能在该线程内部使用
- 如果配合线程池使用,线程可复用,需要调用remove方法回收数据,即重新设置为初始值,避免对后续程序造成影响和内存泄漏
- ThreadLocal变量因为线程独立,所以不在线程安全问题
ThreadLocal应用场景
如上文所述,ThreadLocal 适用于如下两种场景
- 每个线程需要自己独立的数据
- 数据在线程内的共享,不需要在多线程之间共享
如:
- 游戏玩家个人的属性,装备,积分等
- Spring中也通过ThreadLocal解决线程安全问题,在同一次请求响应的调用线程中,所有对象所访问的同一ThreadLocal变量都是当前线程所绑定的
[666]本篇达成实战会用,下一篇通过原理介绍,实现面试会说!