ThreadLocal详解

ThreadLocal详解

两大使用场景

典型场景1

每个线程需要一个独享的对象的时候

即每个Thread类中有自己的实例副本,而各个线程之间的实例之间是不共享的,如同教材只有一本,一起做笔记就会有线程安全问题,使并发读写带来数据的不一致,而使用ThreadLocal相当于把书本复印多份,这样每个用户使用自己的教材,就不会出问题了。而每一本书(每一个实例)都是由当前的线程访问并使用,其他线程是无法访问,不是共用的,这样就解决了线程问题

通常是工具类,典型需要使用的类有SimpleDateFormat和Random,这两个类都是线程不安全的

SimpleDateFormat的进化之路

用SimpleDateFormat的进化之路来说明ThreadLocal的好处

image-20200203115545035

2个线程分别用自己的SimpleDateFormat

image-20200203120051499
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 描述:两个线程打印日期
*/
public class ThreadLocalNormalUsage00 {

public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage00().date(10);
System.out.println(date);
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage00().date(104707);
System.out.println(date);
}
}).start();
}

public String date(int seconds) {
//参数的单位是毫秒,乘以1000变成秒,从1970.1.1 00:00:00 GMT计时
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return dateFormat.format(date);
}
}

输出结果

1
2
1970-01-01 08:00:10
1970-01-02 13:05:07

为什么是08不是00,00是标准时区,08是东八区

10个线程分别用自己的SimpleDateFormat

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 描述:10个线程打印日期
*/
public class ThreadLocalNormalUsage01 {

public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 30; i++) {
int finalI = i;
new Thread(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage01().date(finalI);
System.out.println(date);
}
}).start();
Thread.sleep(100);
}

}

public String date(int seconds) {
//参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return dateFormat.format(date);
}
}

输出结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
1970-01-01 08:00:00
1970-01-01 08:00:01
1970-01-01 08:00:02
1970-01-01 08:00:03
1970-01-01 08:00:04
1970-01-01 08:00:05
1970-01-01 08:00:06
1970-01-01 08:00:07
1970-01-01 08:00:08
1970-01-01 08:00:09
1970-01-01 08:00:10
1970-01-01 08:00:11
1970-01-01 08:00:12
1970-01-01 08:00:13
1970-01-01 08:00:14
1970-01-01 08:00:15
1970-01-01 08:00:16
1970-01-01 08:00:17
1970-01-01 08:00:18
1970-01-01 08:00:19
1970-01-01 08:00:20
1970-01-01 08:00:21
1970-01-01 08:00:22
1970-01-01 08:00:23
1970-01-01 08:00:24
1970-01-01 08:00:25
1970-01-01 08:00:26
1970-01-01 08:00:27
1970-01-01 08:00:28
1970-01-01 08:00:29

以上的结果可能会出现相同时间,与预期不符

因为所有的线程都共有一个SimpleDateFormat对象

image-20200215161137705

SimpleDateFormat的进化之路总结

  1. 2个线程分别用自己的SimpleDateFormat,此时是没有问题的
  2. 后来延伸10个,就有10个线程和10个SimpleDateFormat,这虽然写法不优雅,但可以接受
  3. 但是当需求变成1000个,那么必须要使用线程池,否则消耗内存太多
  4. 让所有的线程都共用一个SimpleDateFormat对象
  5. 更好的解决方案是用ThreadLocal

使用ThreadLocal来解决

image-20200215210538040

典型场景2

每个线程内需要保存全局变量的时候

例如在拦截器中获取用户信息,可以让不同方法直接使用,避免参数传递的麻烦

实例

当前用户信息需要被线程内所有方法共享

一个比较繁琐的解决方案是把user作为参数层层传递,从service-1()传到service-2()传到service-3(),以此类推,但是这样会导致代码冗余且不易维护

image-20200215212832502

希望每个线程内需要保存全局变量,可以让不同方法直接使用,避免参数传递的麻烦

方法

  1. 定义 一个全局变量static user保存一些业务内容(用户权限信息、从用户系统中获取到的用户名、user ID等),每次取得时候去拿,但是这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不同的

  2. 在此基础上可以演进,使用UserMap

image-20200215221341767

此时的UserMap,可能被多个线程同时调用,必须要保证线程安全

image-20200215221653468

