ThreadLocal详解-笔记
什么是ThreadLocal
ThreadLocal在《Java核心技术 卷一》中被称作线程局部变量,我们可以利用ThreadLocal创建只能由同一线程读和写的变量。因此就算两个线程正在执行同一段代码,并且这段代码具有对ThreadLocal变量的引用,这两个线程也无法看到彼此的ThreadLocal变量。
ThreadLocal两大使用场景
1、每个线程需要一个独享的线程变量(通常是在工具类中使用,典型的比如线程非安全的SimpleDateFormat和Random类)
2、 每个线程内需要保存全局变量,可以让不同方法使用,避免参数不断传递的麻烦(列如在拦截器中获取用户信息,用于当前线程中使用)。
ThreadLocal在场景一中的使用
/**
* @author: Durian
* @date: 2020/9/3 16:59
* @description: 场景一 SimpleDateFormat
*/
public class ThreadLocalDemo1
{
public static void main(String[] args)
{
new Thread(() -> new ThreadLocalDemo1().date(10)).start();
new Thread(() -> new ThreadLocalDemo1().date(120)).start();
}
private void date(int seconds)
{
Date date = new Date(seconds * 1000); // 从1970-1-1开始
SimpleDateFormat sdf = new SimpleDateFormat("YYYY-MM-dd hh:mm:ss");
String s = sdf.format(date);
System.out.println(Thread.currentThread().getName() + " = " + s);
}
}
运行结果:
Thread-0 = 1970-01-01 08:00:10
Thread-1 = 1970-01-01 08:02:00
上面的代码展示了当两个线程同时使用SimpleDateFormat去格式化时间,可以看出并没有任何问题,因为在该代码中,SimpleDateFormat在每个线程中都会被实例化,每个线程都拥有自己的SimpleDateFormat类,所以并不会有线程安全问题。但是这里仅仅是两个线程使用,如果是很多线程使用呢,那么就会创建许多SimpleDateFormat对象,但是使用完马上又被销毁,这是很消耗资源的操作,于是将SimpleDateFormat提取出来作为一个static全局变量,降低资源 消耗。
看下面的代码:
/**
* @author: Durian
* @date: 2020/9/3 17:09
* @description: 许多线程使用SimpleDateFormat
*/
public class ThreadLocalDemo2
{
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
private static Set<String> dates = Collections.synchronizedSet(new HashSet<>());
public static void main(String[] args) throws InterruptedException
{
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++)
{
int finalI = i;
executorService.execute(() -> new ThreadLocalDemo2().date(finalI * 10));
}
executorService.shutdown();
Thread.sleep(1000);
System.out.println(dates.size());
}
private void date(int seconds)
{
Date date = new Date(seconds * 1000); // 从1970-1-1开始
String s = sdf.format(date);
System.out.println(Thread.currentThread().getName() + " = " + s);
dates.add(s);
}
}
部分运行结果:
pool-1-thread-9 = 1970-01-01 08:01:20 // 1970-01-01 08:01:20
pool-1-thread-3 = 1970-01-01 08:00:00
pool-1-thread-8 = 1970-01-01 08:01:20 // 1970-01-01 08:01:20
pool-1-thread-4 = 1970-01-01 08:00:40
pool-1-thread-10 = 1970-01-01 08:02:00
pool-1-thread-10 = 1970-01-01 08:02:10
pool-1-thread-3 = 1970-01-01 08:02:30
set总数:81
运行结果出现了重复的结果,并且set集合大小不等于100,可以表明在多个线程同时使用一个SimpleDateFormat变量时,将出现线程安全问题。
在SimpleDateFormat的源码中,可以看到format的方法中直接对calendar进行了直接的值操作,在多线程环境中将出现安全问题。
从上面代码可以看出,多线程中使用SimpleDateFormat出现线程安全问题的源头就是,SimpleDateFormat对象被线程共享了,而SimpleDateFormat类的方法并不是线程安全的,所以解决的办法就是使得SimpleDateFormat不被线程所共享。而ThreadLocal的本意就是“线程本地”,意思就是将一个全局变量为每一个线程做一份拷贝,每个线程的变量是隔离的,互不影响。
通过代码演示:
/**
* @author: Durian
* @date: 2020/9/3 17:23
* @description:
*/
public class ThreadLocalDemo3
{
private static ThreadLocal<SimpleDateFormat> sdfThreadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss")); // 使用lambda表达式初始化ThreadLocal
// 用来判断结果是否都是唯一
private static Set<String> dates = Collections.synchronizedSet(new HashSet<>());
public static void main(String[] args) throws InterruptedException
{
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++)
{
int finalI = i;
executorService.execute(() -> new ThreadLocalDemo3().date(finalI * 10));
}
executorService.shutdown();
Thread.sleep(3000); // 确保线程全部执行完
System.out.println("set总数: " + dates.size());
}
private void date(int seconds)
{
Date date = new Date(seconds * 1000); // 从1970-1-1开始
SimpleDateFormat sdf = sdfThreadLocal.get();
String s = sdf.format(date);
System.out.println(Thread.currentThread().getName() + " = " + s);
dates.add(s);
sdfThreadLocal.remove();
}
}
运行结果
pool-1-thread-10 = 1970-01-01 08:14:20
pool-1-thread-6 = 1970-01-01 08:16:10
pool-1-thread-3 = 1970-01-01 08:14:30
pool-1-thread-4 = 1970-01-01 08:14:10
pool-1-thread-7 = 1970-01-01 08:14:00
pool-1-thread-2 = 1970-01-01 08:13:50
pool-1-thread-6 = 1970-01-01 08:16:30
pool-1-thread-10 = 1970-01-01 08:16:20
pool-1-thread-9 = 1970-01-01 08:16:00
pool-1-thread-1 = 1970-01-01 08:15:50
pool-1-thread-8 = 1970-01-01 08:15:30
pool-1-thread-5 = 1970-01-01 08:15:20
set总数: 100
从运行结果可以发现,使用了ThreadLocal后,SimpleDateFormat线程安全的问题被解决了。
建议使用Java8的日期时间类和对应的线程安全的DateTimeFormat类
ThreadLocal在场景二中的使用
比如我们有这么一个类,它需要获取一个用户信息,然后执行一系列的业务逻辑操作,这些逻辑操作分散在各个方法中。
如果不使用一个全局变量,就得对每一个方法进行参数传递,而使用全局变量后的代码如下:
/**
* @author: Durian
* @date: 2020/9/3 17:41
* @description: 场景二的使用,每个线程内需要保存全局变量
*/
public class ThreadLocalDemo4
{
private static User user;
public static void main(String[] args)
{
for (int i = 0; i < 10; i++)
{
new Thread(() ->
{
new Intercept().intercept();
new Service1().intercept();
}).start();
}
}
// 模拟拦截器
static class Intercept
{
public void intercept()
{
// 拦截器获取用户信息放入全局变量user中,方便操作
user = new User(Math.random() * 10);
}
}
// 业务类1
static class Service1
{
public void intercept()
{
new Service2().process();
new Service3().process();
}
}
// 业务类2
static class Service2
{
public void process()
{
System.out.println(user.getMoney());
user.setMoney(Math.random() * 10);
}
}
// 业务类3
static class Service3
{
public void process()
{
System.out.println(user.getMoney());
}
}
}
@Data
@AllArgsConstructor
class User
{
private Double money;
}
部分运行结果:
2.295436641359472
2.295436641359472
2.295436641359472
2.295436641359472
2.295436641359472
2.295436641359472
0.1597985241974753
可以看到运行结果有很多money是重复的,这是不符合预期的,因为user是全局变量,在多线程环境中对user的值进行操作会出现线程安全问题。这也就是ThreadLocal使用的第二种场景,每个线程内需要保存全局变量,可以让不同方法使用,避免参数不断传递的麻烦。
使用ThreadLocal后:
/**
* @author: Durian
* @date: 2020/9/3 17:41
* @description: 场景二的使用,每个线程内需要保存全局变量
*/
public class ThreadLocalDemo5
{
public static void main(String[] args)
{
for (int i = 0; i < 10; i++)
{
new Thread(() ->
{
new Intercept().intercept();
new Service1().intercept();
}).start();
}
}
// 模拟拦截器
static class Intercept
{
public void intercept()
{
User user = new User(Math.random() * 10);
// 将拦截器获取到的用户信息放入ThreadLocal中
UserContextHolder.holder.set(user);
}
}
// 业务类1
static class Service1
{
public void intercept()
{
new Service2().process();
new Service3().process();
}
}
// 业务类2
static class Service2
{
public void process()
{
System.out.println(UserContextHolder.holder.get().getMoney());
// 使用ThreadLocal的get方法拿到User
UserContextHolder.holder.get().setMoney(Math.random() * 10);
}
}
// 业务类3
static class Service3
{
public void process()
{
System.out.println(UserContextHolder.holder.get().getMoney());
// 使用完成后remove掉user
UserContextHolder.holder.remove();
}
}
}
// 表示持有User对象
class UserContextHolder
{
// 在第二种场景下使用ThreadLocal是不需要先初始化的
public static ThreadLocal<User> holder = new ThreadLocal<>();
}
在引入ThreadLocal后,每个业务类的方法可以方便的通过UserContextHolder的静态属性holder拿到拦截器放入的User信息,不用再进行参数传递,也避免了使用static变量造成的线程安全问题。
ThreadLocal的原理
在ThreadLocal中有一个重要内部类ThreadLocalMap;而在每个Thread类中,拥有一个ThreadLocal.ThreadLocalMap类型的属性threadLocals,也就是说,每个线程都有属于自己的threadLocals,而ThreadLocalMap中有一个核心类Entry,Entry为key-value对,key中存储的是当前线程的ThreadLocal,value为要保持的线程的私有变量,也就是想要放入ThreadLocal的变量。它们的关系如下图所示:
每个线程的ThreadLocalMap拥有多个Entry,Entry的key是本线程的ThreadLocal,而Value可以有多个不同的值,这是为了保证在一个线程中可能会使用多个ThreadLocal来保持变量。
上面的分析可知,ThreadLocal的核心就时ThreadLocalMap,接下来看一看ThreadLocalMap的分析。
ThreadLocalMap解析
static class ThreadLocalMap {
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
/**
* The number of entries in the table.
*/
private int size = 0;
/**
* The next size value at which to resize.
*/
private int threshold; // Default to 0
}
由上面可见在ThreadLocalMap中维护着table
,size
以及threshold
三个属性。table
是一个Entry数组主要用来保存具体的数据,size
是table
的大小,而threshold
这表示当table
中元素数量超过该值时,table
就会扩容。了解了ThreadLocalMap的结构之后,我们就来看下其set
方法吧。
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
通过上面的代码分析得出,整个的设值过程如下:
- 通过ThreadLocal的threadLocalHashCode值定位到table中的位置i。
- 如果table中i这个位置是空的,那么就新创建一个Entry对象放置在i这个位置。
- 如果table中i这个位置不为空,则取出来i这个位置的key。
- 如果这个key刚好就是当前ThreadLocal对象,则直接修改该位置上Entry对象的value。
- 如果这个key不是当前TreadLocal对象,则寻找下一个位置的Entry对象,然后重复上述步骤进行判断(ThreadLocalMap处理hash冲突的策略时线性探测)。
对于get方法也是同样的原理从ThreadLocalMap中获取值。那么ThreadLocal是如何生成threadLocalHashCode值的呢?
public class ThreadLocal<T> {
private final int threadLocalHashCode = nextHashCode();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
}
复制代码
可见我们在初始化一个ThreadLocal对象的时候都为其会生成一个threadLocalHashCode值,每初始化一个ThreadLocal该值就增加0x61c88647。这样就可以做到每个ThreadLocal在ThreadLocalMap中找到一个存储值的位置了。
ThreadLocal重要方法介绍
protected T initialValue()
ThreadLocal的初始化方法
protected T initialValue() {
return null;
}
该方法会放回当前线程对应的初始值,这是一个延迟加载的方法,只有在调用get()方法时,才会触发。
当线程第一次使用get方法访问变量时,将触发此方法,如果线程之前调用了set方法,则不会为再为该线程调用initiaValue方法。
通常,每个线程只会调用一次initiaValue方法,除非再调用remove()后再次调用get方法,则会再次调用该方法。
如果在创建ThreadLocal时就想要初始化,需要使用内部类的方式实现initiaValue方法。在上文的第一种使用场景中就是这样做的,但是使用的是withInitial方法,该方法作用与initiaValue一致,但可用lambda简化初始化代码。
void set(T value)
为这个线程设置一个值,会覆盖已有的值
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); // 获取该线程的ThreadLocalMap
if (map != null)
map.set(this, value); // 如果map != null 将值设置到对应ThreadLocal的value
else
createMap(t, value); // 如果map == null,创建一个ThreadLocalMap再将值设置进去
}
T get()
获取该线程中的值
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this); // 获取当前ThreadLocal对应的Entry
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result; // 返回获得的对应ThreadLocal的value
}
}
return setInitialValue(); // 如果map == null 调用初始化方法,返回重写方法的返回值
}
void remve()
删除对应线程中的值
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this); // 移除对应ThreadLocal的值,不会删除线程中其他ThreadLocal的
}
可以看到,不管是get、set和remove最终调用的方法都是map的get、set和remove方法。
ThreadLocal的使用注意点
内存泄露问题
每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个threadlocal实例. 这个Map的确使用了弱引用,不过弱引用只是针对key. 每个key都弱引用指向threadlocal. 当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收. 但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用. 只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收。所以得出一个结论就是只要这个线程对象被gc回收,就不会出现内存泄露,但在threadLocal设为null和线程结束这段时间不会被回收的,就发生了我们认为的内存泄露。其实这是一个对概念理解的不一致,也没什么好争论的。最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的就可能出现内存泄露 。(在web应用中,每次http请求都是一个线程,tomcat容器配置使用线程池时会出现内存泄漏问题)
ThreadLocalMap的每个Entry都是一个对key的弱引用(弱引用将会在发生GC的时候被回收),同时,每个Entry都包含了一个强引用的value,。
正常情况下,当线程终止,保存在ThreadLocal中的value就是会垃圾回收,因为没有任何强引用了
但是如果线程不被终止(线程持续很久)那么key对应的value就不会被回收,因为调用链还存在,如下图
因为Thread和value之间的强引用还存在,导致value无法被回收,可能出现OOM。
JDK中的处理是,在set、remove、rehash方法中会扫描key为null的Entry,并把对应的value设置为nul,这样value的对象就可以被回收。
但是如果一个ThreadLocal不被使用,那么实际上set、remove、rehash方法也不会被调用,同时线程又不停止,那么调用链会一直存在,将导致value的内存泄露。
ThreadLocal只是操作Thread中的ThreadLocalMap,每个Thread都有一个map,ThreadLocalMap是线程内部属性,ThreadLocalMap生命周期是和Thread一样的,不依赖于ThreadLocal。
ThreadLocal通过Entry保存在map中,key为Thread的弱引用(GC时会自动回收),value为存入的变量副本,一个线程不管有多少个ThreadLocal,都是通过一个ThreadLocalMap来存放局部变量的,可以再源码中看到,set值时先获取map对象,如果不存在则创建,threadLocalMap初始大小为16,当容量超过2/3时会自动扩容。
避免线value内存露方法
- 调用remove方法,就会删除对应的Entry对象,可以避免内存泄露,所以应该在每次使用完ThreadLocal后,调用remove方法。