ThreadLocal分析
形而上 Lv4

ThreadLocal介绍

ThreadLocal是什么

ThreadLocal本地线程变量,线程自带的变量副本(实现了每一个线程副本都有一个专属的本地变量,主要解决的就是让每一个线程绑定自己的值,自己用自己的,不跟别人争抢。通过使用get()和set()方法,获取默认值或将其值更改为当前线程所存的副本的值从而避免了线程安全的问题)

synchronized或者lock,有个管理员,好比,现在大家签到,多个同学(线程),但是只有一只笔,只能同一个时间,只有一个线程(同学)签到,加锁(同步机制是以时间换空间,执行时间不一样,类似于排队)

ThreadLocal,人人有份,每个同学手上都有一支笔,自己用自己的,不用再加锁来维持秩序(同步机制是以空间换时间,为每一个线程都提供了一份变量的副本,从而实现同时访问,互不干扰同时访问,肯定效率高啊)

api介绍

1

  • protected T initialValue?():initialValue():返回此线程局部变量的当前线程的”初始值”
    (对于initialValue()较为老旧,jdk1.8又加入了withInitial()方法)

  • static ThreadLocal withInitial?(Supplier supplier):创建线程局部变量

  • T get?():返回当前线程的此线程局部变量的副本中的值

  • void set?(T value):将当前线程的此线程局部变量的副本设置为指定的值

  • void remove?():删除此线程局部变量的当前线程的值

ThreadLocal源码分析

Thread|ThreadLocal|ThreadLocalMap关系

  • Thread和ThreadLocal
    1
  • ThreadLocal和ThreadLocalMap
    1
  • 三者总概括
  1. Thread类中有一个ThreadLocal.ThreadLocalMap threadLocals = null的变量,这个ThreadLocal相当于是Thread类和ThreadLocalMap的桥梁,在ThreadLocal中有静态内部类ThreadLocalMap,ThreadLocalMap中有Entry数组
  2. 当我们为threadLocal变量赋值,实际上就是以当前threadLocal实例为key,值为value的Entry往这个threadLocalMap中存放
  3. t.threadLocals = new ThreadLocalMap(this, firstValue) 如下这行代码,可以知道每个线程都会创建一个ThreadLocalMap对象,每个线程都有自己的变量副本
    1

核心代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//核心代码说明
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}

set方法详解

  1. 首先获取当前线程,并根据当前线程获取一个Map
  2. 如果获取的Map不为空,则将参数设置到Map中(当前ThreadLocal的引用作为key)
  3. 如果Map为空,则给该线程创建 Map,并设置初始值
    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
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    /**
    * 设置当前线程对应的ThreadLocal的值
    *
    * @param value 将要保存在当前线程对应的ThreadLocal的值
    */
    public void set(T value) {
    // 获取当前线程对象
    Thread t = Thread.currentThread();
    // 获取此线程对象中维护的ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
    // 判断map是否存在
    if (map != null)
    // 存在则调用map.set设置此实体entry
    map.set(this, value);
    else
    // 1)当前线程Thread 不存在ThreadLocalMap对象
    // 2)则调用createMap进行ThreadLocalMap对象的初始化
    // 3)并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
    createMap(t, value);
    }

    /**
    * 获取当前线程Thread对应维护的ThreadLocalMap
    *
    * @param t the current thread 当前线程
    * @return the map 对应维护的ThreadLocalMap
    */
    ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
    }
    /**
    *创建当前线程Thread对应维护的ThreadLocalMap
    *
    * @param t 当前线程
    * @param firstValue 存放到map中第一个entry的值
    */
    void createMap(Thread t, T firstValue) {
    //这里的this是调用此方法的threadLocal
    t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

    /*
    * firstKey : 本ThreadLocal实例(this)
    * firstValue : 要保存的线程本地变量
    */
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    //初始化table
    table = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY];
    //计算索引(重点代码)
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    //设置值
    table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue);
    size = 1;
    //设置阈值
    setThreshold(INITIAL_CAPACITY);
    }

get方法详解

