1.什么是线程不安全?

线程不安全也叫非线程安全,是指多线程执行中,程序的执行结果和预期的结果不符的情况就叫做线程不安全

线程不安全的代码

SimpleDateFormat 就是一个典型的线程不安全事例,接下来我们动手来实现一下。首先我们先创建 10 个线程来格式化时间,时间格式化每次传递的待格式化时间都是不同的,所以程序如果正确执行将会打印 10 个不同的值,接下来我们来看具体的代码实现:

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; public class SimpleDateFormatExample {
// 创建 SimpleDateFormat 对象
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss"); public static void main(String[] args) {
// 创建线程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);
// 执行 10 次时间格式化
for (int i = 0; i < 10; i++) {
int finalI = i;
// 线程池执行任务
threadPool.execute(new Runnable() {
@Override
public void run() {
// 创建时间对象
Date date = new Date(finalI * 1000);
// 执行时间格式化并打印结果
System.out.println(simpleDateFormat.format(date));
}
});
}
}
}

我们预期的正确结果是这样的(10 次打印的值都不同):



然而,以上程序的运行结果却是这样的:



从上述结果可以看出,当在多线程中使用 SimpleDateFormat 进行时间格式化是线程不安全的。

2.解决方案

SimpleDateFormat 线程不安全的解决方案总共包含以下 5 种:

  1. SimpleDateFormat 定义为局部变量;
  2. 使用 synchronized 加锁执行;
  3. 使用 Lock 加锁执行(和解决方案 2 类似);
  4. 使用 ThreadLocal
  5. 使用 JDK 8 中提供的 DateTimeFormat

接下来我们分别来看每种解决方案的具体实现。

① 将SimpleDateFormat变为局部变量

SimpleDateFormat 定义为局部变量时,因为每个线程都是独享 SimpleDateFormat 对象的,相当于将多线程程序变成“单线程”程序了,所以不会有线程不安全的问题,具体实现代码如下:

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; public class SimpleDateFormatExample {
public static void main(String[] args) {
// 创建线程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);
// 执行 10 次时间格式化
for (int i = 0; i < 10; i++) {
int finalI = i;
// 线程池执行任务
threadPool.execute(new Runnable() {
@Override
public void run() {
// 创建 SimpleDateFormat 对象
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
// 创建时间对象
Date date = new Date(finalI * 1000);
// 执行时间格式化并打印结果
System.out.println(simpleDateFormat.format(date));
}
});
}
// 任务执行完之后关闭线程池
threadPool.shutdown();
}
}

以上程序的执行结果为:



当打印的结果都不相同时,表示程序的执行是正确的,从上述结果可以看出,将 SimpleDateFormat 定义为局部变量之后,就可以成功的解决线程不安全问题了。

② 使用synchronized加锁

锁是解决线程不安全问题最常用的手段,接下来我们先用 synchronized 来加锁进行时间格式化,实现代码如下:

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; public class SimpleDateFormatExample2 {
// 创建 SimpleDateFormat 对象
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss"); public static void main(String[] args) {
// 创建线程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);
// 执行 10 次时间格式化
for (int i = 0; i < 10; i++) {
int finalI = i;
// 线程池执行任务
threadPool.execute(new Runnable() {
@Override
public void run() {
// 创建时间对象
Date date = new Date(finalI * 1000);
// 定义格式化的结果
String result = null;
synchronized (simpleDateFormat) {
// 时间格式化
result = simpleDateFormat.format(date);
}
// 打印结果
System.out.println(result);
}
});
}
// 任务执行完之后关闭线程池
threadPool.shutdown();
}
}

以上程序的执行结果为:

③ 使用Lock加锁

在 Java 语言中,锁的常用实现方式有两种,除了 synchronized 之外,还可以使用手动锁 Lock,接下来我们使用 Lock 来对线程不安全的代码进行改造,实现代码如下:

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; /**
* Lock 解决线程不安全问题
*/
public class SimpleDateFormatExample3 {
// 创建 SimpleDateFormat 对象
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss"); public static void main(String[] args) {
// 创建线程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);
// 创建 Lock 锁
Lock lock = new ReentrantLock();
// 执行 10 次时间格式化
for (int i = 0; i < 10; i++) {
int finalI = i;
// 线程池执行任务
threadPool.execute(new Runnable() {
@Override
public void run() {
// 创建时间对象
Date date = new Date(finalI * 1000);
// 定义格式化的结果
String result = null;
// 加锁
lock.lock();
try {
// 时间格式化
result = simpleDateFormat.format(date);
} finally {
// 释放锁
lock.unlock();
}
// 打印结果
System.out.println(result);
}
});
}
// 任务执行完之后关闭线程池
threadPool.shutdown();
}
}

