并发编程之 ThreadLocal
前言
了解过 SimpleDateFormat 时间工具类的朋友都知道,该工具类非常好用,可以利用该类可以将日期转换成文本,或者将文本转换成日期,时间戳同样也可以。
以下代码,我们采用通用的 SimpleDateFormat 对象,在线程池 threadPool 中,将对应的 i 值调用 sec2Date 方法来实现日期转换,并且 sec2Date 方法是用 synchronized 修饰的,在多线程竞争的场景下,来达到线程安全的目的。
public class SynchronizedTest {
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
public static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(() -> System.out.println(finalI + "---" + new ThreadLocal2().sec2Date(finalI)));
}
threadPool.shutdown();
}
private synchronized String sec2Date(int seconds) {
Date date = new Date(seconds * 1000L);
String format = dateFormat.format(date);
return format;
}
}
输出结果:
但是在结果中,我们不难看出,还是会输出重复值,即使我们用了 synchronized 修饰方法,还是会出现线程不安全的情况。之所以出现这种现象,并非是我们编写的代码出了问题,毕竟在我们平时开发中,通过 synchronized 关键字确实能达到线程安全的目的,这里其实是 SimpleDateFormat 内部并不是线程安全的 导致的。
主要原因:当两个及以上线程同时使用相同的 SimpleDateFormat 对象(如 static 修饰)的话,就拿上面调用的 format 方法时,format 方法内部就会出现多个线程会同时调用 calendar.setTime 方法时,在多线程竞争的情况下,发生幻读,就会导致重复值的发生。
下面,我们去看下 SimpleDateFormat 的 format 源码,去探究下为什么会线程不安全。
以上源码就是 SimpleDateFormat 类下的 format 方法的源码,我们不需要过多了解里面具体的实现细节,我们只需要关注红色框住的内容,即 calendar.setTime(date);
,该 calendar 是 SimpleDateFormat 的父类 DateFormat 定义的一个成员变量。
由此我们可以得到一个结论:在多线程竞争的情况下,它们就会共享这个 calendar 成员变量,并去调用它的 calendar.setTime(date) 修改值,这样就会导致 date 变量被其他线程给修改或覆盖掉,就会导致最终的结果会出现重复的情况,因此 SimpleDateFormat 是线程不安全的。
解决方案一:我们只需要用 synchronized 直接修饰 dateFormat 变量,让每次只有一个线程能够操作 dateFormat 的权利,说白了就是让 synchronized 修饰的这块代码去串行执行,就可以避免发生线程不安全的情况。
public class SynchronizedTest {
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
public static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(() -> System.out.println(finalI + "---" + new SynchronizedTest().sec2Date(finalI)));
}
threadPool.shutdown();
}
private String sec2Date(int seconds) {
Date date = new Date(seconds * 1000L);
String format;
synchronized (dateFormat) {
format = dateFormat.format(date);
}
return format;
}
}
解决方案二:原理如同方案一相同(一个是锁住 dateFormat 变量,另一个是锁着整个 SynchronizedTest 类 )
public class SynchronizedTest {
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
public static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(() -> System.out.println(finalI + "---" + new SynchronizedTest().sec2Date(finalI)));
}
threadPool.shutdown();
}
private String sec2Date(int seconds) {
Date date = new Date(seconds * 1000L);
String format;
synchronized (SynchronizedTest.class) {
format = dateFormat.format(date);
}
return format;
}
}
但是加 synchronized 这种方式虽然也能保证线程安全,但是这种方式效率会比较低,毕竟同一时刻下,只能有一个线程能够执行程序,这显然不是最好的方案,下面我们来了解下更高效的方式,就是利用 ThreadLocal 类来实现。
ThreadLocal
介绍:每个线程需要一个独享的对象,每个 Thread 内有自己的实例副本,这些实例副本是不共享的,让某个需要用到的对象在线程间隔离,即每个线程都有自己的独立的对象。
使用ThreadLocal 的好处
- 达到线程安全
- 不需要加锁,提高执行效率
- 合理利用内存,节省开销
以下代码,我们构建了一个内部类 ThreadSafeFormatter
类,在类内部定义 ThreadLocal 的成员变量,并重写了 initialValue 方法,返回的参数就是 new 出来的 SimpleDateFormat 对象。
public class ThreadLocalTest {
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(() -> System.out.println(new ThreadLocalTest().sec2Date(finalI)));
}
}
private String sec2Date(int seconds) {
// 在 ThreadLocal 第一个 get 的时候把对象初始化出来,对象的初始化时机可以由我们控制
SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal2.get();
return dateFormat.format(seconds * 1000);
}
static class ThreadSafeFormatter {
// 方式一(原始方式)
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
// 初始化
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
}
};
// 方式二(Lambda表达式)
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));
}
}
输出结果:
结果中我们可以看出,没有输出重复的时间值(可以多运行几次观察下),因此我们通过 ThreadLocal 这种方式就达到了线程安全,并且还节省了系统的开销,合理利用了内存。
由此我们可以得到一个结论:每个线程的 SimpleDateFormat 是独立的,一共有 10 个。每个线程会平均执行 100 个任务,每个线程之间都是复用一个 SimpleDateFormat 对象。
ThreadLocal 源码分析
在了解 ThreadLocal 源码之前,我们先了解以下 Thread,ThreadLocalMap 以及 ThreadLocal 三者之间的关系。
首先,我们创建的每一个 Thread 对象中都持有一个 ThreadLocalMap 成员变量,而 ThreadLocalMap 中可以存放着很多的 key 为 ThreadLocal 的键值对。
主要方法介绍
- T initialValue() : 初始化,返回当前线程对应的“初始值”,这是一个延迟加载的方法,只有在调用get的时候,才会触发。
- void set(T t) : 为这个线程设置一个新值。
- T get() : 得到这个线程对应的value。如果是首次调用 get() ,则会调用 initialize 来得到这个值。
- void remove() :删除对应这个线程的值。
initialValue
SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal2.get();
在上述代码,我们并没有显式地调用这个 initialValue 方法,而是调用了 get 方法,而在 get 方法中,它会去调用
setInitialValue 方法,在 该方法内部它才会去调用我们重写的 initialValue 方法。
如果没有重写 initialValue 时,默认会返回 null
如果线程先前调用了set方法,在这种情况下,不会为线程调用本 initialValue 方法,而是直接用之前 set 进去的值。
在通常情况下,每个线程最多只能调用一次 initialValue 方法,但是如果已经调用了 remove 方法之后,再调用 get 方法,则可以再次调用 initialValue 方法。
get
get 方法是先取出当前线程的 ThreadLocalMap ,然后调用 map.getEntry 方法,把本 ThreadLocal 的引用作为参数传入,取出 map 中属于本 ThreadLocal 的value。
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程的 threadLocals 成员变量
ThreadLocalMap map = getMap(t);
if (map != null) {
// this 指的是 ThreadLocal 对象,通过 map.getEntry 来获取我们通过 set 方法设置进去的 value 值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
set
跟 get 一样,同样是先获取当前线程的引用,然后再获取当前线程的 threadLocals 成员变量,如果 threadLocals 为null ,即还未初始化,就会执行 createMap 方法来进行初始化。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
// this 指的是 ThreadLocal 对象,value 就是想要设置进去的值
map.set(this, value);
else
createMap(t, value);
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
map.set(this, value);
需要注意的是,这个 map 以及 map 中的 key 和 value 都是保存在 Thread 线程中的,而不是保存在 ThreadLocal 中。
remove
原理跟 get 和 set 类似,这里就不赘述了。
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
ThreadLocal 的内存泄露
内存泄漏:当某个对象不再有引用,但是所占用的内存不能被回收。
下面我们来看 ThreadLocal 的静态内部类 ThreadLocalMap ,ThreadLocalMap 的 Entry 其实就是存放每一个ThreadLocal 和 value 键值对的集合。
Entry 静态类的构造方法,分别执行了 super(k);
value = v;
其中 super(k)
去父类中进行初始化,而从 Entry extends 的父类我们可以看出,WeakReference 父类是一个弱引用类,则说明了 k 值是一个弱引用的, 而 value 就是一个强引用。
强引用:任何时候都不会被回收,即使发生 GC 的时候也不会被回收(赋值就是一种强引用)
弱引用:对象只被弱引用关联,在下一次 GC 时会被回收。(可以理解为只要触发一次GC,就可以扫描到并被回收掉)
由此我们可以得知,ThreadLocalMap 的每一个 Entry 都是一个对 key 的弱引用,但是每一个 Entry 都包含了一个对 value 的强引用。而由于线程池中的线程池存活时间都比较长,那么 Entry 的 key 是可以被回收掉的,但是 value 无法被回收,就会发生内存泄漏。
JDK 的设计者也考虑到了这个不足之处,所以在经常调用的方法,比如 set, remove, rehash 会主动去扫描 key 为 null 的 Entry,并把对应的 value 设置 null,这样 value 对象也可以被 GC 给回收掉。
另外在阿里巴巴 Java 开发手册也明确指出,应该显式地调用 remove 方法,删除 Entry 对象,避免内存泄漏。
【强制】 必须回收自定义的 ThreadLocal 变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的 ThreadLocal 变量,可能会影响到后续业务逻辑和造成内存泄漏等问题。尽量在代码中使用 try-finally 块进行回收。
objThreadLocal.set(someObject);
try{
...
} finally {
objThreadLocal.remove();
}
并发编程之 ThreadLocal的更多相关文章
- 并发编程之ThreadLocal
并发编程之ThreadLocal 前言 当多线程访问共享可变数据时,涉及到线程间同步的问题,并不是所有时候,都要用到共享数据,所以就需要线程封闭出场了. 数据都被封闭在各自的线程之中,就不需要同步,这 ...
- 并发编程之ThreadLocal、Volatile、synchronized、Atomic关键字扫盲
前言 对于ThreadLocal.Volatile.synchronized.Atomic这四个关键字,我想一提及到大家肯定都想到的是解决在多线程并发环境下资源的共享问题,但是要细说每一个的特点.区别 ...
- 并发编程之ThreadLocal源码分析
当访问共享的可变数据时,通常需要使用同步.一种避免同步的方式就是不共享数据,仅在单线程内部访问数据,就不需要同步.该技术称之为线程封闭. 当数据封装到线程内部,即使该数据不是线程安全的,也会实现自动线 ...
- Java并发编程之ThreadLocal解析
本文讨论的是JDK 1.8中的ThreadLocal ThreadLocal概念 ThreadLocal多线程间并发访问变量的解决方案,为每个线程提供变量的副本,用空间换时间. ThreadLocal ...
- Java并发编程之ThreadLocal类
ThreadLocal类可以理解为ThreadLocalVariable(线程局部变量),提供了get与set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回当 ...
- Java并发编程之ThreadLocal源码分析
## 1 一句话概括ThreadLocal<font face="微软雅黑" size=4> 什么是ThreadLocal?顾名思义:线程本地变量,它为每个使用该对象 ...
- 并发编程之 ThreadLocal 源码剖析
前言 首先看看 JDK 文档的描述: 该类提供了线程局部 (thread-local) 变量.这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局 ...
- 并发编程之:ThreadLocal
大家好,我是小黑,一个在互联网苟且偷生的农民工. 从前上一期[并发编程之:synchronized] 我们学到要保证在并发情况下对于共享资源的安全访问,就需要用到锁. 但是,加锁通常情况下会让运行效率 ...
- 并发编程之 Exchanger 源码分析
前言 JUC 包中除了 CountDownLatch, CyclicBarrier, Semaphore, 还有一个重要的工具,只不过相对而言使用的不多,什么呢? Exchange -- 交换器.用于 ...
随机推荐
- 趋势科技 redirfs模块的一个小问题
最近看的一个问题,消息队列可以创建,但是不能获取属性,也不能发消息,返回错误为:EBADF Bad file descriptor 经过打点,确认走入了这个流程: SYSCALL_DEFINE3(mq ...
- MySQL半同步复制源码解析
今天 DBA 同事问了一个问题,MySQL在半同步复制的场景下,当关闭从节点时使得从节点的数量 < rpl_semi_sync_master_wait_for_slave_count时,show ...
- C#,根据路径获取某个数字开头的所有文件夹,并获取最新文件夹进行替换文件
项目需求获取某路径下为1开头文件夹,并替换最新文件夹内容,话不多说,上代码 private void Form1_Load(object sender, EventArgs e) { try { st ...
- C#,拷贝文件到另一个文件夹下,替换文件夹中的文件
/// <summary> /// 拷贝文件到另一个文件夹下 /// </summary> /// <param name="sourceName"& ...
- k8s驱逐篇(4)-kube-scheduler抢占调度驱逐
介绍kube-scheduler抢占调度驱逐之前,先简单的介绍下kube-scheduler组件: kube-scheduler简介 kube-scheduler组件是kubernetes中的核心组件 ...
- Logstash: 启动监控及集中管理
在本篇文章里,我将详细介绍如果启动Logstash的监控及集中管理. 前提条件 安装好Logstash,设置Elasticsearch及Kibana的安全密码. 如何监控Logstash? 我们安装如 ...
- k8s中pod的容器日志查看命令
如果容器已经崩溃停止,您可以仍然使用 kubectl logs --previous 获取该容器的日志,只不过需要添加参数 --previous. 如果 Pod 中包含多个容器,而您想要看其中某一个容 ...
- python动态参数
Python的动态参数有两种,分别是*args和**kwargs,这里面的关键是一个和两个星号的区别,而不是args和kwargs在名字上的区别,实际上你可以使用*any或**whatever的方式. ...
- 内网横向渗透 之 ATT&CK系列一 之 横向渗透域主机
前言 上一篇文章中已获取了关于域的一些基本信息,在这里再整理一下,不知道信息收集的小伙伴可以看回上一篇文章哦 域:god.org 域控 windows server 2008:OWA,192.168. ...
- CentOS7内置Realtek网卡驱动r8169降级r8168
前几天装了几台服务器测试,在使用的过程中发现,每次重启系统,登录界面会弹出网卡提示 "r8169 0000:02:00 eth0 Invalid ocp reg 17758!" ...