1.ThreadLocal是个啥?
ThreadLocal–顾名思义,翻译成中文可以理解为:线程局部/本地变量。
这个玩意有什么用处,或者说为什么要有这么一个东西?在并发编程的时候,共享的成员变量如果不做任何处理其实是线程不安全的,各个线程都在操作同一个变量,显然是不行的。为了保证线程安全,一般使用者在访问共享变量时需要进行适当的同步,示意如下:
同步的措施一般是加锁,这就需要使用者对锁有一定的了解, 这显然加重了使用者的负担,那么有没有一种方式可以做到,当创建一个变量后,每个线程对其进行访问的时候访问的是自己线程的变量呢?示意如下:
ThreadLocal就可以做到这件事情(虽然ThreadLocal并不是为了解决这个问题而出现的)。ThreadLocal是JDK包提供的,它提供了线程本地变量,也就是如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量时,实际操作的是自己本地内存里面变量,从而避免了线程安全问题。
2.使用演示
下面将用一段代码,展示下ThreadLocal的简单使用
public class ThreadLocalSimpleDemo {
// 创建ThreadLocal变量-全局单例
static ThreadLocal<String> localVariable = new ThreadLocal<>();
public static void main(String[] args) {
// 创建线程
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
//设置线程中本地变量localVariable的值
localVariable.set("thread local variable");
//打印当前线程在本地内存中localVariable变量的值
System.out.println("thread local variable is" + " : " + localVariable.get());
//清除当前线程本地内存中的localVariable变量
localVariable.remove();
//清除后 打印当前线程在本地内存中localVariable变量的值
System.out.println("after removing, thread local variable is" + " : " + localVariable.get());
}
});
//启动线程
thread.start();
}
}
输出如下:
3.源码解读
可以先看下ThreadLocal的类图:
方法简介:
1.set():将变量设置到当前线程中
2.get(): 获取当前线程中的变量
3.remove():从当前线程中移除变量
3.setInitialValue(): 创建ThreadLocal时设置默认值
属性简介:
1.threadLocals:保存当前线程的变量数据,是一个map(其Entry是一个弱引用类型,后续说明)
2.inheritableThreadLocals: 保存线程的变量数据,当使用InheritableThreadLocal时,主线程的该字段不为空时,子线程的该字段也会被赋值(后续说明)
3.1 set()方法解读
3.2 get()方法解读
3.3 remove()方法解读
3.4 ThreadLocal的数据独立性
前文中有说到,ThreadLocal会为每个线程创建一份变量副本,如何体现?下面将用一个demo来演示下:
/**
* 线程One的代码3.1通过set方法设置了localVariable的值,这其实设置的是线程
* One本地内存中的个副本,这个副本线程Two是访问不了的 然后代码3.2调用了print
* 函数,代码2.1通过get函数获取了当前线程(线程One本地内存中localVariable的值
* 线程Two的执行类似于线程One。
**/
public class ThreadLocalTest {
//(1) 创建ThreadLocal变量-全局
static ThreadLocal<String> localVariable = new ThreadLocal<>();
//(2)print函数
static void print(String str) {
//2.1 打印当前线程在本地内存中localVariable变量的值
System.out.println(str + ":" + localVariable.get());
//2.2 清除当前线程本地内存中的localVariable变量
// localVariable.remove();
}
public static void main(String[] args) {
//(3) 创建线程One
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
//3.1 设置线程One中本地变量localVariable的值
localVariable.set("threadOne local variable");
//3.2 调用打印函数
print("threadOne");
//3.3 打印本地变量值
System.out.println("threadOne remove after" + " : " + localVariable.get());
}
});
//(4) 创建线程Two
Thread threadTwo = new Thread(new Runnable() {
@Override
public void run() {
//4.1 设置线程One中本地变量localVariable的值
localVariable.set("threadTwo local variable");
//4.2 调用打印函数
print("threadTwo");
//4.3 打印本地变量值
System.out.println("threadTwo remove after" + " : " + localVariable.get());
}
});
//(5) 启动线程
//5.1启动主线程
localVariable.set("mainThread local variable");
print("mainThread");
System.out.println("mainThread remove after" + " : " + localVariable.get());
//5.2启动子线程
threadOne.start();
threadTwo.start();
}
}
输出如下:
解除2.2的注释,输出如下:
3.5 拓展:如何在子线程中访问主线程的变量(InheritableThreadLocal)
由上面的内容可以知道:各个线程在访问ThreadLocal中变量时是互不干扰的。但如果子线程想访问主线程的变量时,应该怎么做呢?
答:可以使用ThreadLocal的子类–InheritableThreadLocal,具体使用如下:
/**
* 如何实现子线程访问主线程中的本地变量呢?如下
**/
public class InheritableThreadLocalTest {
//(1) 创建ThreadLocal变量-全局
//1.1 使用原始的ThreadLocal--子线程不能访问主线程变量
// static ThreadLocal<String> localVariable = new ThreadLocal<>();
//1.2 使用子类InheritableThreadLocal--子线程可以访问主线程变量
static InheritableThreadLocal<String> localVariable = new InheritableThreadLocal<>();
//(2)print函数
static void print(String str) {
//2.1 打印当前线程在本地内存中localVariable变量的值
System.out.println(str + ":" + localVariable.get());
}
public static void main(String[] args) {
//(3) 主线程设置本地变量localVariable的值
localVariable.set("mainThread local variable");
//3.1 打印本地变量值
print("mainThread");
//(4) 创建线程One
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
//4.1 打印本地变量值
print("threadOne");
}
});
//(5) 创建线程Two
Thread threadTwo = new Thread(new Runnable() {
@Override
public void run() {
//5.1 打印本地变量值
print("threadTwo");
}
});
//(6) 启动线程
threadOne.start();
threadTwo.start();
}
}
输出如下:
3.6 拓展:弱引用(WeakReference)的在GC中的逻辑
Java中的引用类型分为:强引用、软引用、弱引用和虚引用;GC的发生的可能性:虚引用>弱引用>软引用>强引用,具体如下:
- 强引用:无论内存是否足够,不会回收。
- 软引用:内存不足时,回收该引用关联的对象。
- 弱引用:垃圾回收时,无论内存是否足够,都会回收。
- 虚引用:任何时候都可能被垃圾回收器回收。
ThreadLocal.ThreadLocalMap的Entry是一个弱引用类型,弱引用的使用是为了避免内存泄漏
Java的GC分为Minor GC和Full GC:
- 发生Minor GC时,如果是标记为WeakReference的对象,位置在新生代,而且只有弱引用,没有其他引用,则会被回收。
- 发生Full GC时,如果是标记为WeakReference的对象,只有弱引用,无论在新生代还是老年代,都会被回收。
这说明,只要堆上的对象仅仅只被弱引用所指向,不管当前内存空间是否足够,下次GC都会回收对象的内存空间。
3.7 拓展:Synchronized和ThreadLocal的对比
Synchronized | ThreadLocal | |
---|---|---|
原理 | 同步机制采用了时间换空间的方式,只提供一份变量,让不同线程排队访问(临界区排队) | 采用空间换时间的方式,为每一个线程都提供一份变量的副本,从而实现同时访问而互不相干扰 |
侧重点 | 多个线程之间访问资源的同步 | 多线程中让每个线程之间的数据相互隔离 |
4.总结
- 为了避免并发问题,相对于加锁,threadLocal是较为轻量的解决方案
- ThreadLocal会为每个线程创建一个变量副本,访问是互不影响的
- 子线程访问主线程的变量,可以使用InheritableThreadLocal
- ThreadLocal的常见使用场景:全局存储用户信息
- 弱引用的对象在GC时,无论是Minor GC还是Full GC都会被回收
- ThreadLocal放在线程池中的坑:多个任务复用同一个线程时会有问题
5.参考资料
《Java并发编程之美》
ThreadLocal详解
WeakReference在GC中的逻辑
Java的四种引用方式