言成言成啊 | Kit Chen's Blog

线程不安全案例以及ThreadLocal

发布于2021-08-31 23:23:06,更新于2021-12-20 22:08:27,标签:java open  文章会持续修订,转载请注明来源地址:https://meethigher.top/blog

一、SimpleDateFormat线程安全问题

参考文章

我的写法就类似于下面这种,将SimpleDateFormat作为了静态成员变量,大佬的建议就是提取一个方法,然后通过该方法获取SimpleDateFormat对象。

1.1 parse

先看个parse案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Test {
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
private static final String str = "2021-08-30 12:23:11";

public static void main(String[] args) {
Runnable runnable = new Runnable() {

@Override
public void run() {
try {
System.out.println(sdf.parse(str));
} catch (ParseException e) {
e.printStackTrace();
}

}
};
for (int i = 0; i < 10; i++) {
new Thread(runnable).start();
}
}
}

运行结果如图

通过查看源码来找出这个问题的原因。

左键点进去,发现是调用了parse(String source)方法

再次左键点进去,发现是调用了自身的抽象方法,那么就找这个抽象方法的实现类,也就是SimpleDateFormat。

通过阅读文档,大概能了解到,如果将SimpleDateFormat作为静态变量,那么就相当于是一个共享变量,而其父类DateFormat中有一个成员变量Calendar。此时,多个线程进行操作时,因为SimpleDateFormat已经作为共享变量了,所以多线程中使用的Calendar其实是同一个,此时进行clear方法,之后再进行set时,会出现各种问题。比如:

  • a线程赋值之后,b线程给clear掉,此时a线程结果不对
  • a线程赋值之后,b线程也赋值,a线程结果不对
  • a线程执行到一半的时候,b线程也执行,a线程部分数值不对

看网上说,ParsePositioin也会存在线程安全问题,但是我发现源码中,每次执行时,ParsePosition都是重新new的,所以应该不存在该问题。

1.2 format

format就很好理解了

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
public class Test {
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

public static void main(String[] args) {
Runnable runnable = new Runnable() {

@Override
public void run() {
Date date = randomDate("2010-09-20", "2021-09-22");
System.out.println(Thread.currentThread()+":"+date+"->"+sdf.format(date));

}
};
for (int i = 0; i < 10; i++) {
new Thread(runnable).start();
}
}

public static Date randomDate(String beginDate, String endDate) {
try {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
Date start = format.parse(beginDate);// 构造开始日期
Date end = format.parse(endDate);// 构造结束日期
// getTime()表示返回自 1970 年 1 月 1 日 00:00:00 GMT 以来此 Date 对象表示的毫秒数。
if (start.getTime() >= end.getTime()) {
return null;
}
long date = random(start.getTime(), end.getTime());
return new Date(date);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

public static long random(long begin, long end) {
long rtn = begin + (long) (Math.random() * (end - begin));
// 如果返回的是开始时间和结束时间,则递归调用本函数查找随机值
if (rtn == begin || rtn == end) {
return random(begin, end);
}
return rtn;
}
}

运行结果

查看源码

这个就很容易想了,多个线程执行时,a线程设置了时间,b线程又设置另一个时间,就会导致问题。

1.3 解决方案

三种种主流的解决方案

  1. 使用局部变量,每次调用,都有一个对象。也就是我现在的做法,将new对象的操作提取个方法出来。这个做法有个问题,频繁地创建对象,对象在方法内不被外部调用时,方法结束,就会被回收。频繁的进行创建对象、回收对象,会影响性能。
  2. 使用 ThreadLocal。使用 ThreadLocal 实现每个线程都可以得到单独的一个SimpleDateFormat的实例。与使用局部变量的不同的是,1个请求进来是1个线程,如果是使用局部变量来处理,那么每个方法里,都需要new一个SimpleDateFormat,如果三个方法用到,就有三个对象;而用ThreadLocal的话,只会产生1个对象,1个线程1个对象。
  3. java8及其以上,可以使用DateTimeFormatter,线程安全的

使用ThreadLocal,参考Java开发手册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Test {
private static final ThreadLocal<SimpleDateFormat> sdfThreadLocal = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};

public static void main(String[] args) {
SimpleDateFormat sdf = sdfThreadLocal.get();
String strDate = sdf.format(new Date());
System.out.println(strDate);
}
}

使用DateTimeFormatter

1
2
3
4
5
6
7
8
9
10
11
public class Test {
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

public static void main(String[] args) {
// DateTimeFormatter 自带的格式方法
LocalDateTime now = LocalDateTime.now();
// DateTimeFormatter 把日期对象,格式化成字符串
String strDate = formatter.format(now);
System.out.println(strDate);
}
}

二、ThreadLocal

2.1 概念

ThreadLocal类用来提供线程内部的局部变量,让这些变量在多线程访问时,能保证各个线程里的变量相对独立于其他线程内的变量。

简单理解