当多线程同时工作的时候,需要保证线程安全,可以使用synchronized,也可以使用ConcurrentHashMap,把当前线程的ID作为key,把user作为value来保存,这样可以做到线程间的隔离,但无论使用什么,都会对性能产生影响

  1. 更好的方法是使用ThreadLocal,这样无需synchronized,可以在不影响性能的情况下,也无须层层传递参数,就可以达到保存当前线程对应的用户信息的目的

方法总结

用ThreadLocal保存一些业务内容(用户权限信息、从用户系统获取用户的用户名、userID等)

这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不同的

在线程生命周期内,都通过这个静态ThreadLocal实例的get()方法取得自己set过的那个对象,避免了将这个对象(例如User)作为参数传递的麻烦

ThreadLocal强调的是同一个请求内(同一个线程内)不同方法间的共享

不需要重写initialValue()方法,但是必须手动调用set()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* 描述: 演示ThreadLocal用法2:避免传递参数的麻烦
*/
public class ThreadLocalNormalUsage06 {

public static void main(String[] args) {
new Service1().process("");

}
}

class Service1 {

public void process(String name) {
User user = new User("超哥");
UserContextHolder.holder.set(user);
new Service2().process();
}
}

class Service2 {

public void process() {
User user = UserContextHolder.holder.get();
ThreadSafeFormatter.dateFormatThreadLocal.get();
System.out.println("Service2拿到用户名:" + user.name);
new Service3().process();
}
}

class Service3 {

public void process() {
User user = UserContextHolder.holder.get();
System.out.println("Service3拿到用户名:" + user.name);
UserContextHolder.holder.remove();
}
}

class UserContextHolder {

public static ThreadLocal<User> holder = new ThreadLocal<>();


}

使用ThreadLocal,这样无需synchronized,可以在不影响性能的情况下,也无须层层传递参数,就可以达到保存当前线程对应的用户信息的目的

image-20200216073424422

ThreadLocal的两个作用

  1. 让某个需要用到的独立对象在线程间隔离(每个线程都有自己的独立的对象)
  2. 在任何方法中都可以轻松获取到该对象

根据共享对象的生成时机不同,选择initialValue或set来保存对象

场景一:initialValue

在ThreadLocal第一次get的时候把对象给初始化出来,对象的初始化时机可以由我们自己控制

场景二:set

如果需要保存到ThreadLocal里的对象的生成时机不由我们随意控制,例如拦截器生成的用户信息,用ThreadLocal.set直接放到我们的ThreadLocal中去,以便后续使用

使用ThreadLocal带来的好处

达到线程安全

不需要加锁,提高执行效率

更高效地利用内存、节省开销:相比于每个任务都新建一个SimpleDateFormat,显然用ThreadLocal可以节省内存和开销

免去传参的繁琐:无论是场景一的工具类,还是场景二的用户名,都可以在任何地方直接通过ThradLocal拿到,再也不需要每次传同样的参数,ThreadLocal使得代码耦合度更低,更优雅

ThreadLocal详解

原理、源码分析

搞清楚Thread,ThreadLocal以及ThreadLocalMap三者之间的关系

每个Thread对象中都持有一个ThreadLocalMap成员变量

image-20200216083133140
1
2
3
4
public
class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
}

主要方法介绍

T initialValue()

初始化

该方法会返回当前线程对应的“初始值”,这是一个延迟加载的方法,只有在调用get的时候才会触发

当线程第一次使用get方法访问变量时,将调用此方法,除非线程先前调用了set方法,在这种情况下,不会为线程调用本initialValue方法

通常,每个线程最多调用一次此方法,但如果已经调用了remove()后,在调用get(),则可以再次调用次方法

如果不重写本方法,这个方法会返回null,一般使用匿名内部类的方法来重写initialValue()方法,以便在后续使用中可以初始化副本对象

initialValue方法是没有默认实现的,如果要使用initialValue方法,需要自己实现,通常是匿名内部类的方式

1
2
3
protected T initialValue() {
return null;
}

void set(T t)

为这个线程设置一个新值

1
2
3
4
5
6
7
8
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

T get()

得到这个线程对应的value,如果是首次调用get(),则会调用initialize来得到这个值

get方法先取出当前线程的ThreadLocalMap,然后调用map.getEntry方法,把ThreadLocal的引用作为参数传入,取出map中属于ThreadLocal的value

