前言

了解过 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的更多相关文章

  1. 并发编程之ThreadLocal

    并发编程之ThreadLocal 前言 当多线程访问共享可变数据时,涉及到线程间同步的问题,并不是所有时候,都要用到共享数据,所以就需要线程封闭出场了. 数据都被封闭在各自的线程之中,就不需要同步,这 ...

  2. 并发编程之ThreadLocal、Volatile、synchronized、Atomic关键字扫盲

    前言 对于ThreadLocal.Volatile.synchronized.Atomic这四个关键字,我想一提及到大家肯定都想到的是解决在多线程并发环境下资源的共享问题,但是要细说每一个的特点.区别 ...

  3. 并发编程之ThreadLocal源码分析

    当访问共享的可变数据时,通常需要使用同步.一种避免同步的方式就是不共享数据,仅在单线程内部访问数据,就不需要同步.该技术称之为线程封闭. 当数据封装到线程内部,即使该数据不是线程安全的,也会实现自动线 ...

  4. Java并发编程之ThreadLocal解析

    本文讨论的是JDK 1.8中的ThreadLocal ThreadLocal概念 ThreadLocal多线程间并发访问变量的解决方案,为每个线程提供变量的副本,用空间换时间. ThreadLocal ...

  5. Java并发编程之ThreadLocal类

    ThreadLocal类可以理解为ThreadLocalVariable(线程局部变量),提供了get与set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回当 ...

  6. Java并发编程之ThreadLocal源码分析

    ## 1 一句话概括ThreadLocal<font face="微软雅黑" size=4>  什么是ThreadLocal?顾名思义:线程本地变量,它为每个使用该对象 ...

  7. 并发编程之 ThreadLocal 源码剖析

    前言 首先看看 JDK 文档的描述: 该类提供了线程局部 (thread-local) 变量.这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局 ...

  8. 并发编程之:ThreadLocal

    大家好,我是小黑,一个在互联网苟且偷生的农民工. 从前上一期[并发编程之:synchronized] 我们学到要保证在并发情况下对于共享资源的安全访问,就需要用到锁. 但是,加锁通常情况下会让运行效率 ...

  9. 并发编程之 Exchanger 源码分析

    前言 JUC 包中除了 CountDownLatch, CyclicBarrier, Semaphore, 还有一个重要的工具,只不过相对而言使用的不多,什么呢? Exchange -- 交换器.用于 ...

随机推荐

  1. 常用类--String

    一.String 1.1 String是不可变对象 String的底层是一个 char类型字符数组 String类是final修饰的,不能被继承,不能改变,但引用可以重新赋值 String采用的编码方 ...

  2. Java SE 9 模块化示例

    Java SE 9 模块化示例 作者:Grey 原文地址:Java SE 9 模块化示例 说明 Java SE 9引入了模块系统,模块就是代码和数据的封装体.模块的代码被组织成多个包,每个包中包含Ja ...

  3. q 短引用标签

    <q/>标签可以使一段文本作为引用. <p>他说:<q>明天要下雨</q>.</p> 注意,源代码中并没有为这段文字添加引用符号,而是添加了 ...

  4. C#基础_XML文件介绍

    XML简介 XML 被设计用来传输和存储数据. HTML 被设计用来显示数据. 什么是 XML? XML 指可扩展标记语言(EXtensible Markup Language) XML 是一种标记语 ...

  5. 【JDBC】学习路径8-连接池

    为什么是连接池? 第一.受我们硬件资源的限制,我们的一些资源使用时有限制的比如我们的数据库 连接数和线程数.为了摆脱这些限制,我们就使用了池化技术来将这些资源限制在一定范围内. 第二.我们创建和销毁这 ...

  6. Spring的俩大核心概念:IOC、AOP

    1.Spring 有两个核心部分: IOC 和 Aop (1)IOC:控制反转,把创建对象过程交给 Spring 进行管理   (2)Aop:面向切面,不修改源代码进行功能增强 2.Spring 特点 ...

  7. KingbaseES V8R6集群维护案例之--单实例数据迁移到集群案例

    案例说明: 生产环境是单实例,测试环境是集群,现需要将生产环境的数据迁移到集群中运行,本文档详细介绍了从单实例环境恢复数据到集群环境的操作步骤,可以作为生产环境迁移数据的参考. 适用版本: Kingb ...

  8. Windows编程之线程同步

    本笔记整理自:<Windows核心编程(第五版)> 目录 什么是线程同步 用户方式中的线程同步 原子访问:Interlocked系列函数 CRITICAL_SECTION:关键段 内核对象 ...

  9. setw()

    setw() 头文件是 #include<iomanip> setw(2)是下一个数据输出宽度为2,超过2则以实际输出为准,不足2补空格.仅对下一个数据的输出有效,即只有一次效果.(set ...

  10. flutter系列之:Material中的3D组件Card

    目录 简介 Card详解 Card的使用 总结 简介 除了通用的组件之外,flutter还提供了两种风格的特殊组件,其中在Material风格中,有一个Card组件,可以很方便的绘制出卡片风格的界面, ...