  1. 在多线程并发的场景下
  2. 通过ThreadLocal在同一线程不同组件中传递公共变量
  3. 每个线程的变量都是独立的,不会互相影响

2.2 使用

常用方法

  • set():将变量绑定到当前线程中
  • get():获取当前线程绑定的变量

需求

在多线程并发的场景下,实现线程隔离,也就是每个线程中的变量都是相互独立的。设置变量、取出变量都是同一个。

线程A:操作的是变量1

线程B:操作的是变量2

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
public class Test {
private String content;

public String getContent() {
return content;
}

public void setContent(String content) {
this.content = content;
}

public static void main(String[] args) {
Test test = new Test();
for(int i=0;i<5;i++){
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
test.setContent(Thread.currentThread().getName()+"的数据");
System.out.println("==========");
System.out.println(Thread.currentThread().getName()+"----->"+test.getContent());
}
});
thread.setName("线程"+i);
thread.start();
}
}
}

这就产生了线程安全问题。

解决办法就是将content绑定到ThreadLocal,这样就能保证每个线程拿到的变量都是相互独立的

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
public class Test {
ThreadLocal<String> tl=new ThreadLocal<>();
private String content;

public String getContent() {
//获取当前线程绑定的content
return tl.get();
}

public void setContent(String content) {
//变量content绑定到当前线程
tl.set(content);
}

public static void main(String[] args) {
Test test = new Test();
for(int i=0;i<5;i++){
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
test.setContent(Thread.currentThread().getName()+"的数据");
System.out.println("==========");
System.out.println(Thread.currentThread().getName()+"----->"+test.getContent());
}
});
thread.setName("线程"+i);
thread.start();
}
}
}

2.3 ThreadLocal与synchronized

用synchronized同步代码块,也能达到同样的效果

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 Test {
private String content;

public String getContent() {
return content;
}

public void setContent(String content) {
this.content = content;
}

public static void main(String[] args) {
Test test = new Test();
for(int i=0;i<5;i++){
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
synchronized (Test.class){
test.setContent(Thread.currentThread().getName()+"的数据");
System.out.println("==========");
System.out.println(Thread.currentThread().getName()+"----->"+test.getContent());
}
}
});
thread.setName("线程"+i);
thread.start();
}
}
}

synchronized加了锁,并发执行变成了只能排队进行访问,失去了并发性,效率就会降低。

synchronizedThreadLocal
原理同步机制采用时间换空间的方式,只提供了一份变量,让不同线程排队访问ThreadLocal采用空间换时间的方式,为每一个线程都提供了一份变量副本,从而实现同时访问而相互不干扰。
侧重点多个线程间访问资源同步多个线程间数据相互隔离

关于这个变量副本,就相当于每个线程中new了一个变量,并且仅有一个变量。

2.4 应用场景

1.将数据库连接通过ThreadLocal绑定到线程上,从而能保证成功回滚

2.每个线程内保存一个共享的对象,比如SimpleDateFormat,就不用每用一次就new一次了

2.5 实现原理

JDK1.8以前的ThreadLocal