1
2
3
4
5
6
7
8
9
10
11
12
13
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
1
2
3
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

void remove()

删除对应这个线程的值

1
2
3
4
5
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}

ThreadLocalMap类

ThreadLocalMap类,也就是Thread.threadLocals

1
2
3
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocalMap类是每个线程Thread类里面的变量,里面最重要的是一个键值对数组Entry[] table,可以认为是一个map键值对,键是这个ThreadLocal,值是实际需要的成员变量,比如user或者SimpleDateFormat对象

在发生冲突的时候

普通的HashMap

image-20200216093707098

ThreadLocalMap

ThreadLocalMap采用的是线程探测法,就是如果发生冲突,就继续找下一个空位置,而不是用链表拉链法

通过源码分析,setInitialValue和直接set最后都是利用map.set()方法来设置值

也就是最后都会对应到ThreadLocalMap的一个Entry,只不过是起点和入口不一样

ThreadLocal注意点

内存泄露

什么是内存泄露

某个对象不再有用,但是占用的内存却不能被回收

弱引用的特点,如果这个对象只被弱引用关联(没有任何引用关联),那么这个对象就可以被回收

所以弱引用不会阻止GC,这个是弱引用的机制

ThreadLocalMap的每个Entry都是一个对key的弱引用,同时,每个Entry都包含一个对value的强引用

正常情况下,当线程终止,保存在ThreadLocal里的value会被垃圾回收,因为没有任何强引用了

但是,如果线程不终止(比如线程需要保持很久),那么key对应的value就不能被回收,因为有以下的调用链

image-20200216101531959

因为value和Thread之间还存在这个强引用链路,所以导致value无法回收,就可能会出现OOM

JDK已经考虑到了这个问题,所以set、remove、rehash方法中会扫描key为null的Entry,并发对应的value设置为null,这样对象就会可以被回收

但是如果一个ThreadLocal不被使用,那么实际上set、remove、rehash方法也不会被调用,如果同时线程又不停止,那么调用链就一直存在,就会导致value的内存泄露

如何避免内存泄漏(阿里规范)

调用remove方法,就会删除对应的Entry对象,可以避免内存泄露,所以使用完ThreadLocal之后,应该调用remove方法

空指针异常

共享对象

如果在每个线程中ThreadLocal.set()中的对象本来就是多线程共享同一个对象,比如static对象,那么多线程的ThreadLocal.get()取得的还是这个共享对象本身,还是存在并发访问问题,所以静态对象直接使用即可

如果可以不使用ThreadLocal就解决问题,那么不要强行使用

例如在任务数很少的时候,在局部变量中可以新建对象就可以解决问题,那么就不需要使用ThreadLocal

优先使用框架的支持,而不是自己创造

例如Spring中,如果可以使用RequestContextHolder,那么就不需要自己维护ThreadLocal,因为自己可能会忘记调用remove()方法等,造成内存泄露

实际应用场景

在Spring中的事例分析

DateTimeContextHolder类,看到里面用了ThreadLocal

每次Http请求都对应一个线程,线程之间相互隔离,这就是ThreadLocal的典型应用场景

文章目录
  1. 1. ThreadLocal详解
    1. 1.1. 两大使用场景
      1. 1.1.1. 典型场景1
        1. 1.1.1.1. SimpleDateFormat的进化之路
      2. 1.1.2. 典型场景2
        1. 1.1.2.1. 实例
        2. 1.1.2.2. 方法
        3. 1.1.2.3. 方法总结
      3. 1.1.3. ThreadLocal的两个作用
    2. 1.2. 使用ThreadLocal带来的好处
    3. 1.3. ThreadLocal详解
    4. 1.4. 主要方法介绍
      1. 1.4.1. T initialValue()
      2. 1.4.2. void set(T t)
      3. 1.4.3. T get()
      4. 1.4.4. void remove()
    5. 1.5. ThreadLocalMap类
      1. 1.5.1. 在发生冲突的时候
    6. 1.6. ThreadLocal注意点
      1. 1.6.1. 内存泄露
        1. 1.6.1.1. 什么是内存泄露
        2. 1.6.1.2. 如何避免内存泄漏(阿里规范)
      2. 1.6.2. 空指针异常
      3. 1.6.3. 共享对象
      4. 1.6.4. 实际应用场景
|