先获取当前线程的ThreadLocalMap变量,如果存在则返回值,不存在则创建并返回初始值

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
46
47
48
49
50
51
52
53
54
55
56
57
/**
* 返回当前线程中保存ThreadLocal的值
* 如果当前线程没有此ThreadLocal变量,
* 则它会通过调用{@link #initialValue} 方法进行初始化值
*
* @return 返回当前线程对应此ThreadLocal的值
*/
public T get() {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 如果此map存在
if (map != null) {
// 以当前的ThreadLocal 为 key,调用getEntry获取对应的存储实体e
ThreadLocalMap.Entry e = map.getEntry(this);
// 对e进行判空
if (e != null) {
@SuppressWarnings("unchecked")
// 获取存储实体 e 对应的 value值
// 即为我们想要的当前线程对应此ThreadLocal的值
T result = (T)e.value;
return result;
}
}
/*
初始化 : 有两种情况有执行当前代码
第一种情况: map不存在,表示此线程没有维护的ThreadLocalMap对象
第二种情况: map存在, 但是没有与当前ThreadLocal关联的entry
*/
return setInitialValue();
}

/**
* 初始化
*
* @return the initial value 初始化后的值
*/
private T setInitialValue() {
// 调用initialValue获取初始化的值
// 此方法可以被子类重写, 如果不重写默认返回null
T value = initialValue();
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 判断map是否存在
if (map != null)
// 存在则调用map.set设置此实体entry
map.set(this, value);
else
// 1)当前线程Thread 不存在ThreadLocalMap对象
// 2)则调用createMap进行ThreadLocalMap对象的初始化
// 3)并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
createMap(t, value);
// 返回设置的值value
return value;

remove方法详解

  1. 首先获取当前线程,并根据当前线程获取一个Map
  2. 如果获取的Map不为空,则移除当前ThreadLocal对象对应的entry
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    /**
    * 删除当前线程中保存的ThreadLocal对应的实体entry
    */
    public void remove() {
    // 获取当前线程对象中维护的ThreadLocalMap对象
    ThreadLocalMap m = getMap(Thread.currentThread());
    // 如果此map存在
    if (m != null)
    // 存在则调用map.remove
    // 以当前ThreadLocal为key删除对应的实体entry
    m.remove(this);
    }

ThreadLocal内存泄漏问题

为什么源代码用弱引用?

  1. 当function01方法执行完毕后,栈帧销毁强引用 tl 也就没有了。但此时线程的ThreadLocalMap里某个entry的key引用还指向这个对象
  2. 若这个key引用是强引用,就会导致key指向的ThreadLocal对象及v指向的对象不能被gc回收,造成内存泄漏
  3. 若这个key引用是弱引用就大概率会减少内存泄漏的问题(还有一个key为null的雷)。使用弱引用,就可以使ThreadLocal对象在方法执行完毕后顺利被回收且Entry的key引用指向为null
    1

key为null的entry,原理解析

  1. ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话(比如正好用在线程池),这些key为null的Entry的value就会一直存在一条强引用链
  2. 虽然弱引用,保证了key指向的ThreadLocal对象能被及时回收,但是v指向的value对象是需要ThreadLocalMap调用get、set时发现key为null时才会去回收整个entry、value
  3. 因此弱引用不能100%保证内存不泄露。我们要在不使用某个ThreadLocal对象后,手动调用remoev方法来删除它,尤其是在线程池中,不仅仅是内存泄露的问题,因为线程池中的线程是重复使用的,意味着这个线程的ThreadLocalMap对象也是重复使用的,如果我们不手动调用remove方法,那么后面的线程就有可能获取到上个线程遗留下来的value值,造成bug
  4. 如果当前thread运行结束,threadLocal,threadLocalMap, Entry没有引用链可达,在垃圾回收的时候都会被系统进行回收
  5. 但在实际使用中我们有时候会用线程池去维护我们的线程,比如在Executors.newFixedThreadPool()时创建线程的时候,为了复用线程是不会结束的,所以threadLocal内存泄漏就值得我们小心
  6. 出现内存泄漏的真实原因 (1). 没有手动删除这个Entry (2). CurrentThread依然运行
    1

set、get方法会去检查所有键为null的Entry对象

1
1
1
1
1
1
1

结论(在finally后面调用remove方法)

1

ThreadLocal小总结

  1. ThreadLocal本地线程变量,以空间换时间,线程自带的变量副本,人手一份,避免了线程安全问题
  2. 每个线程持有一个只属于自己的专属Map并维护了Thread Local对象与具体实例的映射,该Map由于只被持有它的线程访问,故不存在线程安全以及锁的问题
    3.ThreadLocalMap的Entry对ThreadLocal的引用为弱引用,避免了ThreadLocal对象无法被回收的问题
  3. 都会通过expungeStaleEntry,cleanSomeSlots, replace StaleEntry这三个方法回收键为 null 的 Entry 对象的值(即为具体实例)以及 Entry 对象本身从而防止内存泄漏,属于安全加固的方法
  4. 用完之后一定要remove操作

使用案例

解决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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//线程安全做法
package h.xd.util;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;

public class ThreadLocalDateUtils {

public static final ThreadLocal<SimpleDateFormat>sdfThreadLocal=
ThreadLocal.withInitial(()->new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

public static Date parseByThreadLocal(String stringDate) throws ParseException {
return sdfThreadLocal.get().parse(stringDate);
}

//DateTimeFormatter 代替 SimpleDateFormat
public static final DateTimeFormatter DATE_TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

public static String formatForDateTime(LocalDateTime localDateTime) {
return DATE_TIME_FORMAT.format(localDateTime);
}

public static LocalDateTime parseForDateTime(String dateString) {
return LocalDateTime.parse(dateString,DATE_TIME_FORMAT);
}

public static void main(String[] args) throws Exception{
for (int i = 1; i <=3; i++) {
new Thread(()->{
try {
System.out.println(ThreadLocalDateUtils.parseByThreadLocal("2021-03-30 11:20:30"));
System.out.println(ThreadLocalDateUtils.parseForDateTime("2021-03-30 11:20:30"));
System.out.println(ThreadLocalDateUtils.formatForDateTime(LocalDateTime.now()));
} catch (Exception e) {
e.printStackTrace();
}finally {
ThreadLocalDateUtils.sdfThreadLocal.remove();
}
},String.valueOf(i)).start();
}
}
}

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
//不安全做法
package h.xd.util;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class DateUtils {

public static SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

/**
解决方案一:加入synchronized,用时间换空间,效率低
*/
/**
如果不加会导致线程安全问题,SimpleDateFormat类内部有一个Calendar对象引用,
SimpleDateFormat相关的日期信息,例如sdf.parse(dateStr),sdf.format(date)
诸如此类的方法参数传入的日期相关String,Date等等, 都是交由Calendar引用来储存的.
这样就会导致一个问题如果你的SimpleDateFormat是个static的,那么多个thread之间
就会共享这个SimpleDateFormat,同时也是共享这个Calendar引用(相当于买票案列)
*/
//public static synchronized Date parse(String stringDate) throws ParseException {
public static Date parse(String stringDate) throws ParseException {
System.out.println(sdf.parse(stringDate));
return sdf.parse(stringDate);
}


public static void main(String[] args) throws Exception{
for (int i = 1; i <=3; i++) {
new Thread(()->{
try {
DateUtils.parse("2021-03-30 11:20:30");
} catch (Exception e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}

解决每个请求一个线程安全的连接问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// TestDao.java
package com.h.xd.threadlocal;

public class TestDao {
private static ThreadLocal<MConnection> connectionThreadLocal = ThreadLocal.withInitial(TestDao::createConnection);

private static MConnection createConnection(){
MConnection mConnection = new MConnection();
System.out.println("实例化了一个对象: " + mConnection);
return mConnection;
}

public static MConnection getConnection(){
return connectionThreadLocal.get();
}

}
1
2
3
4
5
//MConnection.java
package com.h.xd.threadlocal;

public class MConnection {
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//客户端调用,这里用springmvc的一个请求
package com.h.xd.controller;

import com.h.xd.threadlocal.MConnection;
import com.h.xd.threadlocal.TestDao;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(value = "/thread")
@Slf4j
public class ThreadController {


@RequestMapping(value = "")
public void home(){
MConnection connection = TestDao.getConnection();
log.info("{}",connection);
}


}

  • 本文标题:ThreadLocal分析
  • 本文作者:形而上
  • 创建时间:2022-02-10 13:13:00
  • 本文链接:https://deepter.gitee.io/2022_02_10_threadlocal/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!