抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

threadLocal是什么?

本地线程变量,他填充的是当前线程的变量,该变量对其他线程是封闭且隔离的,ThreadLocal给每个线程中的
变量创建了一个副本,这样每个线程都可以访问自己内部的副本变量

ThreadLocal的两大使用场景

场景一:每个线程需要一个独享的对象(通常时工具类,典型需要使用的类有SimpleDateFormat和Random)
场景二:每个线程内需要保存全局变量(例如在拦截器中获取用户信息),可以让不同的方法直接使用,避免参数传递的麻烦
两种场景是有很大的区别的:
第一种诉求是工具类线程不安全,所以让每个线程有独立的工具类
第二种诉求是避免参数传递的安全

场景一示例

2个线程分别用自己的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
package threadlocal;

import java.text.SimpleDateFormat;
import java.util.Date;
/*
* 两个线程打印日期
* */
public class ThreadLocalNormalUsage00 {
public String date(int seconds){
//参数单位是毫秒,从1970.1.1 00:00:00 GMT计时,乘以1000是转换成秒
Date date = new Date(1000*seconds);
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-mm-dd hh:mm:ss");
return simpleDateFormat.format(date);
}

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(1007);
System.out.println(date);
}
}).start();
}
}

当用多个线程分别用自己的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
package threadlocal;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/*
* 1000个打印日期任务,使用线程池来执行
* */
public class ThreadLocalNormalUsage02 {
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
public String date(int seconds){
//参数单位是毫秒
Date date = new Date(1000*seconds);
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-mm-dd hh:mm:ss");
return simpleDateFormat.format(date);
}

public static void main(String[] args){
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage02().date(finalI);
System.out.println(date);
}
});

}
threadPool.shutdown();
}
}

上面的代码并不好因为每次打印日期的过程都会创建一个SimpleDateFormat对象,会浪费资源
所以考虑只用一个SimpleDateFormat对象,于是使用一个静态的SimpleDateFormat对象,但是在使用
这一个对象时出现了日期相同问题,原因是多线程共用了一个对象,发生线程安全问题。
所以考虑使用加锁问题:(但是虽然加锁解决了安全问题,但是在高并发的情况下影响了效率)

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
* 1000个打印日期任务,使用线程池来执行,使线程公用一个SimpleDateFormat对象,这种情况会出现线程安全问题,所以为了解决问题加锁变得安全,
* 虽然添加了锁但是在效率问题还是很低,因为添加了锁所以线程需要排队
* */
public String date(int seconds){
//参数单位是毫秒
Date date = new Date(1000*seconds);
String s = null;
synchronized (ThreadLocalNormalUsage04.class) {
s = simpleDateFormat.format(date);
}
return s;
}

解决这个问题的最好方法是使用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
31
32
33
34
35
36
37
38
39
40
41
42
package threadlocal;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/*
* 利用threadLocal,给每个线程分配自己的dateFormat对象,保证线程安全,高效利用内存
* */
public class ThreadLocalNormalUsage05 {
public static void main(String[] args){
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage05().date(finalI);
System.out.println(date);
}
});

}
threadPool.shutdown();
}
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
public String date(int seconds){
//参数单位是毫秒
Date date = new Date(1000*seconds);
// SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-mm-dd hh:mm:ss");
SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
return dateFormat.format(date);
}
}
class ThreadSafeFormatter{
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>(){
protected SimpleDateFormat initialValue(){
return new SimpleDateFormat("yyyy-mm-dd hh:mm:ss");
}
};

}

场景二(强调的是同一线程不同方法间的共享)

实例:当前用户信息需要被线程内的所有方法共享
方法一:使用一个UserMap存储当前用户信息,但是在多线程的情况下我们要保证线程的安全
可以使用synchronized也可可以使用ConcurrentHashMap,但是无论使用那个性能会有所影响
方法二(最优):更好的方法是使用ThreadLocal,这样无需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
30
31
32
33
34
35
36
37
38
39
40
package threadlocal;
/*
* 演示ThreadLocal用法2:避免传递参数的麻烦
* */
public class ThreadLocalNormalUsage06 {
public static void main(String[] args) {
new Service1().process();
}
}
class UserContextHolder{
public static ThreadLocal<User> holder = new ThreadLocal<>();

}
class User{
String name;
public User(String name) {
this.name = name;
}
}
class Service1{
public void process(){
User user = new User("tom");
UserContextHolder.holder.set(user);
new Service2().process();
}
}
class Service2{
public void process(){
User user = UserContextHolder.holder.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);
}
}

ThreadLocal的两个作用

1.让某个需要用到的对象在线程间隔离(每个线程都有自己独立的对象)

1
2
3
4
5
6
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>(){
protected SimpleDateFormat initialValue(){
return new SimpleDateFormat("yyyy-mm-dd hh:mm:ss");
}
};

2.在任何方法中都可以轻松获取对象

1
2
3
4
5
6
7
8
9
10
11
class UserContextHolder{
public static ThreadLocal<User> holder = new ThreadLocal<>();

}
class Service1{
public void process(){
User user = new User("tom");
UserContextHolder.holder.set(user);
new Service2().process();
}
}

场景一:initialValue
在ThreadLocal第一次get的时候把对象给初始化出来,对象的初始化时机可以由我们自己控制
场景二:set
ThreadLocal里的对象生成时机不由我们随意控制,例如拦截器生成的用户信息

ThreadLocal使用的好处

1.线程安全
2.不需要加锁,效率高
3.更高效的利用内存、节省开销
相比于每个任务都新建一个SimpleDateFormat,显然用ThreadLocal可以节省内存和开销
4.避免传参数的麻烦

ThreadLocal的主要方法

1.initialValue()
该方法会返回当前线程对应的“初始值”,这是一个延迟加载的方法,只有调用get()的时候才会触发。
2.set(T t)
3.get()
4.remove() 删除线程对应的值

Thread,ThreadLocal和ThreadLocalMap三者之间的关系

avatar