以上程序的执行结果为:



从上述代码可以看出,手动锁的写法相比于 synchronized 要繁琐一些。

④ 使用ThreadLocal

加锁方案虽然可以正确的解决线程不安全的问题,但同时也引入了新的问题,加锁会让程序进入排队执行的流程,从而一定程度的降低了程序的执行效率,如下图所示:



那有没有一种方案既能解决线程不安全的问题,同时还可以避免排队执行呢?

答案是有的,可以考虑使用 ThreadLocalThreadLocal 翻译为中文是线程本地变量的意思,字如其人 ThreadLocal 就是用来创建线程的私有(本地)变量的,每个线程拥有自己的私有对象,这样就可以避免线程不安全的问题了,实现如下:



知道了实现方案之后,接下来我们使用具体的代码来演示一下 ThreadLocal 的使用,实现代码如下:

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; /**
* ThreadLocal 解决线程不安全问题
*/
public class SimpleDateFormatExample4 {
// 创建 ThreadLocal 对象,并设置默认值(new SimpleDateFormat)
private static ThreadLocal<SimpleDateFormat> threadLocal =
ThreadLocal.withInitial(() -> new SimpleDateFormat("mm:ss")); public static void main(String[] args) {
// 创建线程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);
// 执行 10 次时间格式化
for (int i = 0; i < 10; i++) {
int finalI = i;
// 线程池执行任务
threadPool.execute(new Runnable() {
@Override
public void run() {
// 创建时间对象
Date date = new Date(finalI * 1000);
// 格式化时间
String result = threadLocal.get().format(date);
// 打印结果
System.out.println(result);
}
});
}
// 任务执行完之后关闭线程池
threadPool.shutdown();
}
}

以上程序的执行结果为:

ThreadLocal和局部变量的区别

首先来说 ThreadLocal 不等于局部变量,这里的“局部变量”指的是像 2.1 示例代码中的局部变量, ThreadLocal 和局部变量最大的区别在于:ThreadLocal 属于线程的私有变量,如果使用的是线程池,那么 ThreadLocal 中的变量是可以重复使用的,而代码级别的局部变量,每次执行时都会创建新的局部变量,二者区别如下图所示:



更多关于 ThreadLocal 的内容,可以访问磊哥前面的文章《ThreadLocal不好用?那是你没用对!》

⑤ 使用DateTimeFormatter

以上 4 种解决方案都是因为 SimpleDateFormat 是线程不安全的,所以我们需要加锁或者使用 ThreadLocal 来处理,然而,JDK 8 之后我们就有了新的选择,如果使用的是 JDK 8+ 版本,就可以直接使用 JDK 8 中新增的、安全的时间格式化工具类 DateTimeFormatter 来格式化时间了,接下来我们来具体实现一下。

使用 DateTimeFormatter 必须要配合 JDK 8 中新增的时间对象 LocalDateTime 来使用,因此在操作之前,我们可以先将 Date 对象转换成 LocalDateTime,然后再通过 DateTimeFormatter 来格式化时间,具体实现代码如下:

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; /**
* DateTimeFormatter 解决线程不安全问题
*/
public class SimpleDateFormatExample5 {
// 创建 DateTimeFormatter 对象
private static DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("mm:ss"); public static void main(String[] args) {
// 创建线程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);
// 执行 10 次时间格式化
for (int i = 0; i < 10; i++) {
int finalI = i;
// 线程池执行任务
threadPool.execute(new Runnable() {
@Override
public void run() {
// 创建时间对象
Date date = new Date(finalI * 1000);
// 将 Date 转换成 JDK 8 中的时间类型 LocalDateTime
LocalDateTime localDateTime =
LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
// 时间格式化
String result = dateTimeFormatter.format(localDateTime);
// 打印结果
System.out.println(result);
}
});
}
// 任务执行完之后关闭线程池
threadPool.shutdown();
}
}

