什么是 ThreadLocal 变量?
ThreadLocal 变量是线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的线程中有不同的副本。这里需要注意以下两点:
- 因为每一个线程内都有自己的实例副本,而且该副本只能由当前的线程使用。这也是 ThreadLocal 命名的由来
- 既然每一个线程都有自己的实例副本,而其它的线程不可访问,那就不存在多线程之间的共享问题
ThreadLocal 提供了线程本地的实例。它与普通的变量区别在于每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被 private static 修饰,这样做的目的是为了让 ThreadLocal 变成强引用
总得来说:ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用也即变量在线程之间隔离而在方法或类间共享的场景
ThreadLocal 的实现原理
ThreadLocal 是一个泛型类,保证可以接收任何类型的对象
因为一个线程内可以存在多个 ThreadLocal 对象,所以其实是 ThreadLocal 内部维护了一个 Map,但这个 Map 不是直接使用 HashMap,而是 ThreadLocal 实现的一个叫做 ThreadLocalMap 的静态内部类
ThreadLocal 类的常用方法
而我们使用的 get()、set() 方法其实都是调用了这个ThreadLocalMap类对应的 get()、set() 方法。例如下面的 set 方法:
set(T value)
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
get()
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
return (T)map.get(this);
// Maps are constructed lazily. if the map for this thread
// doesn't exist, create it, with this ThreadLocal and its
// initial value as its only entry.
T value = initialValue();
createMap(t, value);
return value;
}
createMap(Thread t, T firstValue)
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
内存泄漏问题
实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。
所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。
ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。如果说会出现内存泄漏,那只有在出现了 key 为 null 的记录后,没有手动调用 remove() 方法,并且之后也不再调用 get()、set()、remove() 方法的情况下
使用场景
如上文所述,ThreadLocal 适用于如下两种场景
- 每个线程需要有自己单独的实例:例如可以在线程内部构建一个单独的实例。ThreadLoca 可以以非常方便的形式满足该需求
- 实例需要在多个方法中共享,但不希望被多线程共享:在满足第一种场景(每个线程有自己的实例)的情况下通过方法间引用传递的形式实现。ThreadLocal 使得代码耦合度更低,且实现更优雅
举例:(现在定义一个这样的结构)
package com.java.springtest.test;
/**
* @author Woo_home
* @create by 2020/1/22
*/
class Message { // 要发送的消息体
private String info;
public String getInfo() {
return info;
}
public void setInfo(String info) {
this.info = info;
}
}
class Channel { // 消息的发送通道
private static Message message;
public Channel() {
}
public static void setMessage(Message msg) {
message = msg;
}
public static void send() { // 发送消息
System.out.println("【消息发送】" + message.getInfo());
}
}
public class Demo2 {
public static void main(String[] args) {
Message message = new Message(); // 实例化消息主体对象
message.setInfo("www.baidu.com");// 设置要发送的内容
Channel.setMessage(message); // 设置要发送的消息
Channel.send(); // 发送
}
}
对于当前的程序实际上采用的是一种单线程的模式来进行处理的。那么在多线程的状态下能否实现完成一致的操作效果呢?为此我们将启动三个线程进行处理
举例:(多线程的影响)
package com.java.springtest.test;
/**
* @author Woo_home
* @create by 2020/1/22
*/
class Message { // 要发送的消息体
private String info;
public String getInfo() {
return info;
}
public void setInfo(String info) {
this.info = info;
}
}
class Channel { // 消息的发送通道
private static Message message;
public Channel() {
}
public static void setMessage(Message msg) {
message = msg;
}
public static void send() { // 发送消息
System.out.println("【" + Thread.currentThread().getName() + "消息发送】" + message.getInfo());
}
}
public class Demo2 {
public static void main(String[] args) {
new Thread(() -> {
Message message = new Message(); // 实例化消息主体对象
message.setInfo("第一个线程的消息");// 设置要发送的内容
Channel.setMessage(message); // 设置要发送的消息
Channel.send();
},"消息发送者 A").start();
new Thread(() -> {
Message message = new Message(); // 实例化消息主体对象
message.setInfo("第二个线程的消息");// 设置要发送的内容
Channel.setMessage(message); // 设置要发送的消息
Channel.send();
},"消息发送者 B").start();
new Thread(() -> {
Message message = new Message(); // 实例化消息主体对象
message.setInfo("第三个线程的消息");// 设置要发送的内容
Channel.setMessage(message); // 设置要发送的消息
Channel.send();
},"消息发送者 C").start();
}
}
这个时候消息的处理产生了影响,B 发送了 C 的消息,当然,你多执行几次可能会出现 A 发送 B 的消息,自己去实践即可
在保持 Channel(所有发送通道) 核心结构不改变的情况下,需要考虑到每个线程的独立操作问题,那么在这样的情况下就可以发现对于 Channel 类而言除了要保留有发送的消息之外,还应该多存放有一个每一个线程的标记(当前线程),那么这个时候就可以通过 ThreadLocal 类来存放数据。在 ThreadLocal 类里面提供了有如下操作方法:
- 构造方法:public ThreadLocal()
- 设置数据:public void set(T value)
- 取出数据:public T get()
- 删除数据:public void remove()
举例:(解决线程同步问题)
package com.java.springtest.test;
/**
* @author Woo_home
* @create by 2020/1/22
*/
class Message { // 要发送的消息体
private String info;
public String getInfo() {
return info;
}
public void setInfo(String info) {
this.info = info;
}
}
class Channel { // 消息的发送通道
private static final ThreadLocal<Message> THREAD_LOCAL_MESSAGE = new ThreadLocal<>();
public Channel() {
}
public static void setMessage(Message msg) {
THREAD_LOCAL_MESSAGE.set(msg); // 向 ThreadLocal 中保存数据
}
public static void send() { // 发送消息
// 向 ThreadLocal 获取数据
System.out.println("【" + Thread.currentThread().getName() + "消息发送】" + THREAD_LOCAL_MESSAGE.get().getInfo());
}
}
public class Demo2 {
public static void main(String[] args) {
new Thread(() -> {
Message message = new Message(); // 实例化消息主体对象
message.setInfo("第一个线程的消息");// 设置要发送的内容
Channel.setMessage(message); // 设置要发送的消息
Channel.send();
},"消息发送者 A").start();
new Thread(() -> {
Message message = new Message(); // 实例化消息主体对象
message.setInfo("第二个线程的消息");// 设置要发送的内容
Channel.setMessage(message); // 设置要发送的消息
Channel.send();
},"消息发送者 B").start();
new Thread(() -> {
Message message = new Message(); // 实例化消息主体对象
message.setInfo("第三个线程的消息");// 设置要发送的内容
Channel.setMessage(message); // 设置要发送的消息
Channel.send();
},"消息发送者 C").start();
}
}
输出:这个时候就不会出现不同步的情况了
简单地说:每一个线程通过 ThreadLocal 只允许保存一个数据
解决线程安全问题
比如Java7中的SimpleDateFormat不是线程安全的,可以用ThreadLocal来解决这个问题:
public class DateUtil {
private static ThreadLocal<SimpleDateFormat> format1 = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
public static String formatDate(Date date) {
return format1.get().format(date);
}
}
这里的DateUtil.formatDate()就是线程安全的了。(Java8里的 java.time.format.DateTimeFormatter是线程安全的,Joda time里的DateTimeFormat也是线程安全的)
转载:https://blog.csdn.net/Woo_home/article/details/104069209