每个ThreadLocal都创建一个Map,然后用当前线程作为Key,要存储的局部变量作为Map的value,这样就能达到各个线程的局部变量隔离的效果

JDK1.8以后的ThreadLocal

每个Thread内部有一个ThreadLocalMap,ThreadLocal对象本身作为Key,要存储的局部变量作为value。具体的过程

  1. 每个Thread线程内部都有一个Map,即ThreadLocalMap。如果多个线程在操作同一个threadLocal里的值,那实际上就是每个Thread里面的都有一个key为threadLocal对象,value为变量副本的map。这边动手写源码会好理解不少,可以参照下方的源码理解。
  2. Map里面存储ThreadLocal对象(key)和线程的变量副本(value)
  3. Thread内部的Map由ThreadLocal维护,由ThreadLocal负责向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
57
58
59
60
61
62
63
64
65
66
67
68
public class ThreadLocalTest {
// public static void main(String[] args) {
// ThreadLocal<Integer> threadLocal=new ThreadLocal<>();
// //设置当前线程变量
// threadLocal.set(1);;
// //获取当前线程变量
// Integer integer = threadLocal.get();
// //获取到值之后,进行清空
// threadLocal.remove();
// System.out.println(integer);
// }


ThreadLocal<String> tl=new ThreadLocal<>();
private String content;

public String getContent() {
//获取当前线程绑定的content
return tl.get();
}

public void setContent(String content) {
//变量content绑定到当前线程
tl.set(content);
}

public static void main(String[] args) {
ThreadLocalTest test = new ThreadLocalTest();
System.out.println(test.tl);
for(int i=0;i<5;i++){
@SuppressWarnings("all")
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
String content=Thread.currentThread().getName()+
"的数据"+"======获取threadLocal====>"+
test.tl;
test.setContent(content);
System.out.println(Thread.currentThread().getName()+
"----->"+
test.getContent());
}
});
thread.setName("线程"+i);
thread.start();
}
/**
* 这些线程里面,每个线程,都有一个ThreadLocalMap
* 每个ThreadLocalMap,都存储key为java.lang.ThreadLocal@44e81672,数据为自己专有数据
* 获取的时候,也是去当前线程里的map中,根据java.lang.ThreadLocal@44e81672来拿
*/
Thread[] threads=new Thread[5];
Thread.currentThread().getThreadGroup().enumerate(threads);
System.out.println(Arrays.asList(threads));

/**
* 整个过程运行结果:
*
* java.lang.ThreadLocal@44e81672
* 线程0----->线程0的数据======获取threadLocal====>java.lang.ThreadLocal@44e81672
* [Thread[main,5,main], Thread[线程0,5,main], Thread[线程1,5,main], Thread[线程2,5,main], Thread[线程3,5,main]]
* 线程2----->线程2的数据======获取threadLocal====>java.lang.ThreadLocal@44e81672
* 线程1----->线程1的数据======获取threadLocal====>java.lang.ThreadLocal@44e81672
* 线程3----->线程3的数据======获取threadLocal====>java.lang.ThreadLocal@44e81672
* 线程4----->线程4的数据======获取threadLocal====>java.lang.ThreadLocal@44e81672
*/
}
}

如此设计的好处

  1. 每个Map存储的Entry数量减少,Entry可以理解成键值对的对象。以前的方式是Map的key是存储的线程,线程越多,Entry键值对越多。现在将Map放到Thread里面,Entry数量减少。实际开发中,ThreadLocal数量往往少于Thread数量
  2. Thread销毁的时候,ThreadLocalMap也会相应的进行销毁,减少了内存的使用。

单纯的文字描述看不太懂的话,可以参照ThreadLocal的get方法源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 返回该线程局部变量的当前线程副本中的值。
* 如果变量在当前线程中没有值,它首先被初始化为initialValue方法调用返回的值。
*
* @return 当前线程中对应key为threadLocal本身的值
*/
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);// return t.threadLocals;
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

三、TransmittableThreadLocal

meethigher/transmittablethreadlocal-test: 由浅入深,简单使用transmittablethreadlocal