以上程序的执行结果为:

3.线程不安全原因分析

要了解 SimpleDateFormat 为什么是线程不安全的?我们需要查看并分析 SimpleDateFormat 的源码才行,那我们先从使用的方法 format 入手,源码如下:

private StringBuffer format(Date date, StringBuffer toAppendTo,
FieldDelegate delegate) {
// 注意此行代码
calendar.setTime(date); boolean useDateFormatSymbols = useDateFormatSymbols(); for (int i = 0; i < compiledPattern.length; ) {
int tag = compiledPattern[i] >>> 8;
int count = compiledPattern[i++] & 0xff;
if (count == 255) {
count = compiledPattern[i++] << 16;
count |= compiledPattern[i++];
} switch (tag) {
case TAG_QUOTE_ASCII_CHAR:
toAppendTo.append((char)count);
break; case TAG_QUOTE_CHARS:
toAppendTo.append(compiledPattern, i, count);
i += count;
break; default:
subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
break;
}
}
return toAppendTo;
}

也许是好运使然,没想到刚开始分析第一个方法就找到了线程不安全的问题所在。

从上述源码可以看出,在执行 SimpleDateFormat.format 方法时,会使用 calendar.setTime 方法将输入的时间进行转换,那么我们想象一下这样的场景:

  1. 线程 1 执行了 calendar.setTime(date) 方法,将用户输入的时间转换成了后面格式化时所需要的时间;
  2. 线程 1 暂停执行,线程 2 得到 CPU 时间片开始执行;
  3. 线程 2 执行了 calendar.setTime(date) 方法,对时间进行了修改;
  4. 线程 2 暂停执行,线程 1 得出 CPU 时间片继续执行,因为线程 1 和线程 2 使用的是同一对象,而时间已经被线程 2 修改了,所以此时当线程 1 继续执行的时候就会出现线程安全的问题了。

正常的情况下,程序的执行是这样的:

非线程安全的执行流程是这样的:



在多线程执行的情况下,线程 1 的 date1 和线程 2 的 date2,因为执行顺序的问题,最终都被格式化成 date2 formatted,而非线程 1 date1 formatted 和线程 2 date2 formatted,这样就会导致线程不安全的问题。

4.各方案优缺点总结

如果使用的是 JDK 8+ 版本,可以直接使用线程安全的 DateTimeFormatter 来进行时间格式化,如果使用的 JDK 8 以下版本或者改造老的 SimpleDateFormat 代码,可以考虑使用 synchronizedThreadLocal 来解决线程不安全的问题。因为实现方案 1 局部变量的解决方案,每次执行的时候都会创建新的对象,因此不推荐使用。synchronized 的实现比较简单,而使用 ThreadLocal 可以避免加锁排队执行的问题。

关注公号「Java中文社群」查看更多有意思、涨知识的并发编程文章。

