前言

了解过 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. 谈谈你对Promise的理解

    一.Promise是什么? 理解 抽象表达: Promise 是一门新的技术(ES6 规范) Promise 是 JS 中进行异步编程的新解决方案(备注:旧方案是单纯使用回调函数) 具体表达: 从语法 ...

  2. day24--Java集合07

    Java集合07 14.HashMap底层机制 (k,v)是一个Node,实现了Map.Entry<K,V>,查看HashMap的源码可以看到 jdk7.0 的HashMap底层实现[数组 ...

  3. 第三十六篇:vue3响应式(关于Proxy代理对象,Reflect反射对象)

    好家伙,这个有点难. 1.代理对象Proxy Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找.赋值.枚举.函数调用等). 拦截对象中任意属性的变化,包括:查get, ...

  4. KingbaseES 参数 - ignore_char_null_check

    KingbaseES 基于PostgreSQL进行了大量的Oracle兼容性开发,为了能同时兼容Oracle 和 PG 的特性,增加参数进行控制.以下介绍 KingbaseES 下特有的参数 igno ...

  5. centos7设置时间和上海时区并进行同步

    1.设置时区(同步时间前先设置) timedatectl set-timezone Asia/Shanghai 2.安装组件 yum -y install ntp systemctl enable n ...

  6. Taurus.MVC 微服务框架 入门开发教程:项目集成:3、客户端:其它编程语言项目集成:Java集成应用中心。

    系列目录: 本系列分为项目集成.项目部署.架构演进三个方向,后续会根据情况调整文章目录. 开源地址:https://github.com/cyq1162/Taurus.MVC 本系列第一篇:Tauru ...

  7. 高德地图与CAD图叠加显示方法汇总及优缺点分析

    前言 ​ 高德地图应用在许多领域,平常我们用的地图导航,除过正常的地图导航指引功能之外,其实还有很多实用的功能.如高德影像地图应用在包括地理.土地测量.水文学.生态学.气象学以及海洋学等方面.Auto ...

  8. 我的Vue之旅、01 深入Flexbox布局完全指南

    花了几个小时整合的"A Complete Guide to Flexbox"最新版本,介绍了flexbox的所有属性,外带几个实用的例子. 传统布局.Flexbox 布局的传统解决 ...

  9. mysql8.0.25版本设置主从数据库,并且从库只读

    具体操作步骤 说明:主从数据库版本一致 1.主库创建同步使用的用户 create user 'repl'@'%' identified with 'mysql_native_password' by ...

  10. AlertManager企业微信报警,时间是UTC时间,错8个小时的两种解决办法

    第一种 {{ (.StartsAt.Add 28800e9).Format "2020-01-02 15:04:05" }} 或者是 {{ ($alert.StartsAt.Add ...