摘要
之前代码Review的时候,大佬们说到了,我的写法会引起线程安全问题,简单了解下。
正文
参考文章
我的写法就类似于下面这种,将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的,所以应该不存在该问题。
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 解决方案
三种种主流的解决方案
- 使用局部变量,每次调用,都有一个对象。也就是我现在的做法,将new对象的操作提取个方法出来。这个做法有个问题,频繁地创建对象,对象在方法内不被外部调用时,方法结束,就会被回收。频繁的进行创建对象、回收对象,会影响性能。
- 使用 ThreadLocal。使用 ThreadLocal 实现每个线程都可以得到单独的一个SimpleDateFormat的实例。与使用局部变量的不同的是,1个请求进来是1个线程,如果是使用局部变量来处理,那么每个方法里,都需要new一个SimpleDateFormat,如果三个方法用到,就有三个对象;而用ThreadLocal的话,只会产生1个对象,1个线程1个对象。
- 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类用来提供线程内部的局部变量,让这些变量在多线程访问时,能保证各个线程里的变量相对独立于其他线程内的变量。
简单理解
- 在多线程并发的场景下
- 通过ThreadLocal在同一线程不同组件中传递公共变量
- 每个线程的变量都是独立的,不会互相影响
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加了锁,并发执行变成了只能排队进行访问,失去了并发性,效率就会降低。
| synchronized | ThreadLocal |
|---|
| 原理 | 同步机制采用时间换空间的方式,只提供了一份变量,让不同线程排队访问 | 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。具体的过程
- 每个Thread线程内部都有一个Map,即ThreadLocalMap。如果多个线程在操作同一个threadLocal里的值,那实际上就是每个Thread里面的都有一个key为threadLocal对象,value为变量副本的map。这边动手写源码会好理解不少,可以参照下方的源码理解。
- Map里面存储ThreadLocal对象(key)和线程的变量副本(value)
- Thread内部的Map由ThreadLocal维护,由ThreadLocal负责向map获取和设置线程的变量值
![10.png 10.png]()
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
*/
}
}
|
如此设计的好处
- 每个Map存储的Entry数量减少,Entry可以理解成键值对的对象。以前的方式是Map的key是存储的线程,线程越多,Entry键值对越多。现在将Map放到Thread里面,Entry数量减少。实际开发中,ThreadLocal数量往往少于Thread数量
- 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
*/
}
|