对ThreadLocal的深入理解
Posted Putarmor
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了对ThreadLocal的深入理解相关的知识,希望对你有一定的参考价值。
ThreadLocal
1.ThreadLocal初识
讲ThreadLocal之前,我们先回顾一下线程不安全的解决方案:
1.给对象加锁(synchronized加锁或Lock加锁)
2.让线程拥有私有变量,不让多个线程修改一个变量
对于1方式而言,加锁使得线程排队执行,具有性能消耗;对于2方式而言,设置私有变量的方法有一个缺陷,当任务数量比较多时,每一次任务执行都会创建私有变量,这会导致大量内存被占用,浪费空间。
因而Java中有一种比较高效的解决线程安全的方案:ThreadLocal
ThreadLocal是线程级别的私有变量,与任务级别的私有变量是完全不同的!
举例:
创建一个线程池,核心线程数与最大线程数都是10,任务数为1000
Public void fun(){
Object obj = new Object();
//...
}
对于任务级别的私有变量而言,执行1000次任务同时也会创建1000次变量;如果是ThreadLocal线程级别私有变量,每个线程拥有一个变量,执行1000次任务过程中,因为10个线程实现了复用,所以只创建了10次变量。
2.ThreadLocal方法
1)基本方法
1.set:将变量设置到线程中
2.get:从线程中取得私有变量
3.remove:从线程中移除私有变量(不移除会导致脏读、幻读)
4.initialValue:初始化
5.withInitial:初始化
ThreadLocal使用:
public class ThreadPool13 {
// 创建 ThreadLocal
static ThreadLocal<String> threadLocal =
new ThreadLocal<>();
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try {
// set threadLocal
String tname = Thread.currentThread().getName();
threadLocal.set(tname);
System.out.println(String.format("线程%s 设置了值:%s",
tname, tname));
printTName();
} finally {
threadLocal.remove();
}
}
}, "1");
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
String tname = Thread.currentThread().getName();
// set ThreadLocal
threadLocal.set(tname);
System.out.println(String.format("线程%s 设置了值:%s",
tname, tname));
printTName();
} finally {
threadLocal.remove();
}
}
}, "2");
t2.start();
}
private static void printTName() {
String tname = Thread.currentThread().getName();
// 得到存放在 threadLoca 中的值
String result = threadLocal.get();
System.out.println(String.format("线程%s 取得:%s",
tname, result));
}
}
2)initialValue()的使用
public class ThreadPool14 {
static ThreadLocal<String> threadLocal = new ThreadLocal(){
@Override
protected Object initialValue() {
System.out.println("执行了初始化方法");
return "trq";
}
};
public static void main(String[] args) {
String result = threadLocal.get();
System.out.println("读取到的内容是:"+result);
}
}
注意事项:initialValue返回的数据类型需要和ThreadLocal定义的泛型类型相同。
public class ThreadPool14 {
static ThreadLocal<String> threadLocal = new ThreadLocal(){
@Override
protected Object initialValue() {
System.out.println("执行了初始化方法");
return "trq";
}
};
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(1,1,0, TimeUnit.SECONDS,
new LinkedBlockingDeque<>(1000));
for(int i = 0; i < 2; i++){
executor.execute(new Runnable() {
@Override
public void run() {
String result = threadLocal.get();
System.out.println("读取到的内容是:"+result);
}
});
}
}
}
我们发现initialValue与get可以一起操作,实现正常存取
那么当initialVale与set、get一起操作行不行呢?
答案:不可以。当三个方法都存在时,只会执行set与get,当set后initialValue方法不会被执行。
为什么不会执行呢?
ThreadLocal和线程池一样具有懒加载机制,在创建的时候不能立刻去执行,只是标识其他任务可以调用自己;在创建ThreadLocal并编写初始化方法时没有去执行实例化,其他地方调用ThreadLocal方法后才会去加载。从源码中可以看出,当执行了get方法后,才会去尝试执行initialValue方法;尝试获取ThreadLocal的set方法值,如果获取到了,那么初始化方法永远不被执行。
3)withInitial()使用
public class ThreadPool15 {
//因为withInitial是静态方法,所以无需new操作
static ThreadLocal<String> threadLocal =
ThreadLocal.withInitial(new Supplier<String>() {
@Override
public String get() {
System.out.println("执行了初始化方法!");
return "Java"; //初始化时候返回的值
}
});
public static void main(String[] args) {
try{
String result = threadLocal.get();
System.out.println(result);
}finally{
threadLocal.remove();
}
String str = threadLocal.get();
System.out.println(str);
}
}
!!!remove在任何场景下都可以使用
程序执行结果如下:
可以看出withInitial完成了ThreadLocal初始化;此外当我们执行remove后,再一次执行get发现依然执行了初始化的结果,原因就是与源码设计存在关系,get方法中,当map为null时,会去执行SetInitial方法。
3.ThreadLocal使用场景
1)解决线程安全问题
需求:实现1000个任务的格式化
当没有ThreadLocal时使用线程池完成任务,会导致线程不安全发生:
当使用ThreadLocal时,解决了线程安全:
分析:可以看出,程序运行结果就是我们的预期结果;初始化方法执行了10次,而不是设置任务的1000次,因此可以说,ThreadLocal变量被线程复用,它是线程级别的私有变量而不是任务级别的。
2)实现线程级别的数据传递
/**
* 使用场景2:实现线程级别的数据传递
*/
public class ThreadPool17 {
static class ThreadLocalUnit{
static ThreadLocal<User> threadLocal = new ThreadLocal<>(); //!!只写成静态属性的话只能在当前类中访问
}
static class User{
private String username;
public String getUsername(){
return username;
}
public void setUsername(String username){
this.username = username;
}
}
/*
日志信息
*/
static class Logger{
public void addLog(){
User user = ThreadLocalUnit.threadLocal.get();
System.out.println("添加日志:"+user.getUsername());
}
}
/*
订单系统
*/
static class Order{
public void getOrder(){
User user = ThreadLocalUnit.threadLocal.get();
System.out.println("订单列表:"+user.getUsername());
}
}
public static void main(String[] args) {
//模拟用户的登录操作
User user = new User();
user.setUsername("TRQ");
ThreadLocalUnit.threadLocal.set(user);
//调用订单系统
Order order = new Order();
order.getOrder();
//调用日志系统
Logger logger = new Logger();
logger.addLog();
}
}
4.ThreadLocal缺点
1)父子线程间的不可继承性
从执行结果可以看出,子线程没有获取到主线程中设置的ThreadLocal变量,这就是不可继承性的体现。
怎么解决不可继承性?
解决方法:将要实例化的ThreadLocal类改为它的子类InheritableThreadLocal,这样就解决不可继承性问题。
static ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
上述代码修改后执行结果:
2)不能数据共享
InheritableThreadLocal不能实现不同子线程之间的数据共享
从结果可以看出,线程2没有获取到线程1设置的ThreadLocal变量。
3)脏读问题
一个线程读到了不属于自己的数据,这就出现了脏读现象。
public class ThreadPool20 {
static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
ThreadPoolExecutor executor =
new ThreadPoolExecutor(1, 1, 0,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));
for (int i = 0; i < 2; i++) {
MyTask t = new MyTask();
executor.execute(t);
}
}
static class MyTask extends Thread {
// 标志位,标识是否第一次访问
static boolean first = true;
@Override
public void run() {
if (first) {
// 第一次访问,存储用户信息
threadLocal.set(this.getName() +
" session info.");
first = false;
}
// get threadLocal
String result = threadLocal.get();
System.out.println(this.getName() + ":" + result);
}
}
}
线程1读到了线程0的信息,导致脏读发生。
>>脏读原因
原因:
线程池中使用ThreadLocal出现脏读是因为线程池会复用线程,复用线程时就会复用线程的静态属性,从而导致某些方法不能执行;对于线程而言,使用ThreadLocal不会出现脏读,因为每个线程使用的是自己的私有变量和ThreadLocal。
>>解决脏读办法
1.避免使用静态属性(线程池中静态属性会复用)
2.使用ThreadLocal的remove方法(坚持使用静态属性的情况下解决方法)
4)内存溢出问题(较常出现)
内存溢出
:当一个线程执行完了之后,不释放所占用的内存,或者释放内存不及时的情况,称之为"内存溢出"。
可以看出,使用ThreadLocal发生了内存溢出!(具体原因看后续)
**内存溢出原因:
表层:线程池是长生命周期,而线程是执行完任务就结束了(线程相关资源会释放掉);
5.ThreadLocalMap
1)分析内存溢出(OOM)
2)ThreadLocalMap处理冲突方式
①面试题:HashMap处理冲突的方式与ThreadLocalMap处理冲突的方式有什么区别?
HashMap使用数组加链表的方式解决哈希冲突;ThreadLocalMap使用开放寻址法解决ThreadLocalMap冲突。
为什么二者解决冲突方式要这么设计?
当数据量比较少的情况下,开放寻址法的性能更好,对于ThreadLocal来说,它是存储在线程中的,一般而言线程不会很多,所以使用开放寻址法解决ThreadLocal冲突更好;而HashMap里面一般存储的数据比较多,如果采用开放寻址法效率低下,因此使用链表法是最好的选择。
强引用:最为常见,如Object() object = new Object(),这样的变量声明和定义就会产生该对象的强引用。只要对象有强引用指向,Java内存回收时,即使内存耗尽,也不会回收该对象
软引用:引用力度小于强引用,用在非必须对象的场景,在即将内存溢出之前,垃圾回收器会把软引用指向的对象加入回收范围,以获得更多的内存空间。
弱引用:引用力度比之前两个都小,也是用来描述非必需对象的,如果弱引用指向的对象只存在弱引用,则对象会在下一次GC时被回收。由于GC时间不确定,弱引用指向对象何时被回收也不不确定。
虚引用:极弱的引用变量,当定义完成后,就无法通过引用获取指定向的对象。(可以说是创建即死亡)
对于Entry数组中的对象而言,其key设置为弱引用,value设置为强引用;
①面试题:那么为什么要把Threadlocal中的key设置为弱引用?
为了最大程度避免OOM(内存溢出)
为什么会发生OOM现象呢?
ThreadLocal是长生命周期,线程池中创建了线程就不销毁,线程拥有ThreadLocal,而ThreadLocal中含有静态内部类ThreadLocalMap,ThreadLocalMap里面含有Entry类型数组,数组中的对象为key-value形式,value为强引用,不管是否发生OOM,强引用都不会被回收掉,因此会出现内存溢出问题。
解决ThreadLocal内存溢出:使用remove方法
以上是关于对ThreadLocal的深入理解的主要内容,如果未能解决你的问题,请参考以下文章