SimpleDateFormat线程不安全的5种解决方案!的更多相关文章

  1. iOS多线程全套:线程生命周期,多线程的四种解决方案,线程安全问题,GCD的使用,NSOperation的使用

    目的 本文主要是分享iOS多线程的相关内容,为了更系统的讲解,将分为以下7个方面来展开描述. 多线程的基本概念 线程的状态与生命周期 多线程的四种解决方案:pthread,NSThread,GCD,N ...

  2. SimpleDateFormat线程不安全原因及解决方案

    一. 线程不安全验证: /** * SimpleDateFormat线程安全测试 * 〈功能详细描述〉 * * @author 17090889 * @see [相关类/方法](可选) * @sinc ...

  3. Java多线程初学者指南(8):从线程返回数据的两种方法

    从线程中返回数据和向线程传递数据类似.也可以通过类成员以及回调函数来返回数据.但类成员在返回数据和传递数据时有一些区别,下面让我们来看看它们区别在哪. 一.通过类变量和方法返回数据 使用这种方法返回数 ...

  4. 探讨SQL Server并发处理存在就更新七种解决方案

    前言 本节我们来讲讲并发中最常见的情况存在即更新,在并发中若未存在行记录则插入,此时未处理好极容易出现插入重复键情况,本文我们来介绍对并发中存在就更新行记录的七种方案并且我们来综合分析最合适的解决方案 ...

  5. 线程池ThreadPoolExecutor的一种扩展办法

    概述 在JAVA的世界里,如果想并行的执行一些任务,可以使用ThreadPoolExecutor. 大部分情况下直接使用ThreadPoolExecutor就可以满足要求了,但是在某些场景下,比如瞬时 ...

  6. [Winform]线程间操作无效,从不是创建控件的线程访问它的几个解决方案,async和await?

    目录 概述 取消跨线程检查 使用委托异步调用 sync和await 总结 概述 最近在qq群里有一朋友,问起在winform中怎么通过开启线程的方式去处理耗时的操作,比如,查看某个目录下所有的文件,或 ...

  7. SimpleDateFormat线程不安全及解决办法

    原文链接:https://blog.csdn.net/csdn_ds/article/details/72984646 以前没有注意到SimpleDateFormat线程不安全的问题,写时间工具类,一 ...

  8. SimpleDateFormat线程不安全问题解决及替换方法

    场景:在多线程情况下为避免多次创建SimpleDateForma实力占用资源,将SimpleDateForma对象设置为static. 出现错误:SimpleDateFormat定义为静态变量,那么多 ...

  9. SimpleDateFormat线程不安全及解决办法(转)

    以前没有注意到SimpleDateFormat线程不安全的问题,写时间工具类,一般写成静态的成员变量,不知,此种写法的危险性!在此讨论一下SimpleDateFormat线程不安全问题,以及解决方法. ...

随机推荐

  1. 【死磕JVM】一道面试题引发的“栈帧”!!!

    前言 最近小农的朋友--小勇在找工作,开年来金三银四,都想跳一跳,找个踏(gao)实(xin)点的工作,这不小勇也去面试了,不得不说,现在面试,各种底层各种原理,层出不穷,小勇就遇上了这么一道面试题, ...

  2. 给我一个shell我能干翻你内网

    0x00 前言 在去年小菜鸡学了点内网知识就闲着没事跑点jboss的站看看,在经历过很多次内网横向失败之后终于算是人生圆满了一把,阿三的站一般进去之后很难横向,不知道是不是我太菜的原因,反正阿三的站能 ...

  3. OO 第三单元

    一.JML语言理论基础 JML 是用于对 Java 程序进行规格化设计的一种表示语言,为严格的程序设计提供了一套行之有效的方法. 我个人对于 JML 的几点看法: JML 的规格化设计相较于自然语言的 ...

  4. Leedcode算法专题训练(哈希表)

    Java 中的 HashSet 用于存储一个集合,可以查找元素是否在集合中.如果元素有穷,并且范围不大,那么可以用一个布尔数组来存储一个元素是否存在.例如对于只有小写字符的元素,就可以用一个长度为 2 ...

  5. 【笔记】《Redis设计与实现》chapter2 简单动态字符串

    ------------恢复内容开始------------ 2.1 SDS的定义 struct sdshdr{ // 记录buf数组中已使用字节的数量 // 等于SDS所保存字符串的长度(不含'\0 ...

  6. SOAP与REST API的区别

    之前一直想写些RESTful相关的文章,却一直未付诸行动.直到最近接手一个新的项目,对这块重新温习,乘此机会写些相关帖子.欢迎大家评论留言,如有错漏之处,也望各位大佬不令赐教. 话不多说,干活顶上. ...

  7. Jenkins 项目类型及配置项

    0. 简介 1. Freestyle project 1)General 2)源码管理 3)构建触发器 4)构建环境 5)构建 6)构建后操作 2. Maven 项目 1)所有配置项 2)Build ...

  8. 【cypress】4. 丰富的调试工具

    Cypress附带了一系列调试工具来帮助我们弄明白测试的经过,利于我们更好的调试. 具体这些工具的能力都有啥? 回到每个命令的快照. 可以看到特殊的已发生的page events. 接收关于每个命令的 ...

  9. postman传入cookie的三种方式

    1.直接在环境变量里面设置 2.在登录接口的tests,把返回值的cookie设置为全局变量 3.直接在cookie里面传入

  10. hdu4279 找规律+小想法

    题意:      蛋疼的题意,最后是泽神给我讲的题意,题意是对于一个数来说,如果他不能整除另一个数同时他和另一个数非互质,那么另一个数就是这个数的特别数,如10 的特别数有 4 6 8三个,同时题目还 ...