0%

Java 同步和协作工具类(五):理解 ThreadLocal

1. 怎样理解 ThreadLocal

  • Java 中有一个特殊的概念:线程本地变量,这个概念在 Java 中的实现类就是 ThreadLocal,是一个泛型类
  • 线程本地变量 ThreadLocal 的含义是:每个线程都有同一个变量的独有拷贝

2. 写出下面程序的运行结果并分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ThreadLocalBasic {
static ThreadLocal<Integer> local = new ThreadLocal<> ();

public static void main(String[] args) throws InterruptedException {
Thread child = new Thread() {
@Override
public void run() {
System.out.println("child thread initial: " + local.get());
local.set(200);
System.out.println("child thread final: " + local.get());
}
};

local.set(100);
child.start();
child.join();
System.out.println("main thread final: " + local.get());
}
}
  • 结果:

    1
    2
    3
    child thread initial: null
    child thread final: 200
    main thread final: 100
  • 分析:

    • 主线程 main 对线程本地变量 local 的设置对子线程 child 不起作用
    • 同样的,子线程 child 对线程本地变量 local 的改变也不会影响主线程 main
    • 二者访问的虽然是同一个变量 local,但每个线程都有自己的独立的值,这就是线程本地变量的含义

3. ThreadLocal 的主要方法有哪些

  • ThreadLocal 是一个泛型类,接受一个类型参数 T,它只有一个空的构造方法
  • public T get()获取值,如果没有,返回 null
  • public void set(T value)设置值
  • protected T initialValue()提供初始值,是一个受保护方法,可以通过匿名内部类的方式提供。当调用 get() 方法时,如果之前没有设置过,会调用该方法获取初始值,默认实现是返回 null
  • public void remove()删掉当前线程对应的值,如果删掉后,再次调用 get(),会再调用 initialValue() 获取初始值

4. 线程本地变量 ThreadLocal 的使用场景

  • 日期处理
  • 随机数
  • 上下文信息

5. Java 中的日期和时间操作类 DateFormat/SimpleDateFormat 不是线程安全的,那实现线程安全的方法有哪些

  • 使用
  • 每次都创建一个新的对象
  • 更好的方式就是使用线程本地变量 ThreadLocal

6. 写一个 ThreadLocal 在日期处理的应用实现线程安全的 Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ThreadLocalDateFormat {
static ThreadLocal<DateFormat> sdf = new ThreadLocal<DateFormat> () { //ThreadLocal 对象一般都定义为 static,便于引用
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};

public static String date2String(Date date) {
return sdf.get().format(date);
}

public static Date string2Date(String str) throws ParseException {
return sdf.get().parse(str);
}
}

7. ThreadLocal 在随机数中是怎样应用的

  • 即使对象是线程安全的,使用 ThreadLocal 也可以减少竞争
  • Random 是线程安全的,但如果并发访问竞争激烈的话,性能会下降
  • 所以,Java 并发包提供了类 ThreadLocalRandom,它是 Random 的子类,利用了 ThreadLocal

8. ThreadLocal 在上下文信息中的应用

  • ThreadLocal 的典型用途是提供上下文信息
  • 举个例子
    • 在一个 Web 服务器中,一个线程执行用户的请求,在执行过程中,很多代码都会访问一些共同的信息,比如请求信息、用户身份信息、数据库连接、当前事务等
    • 它们是线程执行过程中的全局信息,如果作为参数在不同代码间传递,代码会很烦琐。这时,使用 ThreadLocal 就很方便,所以被用于各种框架如 Spring

9. 写一个使用 ThreadLocal 保存上下文信息的 Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class RequestContext { 
private static ThreadLocal<String> localUserId = new ThreadLocal<> ();
private static ThreadLocal<Request> localRequest = new ThreadLocal<> ();

public static class Request {

}

public static String getCurrentUserId() {
return localUserId.get();
}

public static void setCurrentUserId(String userId) {
localUserId.set(userId);
}

public static Request getCurrentRequest() {
return localRequest.get();
}

public static void setCurrentRequest(Request request) {
localRequest.set(request);
}
}

10. ThreadLocal 的实现原理

11. 总结一下 Java 并发包中的一些同步协作工具

  • 读多写少的场景中使用 ReentrantReadWriteLock 代替 ReentrantLock,以提高性能
  • 使用 Semaphore 限制对资源的并发访问数
  • 使用 CountDownLatch 实现不同角色线程间的同步
  • 使用 CyclicBarrier 实现同一角色线程间的协调一致

12. 简单总结下 ThreadLocal

  • ThreadLocal 使得每个线程对同一个变量有自己的独立副本,是实现线程安全减少竞争的一种方案
  • ThreadLocal 经常用于存储上下文信息,避免在不同代码间来回传递,简化代码
  • 每个线程都有一个 Map,调用 ThreadLocal 对象的 get()/set() 方法实际上就是以 ThreadLocal 对象为键读写当前线程的该 Map
-------------------- 本文结束感谢您的阅读 --------------------