参考文章

3.1 ThreadLocal存在的问题

如果想要在父线程中,通过ThreadLocal赋值,子线程中取get的时候,是获取不到的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) {
ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
threadLocal.set(1);
/**
* 父线程中,给threadLocal赋值,父线程的ThreadLocalMap中存储了该threadLocal
* 在子线程的map中,没有threadLocal对象这个key,所以直接threadLocal.get()是拿不到值的
* 如果想要在异步线程中获取父线程的值,需要通过InheritableThreadLocal
*/
new Thread(new Runnable() {
@Override
public void run() {
Integer integer = threadLocal.get();//null
threadLocal.remove();
System.out.println(integer);
}
}).start();

3.2 InheritableThreadLocal

InheritableThreadLocal可以解决在父子线程中,变量共享。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) {
ThreadLocal<Integer> threadLocal=new InheritableThreadLocal<>();
threadLocal.set(1);
/**
* InheritableThreadLocal只适用于显示创建线程(new Thread())
* 如果用线程池,由于是线程重复使用,就需要使用TransmittableThreadLocal
*/
new Thread(new Runnable() {
@Override
public void run() {
Integer integer = threadLocal.get();
threadLocal.remove();
System.out.println(integer);
}
}).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
public static void main(String[] args) throws InterruptedException {
/**
* 使用InheritableThreadLocal在使用线程池时,会存在父子线程间传递数据混乱的问题
* 使用TransmittableThreadLocal解决
*/
ThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>();
ThreadPoolExecutor executor = getExecutor();
CountDownLatch firstCountDownLatch = new CountDownLatch(1);
CountDownLatch secondCountDownLatch = new CountDownLatch(1);
threadLocal.set(1);
executor.execute(() -> {
System.out.println("子线程获取" + threadLocal.get());
threadLocal.remove();
System.out.println("异步任务1");
firstCountDownLatch.countDown();
});
firstCountDownLatch.await();
System.out.println("父线程获取" + threadLocal.get());

threadLocal.set(2);
executor.execute(() -> {
System.out.println("子线程获取" + threadLocal.get());
threadLocal.remove();
System.out.println("异步任务2");
secondCountDownLatch.countDown();
});
secondCountDownLatch.await();
System.out.println("父线程获取" + threadLocal.get());

executor.shutdown();

/**
* 输出结果:
* 子线程获取1
* 异步任务1
* 父线程获取1
* 子线程获取null
* 异步任务2
* 父线程获取2
*/
}

3.3 TransmittableThreadLocal

通过TransmittableThreadLocal就可以解决上面存在的问题

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
public static void main(String[] args) throws InterruptedException {
ThreadLocal<Integer> threadLocal = new TransmittableThreadLocal<>();
ThreadPoolExecutor executor = getExecutor();
CountDownLatch firstCountDownLatch = new CountDownLatch(1);
CountDownLatch secondCountDownLatch = new CountDownLatch(1);
threadLocal.set(1);
executor.execute(TtlRunnable.get(() -> {
System.out.println("子线程获取" + threadLocal.get());
threadLocal.remove();
System.out.println("异步任务1");
firstCountDownLatch.countDown();
}));
firstCountDownLatch.await();
System.out.println("父线程获取" + threadLocal.get());

threadLocal.set(2);
executor.execute(TtlRunnable.get(() -> {
System.out.println("子线程获取" + threadLocal.get());
threadLocal.remove();
System.out.println("异步任务2");
secondCountDownLatch.countDown();
}));
secondCountDownLatch.await();
System.out.println("父线程获取" + threadLocal.get());

executor.shutdown();

/**
* 运行结果:
*
* 子线程获取1
* 异步任务1
* 父线程获取1
* 子线程获取2
* 异步任务2
* 父线程获取2
*/
}
发布:2021-08-31 23:23:06
修改:2021-12-20 22:08:27
链接:https://meethigher.top/blog/2021/threadlocal/
标签:java open 
付款码 打赏 分享
Shift+Ctrl+1 可控制工具栏