JUC-ThreadLocal-0-基本使用

了解了线程池之后,再来看一个常用的类,就是 ThreadLocal 这个类在面试中也是很常见的,下面来看一下这个类常见的使用场景

常见使用场景

ThreadLocal 比较常见的有两个地方

  1. 每个线程都需要一个独享的对象,通常是工具类,比如经常用的 SimpleDateFormatRandom ,这两个类都不是线程安全的类,使用 ThreadLocal 就可以保证线程安全
  2. 每个线程内需要保存一个全局变量,让不通的方法使用,这种场景可能需要把这个全局变量一级一级通过参数传递, 使用 ThreadLocal 可以避免这种参数传递的麻烦

下面分别看以下这两个场景

第一种场景

以常见的时间转换工具类 SimpleDateFormat 为例,写一个测试的示例,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Slf4j
public class ThreadLocalTest1 {

public String date(long mill){
Date date = new Date(mill);
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
return simpleDateFormat.format(date);
}

public static void main(String[] args) {
Thread thread = new Thread(() -> new ThreadLocalTest1().date(System.currentTimeMillis()));
thread.start();
}
}

代码很简单,就是创建一个 SimpleDateFormat 做时间转换,上面这段只有一个线程,而且 SimpleDateFormat 也是局部变量,所以是没有线程安全的问题的

当任务数量很大的时候,每个线程都会去执行创建 SimpleDateFormat 的实例,这就可能造成资源的浪费,我们可以把这个工具类转成共享变量去处理,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Slf4j
public class ThreadLocalTest1 {

private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

public String date(long mill) {
Date date = new Date(mill);
return simpleDateFormat.format(date);
}

public static void main(String[] args) {
Thread thread = new Thread(() -> new ThreadLocalTest1().date(System.currentTimeMillis()));
thread.start();
Thread thread1 = new Thread(() -> new ThreadLocalTest1().date(System.currentTimeMillis()));
thread1.start();
Thread thread2 = new Thread(() -> new ThreadLocalTest1().date(System.currentTimeMillis()));
thread2.start();
}
}

这样就只会创建一个对象,但是这里是会有线程安全的问题的, 这里可以去通过加锁同步的方式去实现,但是同步的方式效率太低了,所以这种情况下就可以去使用 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
@Slf4j
public class ThreadLocalTest1 {

public String date(long mill) {
Date date = new Date(mill);
return ThreadSafeSimpleDateFormat.simpleDateFormatThreadLocal.get().format(date);
}

public static void main(String[] args) {
Thread thread = new Thread(() -> new ThreadLocalTest1().date(System.currentTimeMillis()));
thread.start();
}

}


class ThreadSafeSimpleDateFormat{
// public static ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>(){
// @Override
// protected SimpleDateFormat initialValue() {
// return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
// }
// };

public static ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));
}

这里首先创建一个类,ThreadSafeSimpleDateFormat 里面就存放了 ThreadLocal<SimpleDateFormat> 需要的时候通过这个类去拿

ThreadLocal 初始化有两种方法,一种是直接重写 initialValue() 方法,另一种是 ThreadLocal.withInitial() 用 lambda 实现,第二种更加简洁,但是效果一样的

以上就是 ThreadLocal 的第一种用法,然后看第二种场景

第二种方式

举个例子,假如有一个web请求,这个请求,会调用依次调用 service1() service2() service3() 这三个方法,这三个方法里面都需要一个user参数,通常情况就是在最上面一层获取到user对象,然后一级一级通过参数传递下去,这样就有点繁琐,这种情况下也可以通过 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
@Slf4j
public class ThreadLocalTest2 {
public static void main(String[] args) {
new Service1().process();
}
}

class Service1{
public void process(){
User user = new User("张三");
UserHolder.userThreadLocal.set(user);
new Service2().process();
}
}

@Slf4j
class Service2{
public void process(){
User user = UserHolder.userThreadLocal.get();
log.info("获取到用户信息:{}", user.getName());
new Service3().process();
}
}

@Slf4j
class Service3{
public void process(){
User user = UserHolder.userThreadLocal.get();
log.info("获取到用户信息:{}", user.getName());
}
}

class UserHolder{
public static ThreadLocal<User> userThreadLocal = new ThreadLocal<User>();
}

@AllArgsConstructor
@NoArgsConstructor
@Data
class User{
private String name;
}

这里主要看一下 UserHolder ,这里直接使用 new 来创建对象,并没有做初始化,之后在 Service1 中通过 set() 方法把对象传过去

按上面这种实现方式就可以实现不同的请求对应不通的线程,然后各个线程存储自己对应的 User 对象,这里主要强调的是同一个线程内的不同方法之间的共享

总结

通过两个案例对 ThreadLocal 的用法有一个基本的了解

总结一下 ThreadLocal 的两个作用

  • 让某个需要用到的对象在线程间隔离,每个线程都有自己独立的对象
  • 在同一个线程的任何方法中都可以轻松获取到该对象

然后是 ThreadLocal 设置对象的两种方式:

  • 通过 initialValue 设置对象,然后这个是会懒加载的,在调用 get() 方法的时候才回去初始化
  • 第二种就是通过 set() 方法设置

使用 ThreadLocal 的优点:

  • 线程安全
  • 不需要加锁,提高执行效率
  • 高效利用内存,节省开销
  • 免去繁琐的传参