本文将为您提供关于【多线程补充】SimpleDateFormat非线程安全与线程中、线程组中异常的处理的详细介绍,同时,我们还将为您提供关于6种方法帮你搞定SimpleDateFormat类不是线程安
本文将为您提供关于【多线程补充】SimpleDateFormat非线程安全与线程中、线程组中异常的处理的详细介绍,同时,我们还将为您提供关于6种方法帮你搞定SimpleDateFormat类不是线程安全的问题、Java SimpleDateFormat 使用注意:不支持多线程中定义全局的(static)SimpleDateFormat、Java SimpleDateFormat 非线程安全、Java SimpleDateFormat线程安全问题原理详解的实用信息。
本文目录一览:- 【多线程补充】SimpleDateFormat非线程安全与线程中、线程组中异常的处理
- 6种方法帮你搞定SimpleDateFormat类不是线程安全的问题
- Java SimpleDateFormat 使用注意:不支持多线程中定义全局的(static)SimpleDateFormat
- Java SimpleDateFormat 非线程安全
- Java SimpleDateFormat线程安全问题原理详解
【多线程补充】SimpleDateFormat非线程安全与线程中、线程组中异常的处理
1.SimpleDateFormat非线程安全的问题
类SimpleDateFormat主要负责日期的转换与格式化,但在多线程环境中,使用此类容易造成数据转换及处理的不正确,因为SimpleDateFormat类并不是线程安全的。
1.多线程中存在的问题:
package cn.qlq.thread.seventeen;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Demo1 extends Thread {
private static final Logger LOGGER = LoggerFactory.getLogger(Demo1.class);
private SimpleDateFormat simpleDateFormat;
private String dateStr;
public Demo1(SimpleDateFormat simpleDateFormat, String dateStr) {
super();
this.simpleDateFormat = simpleDateFormat;
this.dateStr = dateStr;
}
@Override
public void run() {
try {
Date parse = simpleDateFormat.parse(dateStr);
String format = simpleDateFormat.format(parse).toString();
LOGGER.info("threadName ->{} ,dateStr ->{},格式化后的->{}", Thread.currentThread().getName(), dateStr, format);
} catch (ParseException e) {
LOGGER.error("parseException", e);
}
}
public static void main(String[] args) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
String[] dateStrs = new String[] { "2018-01-01", "2018-01-03", "2018-01-03" };
Thread[] threads = new Thread[3];
for (int i = 0; i < 3; i++) {
threads[i] = new Demo1(simpleDateFormat, dateStrs[i]);
}
for (int i = 0; i < 3; i++) {
threads[i].start();
}
}
}
结果:
10:07:27 [cn.qlq.thread.seventeen.Demo1]-[INFO] threadName ->Thread-0 ,dateStr ->2018-01-01,格式化后的->2001-01-01
10:07:27 [cn.qlq.thread.seventeen.Demo1]-[INFO] threadName ->Thread-1 ,dateStr ->2018-01-03,格式化后的->2001-01-03
10:07:27 [cn.qlq.thread.seventeen.Demo1]-[INFO] threadName ->Thread-2 ,dateStr ->2018-01-03,格式化后的->2001-01-01
多次执行发现结果不固定,有时候报错如下:
Exception in thread "Thread-1" Exception in thread "Thread-0" java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1110)
at java.lang.Double.parseDouble(Double.java:540)
at java.text.DigitList.getDouble(DigitList.java:168)
at java.text.DecimalFormat.parse(DecimalFormat.java:1321)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1793)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1455)
at java.text.DateFormat.parse(DateFormat.java:355)
at cn.qlq.thread.seventeen.Demo1.run(Demo1.java:24)
java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1110)
at java.lang.Double.parseDouble(Double.java:540)
at java.text.DigitList.getDouble(DigitList.java:168)
at java.text.DecimalFormat.parse(DecimalFormat.java:1321)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1793)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1455)
at java.text.DateFormat.parse(DateFormat.java:355)
at cn.qlq.thread.seventeen.Demo1.run(Demo1.java:24)
10:08:20 [cn.qlq.thread.seventeen.Demo1]-[INFO] threadName ->Thread-2 ,dateStr ->2018-01-03,格式化后的->2200-01-03
因为上面多个线程使用了同一个SimpleDateFormat实例,所以在多线程环境下使用此类是不安全的。
补充:SimpleDateFormat线程非安全的原因:
SimpleDateFormat内部继承了一个DateFormat的calendar成员变量,所以多个线程共用同一个calendar,所以容易造成线程非安全。
public class SimpleDateFormat extends DateFormat {
// Called from Format after creating a FieldDelegate
private StringBuffer format(Date date, StringBuffer toAppendTo,
FieldDelegate delegate) {
// Convert input date to time field list
calendar.setTime(date);
...
}
}
public abstract class DateFormat extends Format {
protected Calendar calendar;
}
补充:再次查看apache的commons-lang包的工具类:DateFormatUtils
其方法内部新建了一个局部变量,并进行格式化,因此不存在线程非安全的问题
public static String format(final Date date, final String pattern, final TimeZone timeZone, final Locale locale) {
final FastDateFormat df = FastDateFormat.getInstance(pattern, timeZone, locale);
return df.format(date);
}
public String format(final Date date) {
final Calendar c = newCalendar(); // hard code GregorianCalendar
c.setTime(date);
return applyRulesToString(c);
}
2.解决办法
1.每个线程使用一个SimpleDateFormat
修改main方法中的代码如下:(每个线程使用一个SimpleDateFormat)
String[] dateStrs = new String[] { "2018-01-01", "2018-01-03", "2018-01-03" };
Thread[] threads = new Thread[3];
for (int i = 0; i < 3; i++) {
threads[i] = new Demo2(new SimpleDateFormat("yyyy-MM-dd"), dateStrs[i]);
}
for (int i = 0; i < 3; i++) {
threads[i].start();
}
2.使用ThreadLocal解决上面问题
在前面学习过ThreadLocal的作用,可以实现多线程之间的隔离。
package cn.qlq.thread.seventeen;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Demo2 extends Thread {
private static final Logger LOGGER = LoggerFactory.getLogger(Demo2.class);
private static final ThreadLocal<SimpleDateFormat> THREAD_LOCAL = new ThreadLocal<SimpleDateFormat>();
public static SimpleDateFormat getSimpleDateFormat() {
SimpleDateFormat simpleDateFormat2 = THREAD_LOCAL.get();
if (simpleDateFormat2 == null) {
simpleDateFormat2 = new SimpleDateFormat("yyyy-MM-dd");
THREAD_LOCAL.set(simpleDateFormat2);
}
return simpleDateFormat2;
}
private SimpleDateFormat simpleDateFormat;
private String dateStr;
public Demo2(String dateStr) {
super();
this.dateStr = dateStr;
}
@Override
public void run() {
try {
// 从ThreadLocal中获取simpleDateFormat
this.simpleDateFormat = Demo2.getSimpleDateFormat();
Date parse = simpleDateFormat.parse(dateStr);
String format = simpleDateFormat.format(parse).toString();
LOGGER.info("threadName ->{} ,dateStr ->{},格式化后的->{}", Thread.currentThread().getName(), dateStr, format);
} catch (ParseException e) {
LOGGER.error("parseException", e);
}
}
public static void main(String[] args) {
String[] dateStrs = new String[] { "2018-01-01", "2018-01-03", "2018-01-03" };
Thread[] threads = new Thread[3];
for (int i = 0; i < 3; i++) {
threads[i] = new Demo2(dateStrs[i]);
}
for (int i = 0; i < 3; i++) {
threads[i].start();
}
}
}
结果:
10:37:53 [cn.qlq.thread.seventeen.Demo2]-[INFO] threadName ->Thread-1 ,dateStr ->2018-01-03,格式化后的->2018-01-03
10:37:53 [cn.qlq.thread.seventeen.Demo2]-[INFO] threadName ->Thread-0 ,dateStr ->2018-01-01,格式化后的->2018-01-01
10:37:53 [cn.qlq.thread.seventeen.Demo2]-[INFO] threadName ->Thread-2 ,dateStr ->2018-01-03,格式化后的->2018-01-03
2.线程中出现异常的处理
线程中也容易出现异常。在多线程中也可以对多线程中的异常进行捕捉,使用的是UncaughtExceptionHandler类,从而可以对发生的异常进行有效的处理。
package cn.qlq.thread.seventeen;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Demo3 extends Thread {
private static final Logger LOGGER = LoggerFactory.getLogger(Demo3.class);
@Override
public void run() {
int i = 1 / 0;
}
public static void main(String[] args) {
new Demo3().start();
}
}
结果:
Exception in thread "Thread-0" java.lang.ArithmeticException: / by zero
at cn.qlq.thread.seventeen.Demo3.run(Demo3.java:12)
使用UncaughtExceptionHandler捕捉多线程中的异常。查看线程类Thread的源码,发现其有两个异常处理器,一个是所有线程共享的默认静态异常处理器、另一个是线程独有的成员属性。
// null unless explicitly set
private volatile UncaughtExceptionHandler uncaughtExceptionHandler;
// null unless explicitly set
private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler;
public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(
new RuntimePermission("setDefaultUncaughtExceptionHandler")
);
}
defaultUncaughtExceptionHandler = eh;
}
public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
checkAccess();
uncaughtExceptionHandler = eh;
}
(1)多所有线程设置默认的默认异常处理器
package cn.qlq.thread.seventeen;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Demo5 extends Thread {
private static final Logger LOGGER = LoggerFactory.getLogger(Demo5.class);
@Override
public void run() {
int i = 1 / 0;
}
public static void main(String[] args) {
Demo5.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
LOGGER.error("所有Demo5线程默认的异常处理器, threadName -> {}", t.getName(), e);
}
});
Demo5 demo5 = new Demo5();
demo5.start();
}
}
结果:
14:12:54 [cn.qlq.thread.seventeen.Demo5]-[ERROR] 所有Demo5线程默认的异常处理器, threadName -> Thread-0
java.lang.ArithmeticException: / by zero
at cn.qlq.thread.seventeen.Demo5.run(Demo5.java:12)
(2)对单个线程设置异常处理器
package cn.qlq.thread.seventeen;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Demo5 extends Thread {
private static final Logger LOGGER = LoggerFactory.getLogger(Demo5.class);
@Override
public void run() {
int i = 1 / 0;
}
public static void main(String[] args) {
Demo5 demo5 = new Demo5();
// 设置异常捕捉器
demo5.setUncaughtExceptionHandler(new UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
LOGGER.error("单独给线程设置的, threadName -> {}", t.getName(), e);
}
});
demo5.start();
}
}
结果:
14:15:00 [cn.qlq.thread.seventeen.Demo5]-[ERROR] 单独给线程设置的, threadName -> Thread-0
java.lang.ArithmeticException: / by zero
at cn.qlq.thread.seventeen.Demo5.run(Demo5.java:12)
(3)如果都设置查看默认的生效的异常处理器---是单独设置的异常处理器uncaughtExceptionHandler发挥作用
package cn.qlq.thread.seventeen;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Demo5 extends Thread {
private static final Logger LOGGER = LoggerFactory.getLogger(Demo5.class);
@Override
public void run() {
int i = 1 / 0;
}
public static void main(String[] args) {
// 设置全局的
Demo5.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
LOGGER.error("所有Demo5线程默认的异常处理器, threadName -> {}", t.getName(), e);
}
});
Demo5 demo5 = new Demo5();
// 设置单独的
demo5.setUncaughtExceptionHandler(new UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
LOGGER.error("单独给线程设置的, threadName -> {}", t.getName(), e);
}
});
demo5.start();
}
}
结果:(生效的是线程单独设置的异常处理器)
14:19:25 [cn.qlq.thread.seventeen.Demo5]-[ERROR] 单独给线程设置的, threadName -> Thread-0
java.lang.ArithmeticException: / by zero
at cn.qlq.thread.seventeen.Demo5.run(Demo5.java:12)
3.线程组出现异常的处理
3.1线程组内出现异常
package cn.qlq.thread.seventeen;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Demo3 extends Thread {
private static final Logger LOGGER = LoggerFactory.getLogger(Demo3.class);
private Integer num;
public Demo3(Integer num, ThreadGroup threadGroup) {
super(threadGroup, num + "" + Math.random());
this.num = num;
}
@Override
public void run() {
int i = 1 / num;
while (true) {
try {
Thread.sleep(1 * 1000);
LOGGER.info("num = {}", num++);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ThreadGroup threadGroup = new ThreadGroup("myGroup");
Thread t1 = new Demo3(0, threadGroup);
Thread t2 = new Demo3(0, threadGroup);
Thread t3 = new Demo3(1, threadGroup);
t1.start();
t2.start();
t3.start();
}
}
结果:(t1,t2由于异常停止,t3仍然在执行while循环)。
查看线程信息
Administrator@MicroWin10-1535 MINGW64 /e/xiangmu/ThreadStudy (master)
$ jstack 2712
2019-01-03 12:00:26
Full thread dump Java HotSpot(TM) 64-Bit Server VM (24.80-b11 mixed mode):
"DestroyJavaVM" prio=6 tid=0x000000000c8f5000 nid=0xbc8 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"10.0050164194939836815" prio=6 tid=0x000000000c8f6800 nid=0x54d4 waiting on condition [0x000000000ce4f000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at cn.qlq.thread.seventeen.Demo3.run(Demo3.java:21)
"Service Thread" daemon prio=6 tid=0x000000000ae14000 nid=0x31d0 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread1" daemon prio=10 tid=0x000000000adeb800 nid=0x754 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread0" daemon prio=10 tid=0x000000000adea000 nid=0x22e4 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Attach Listener" daemon prio=10 tid=0x000000000ade8800 nid=0x51c waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Signal Dispatcher" daemon prio=10 tid=0x000000000ae01000 nid=0x2914 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Finalizer" daemon prio=8 tid=0x000000000adae800 nid=0x1ef8 in Object.wait() [0x000000000c14f000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000007d6204858> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:135)
- locked <0x00000007d6204858> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:151)
at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209)
"Reference Handler" daemon prio=10 tid=0x000000000ada5800 nid=0x3820 in Object.wait() [0x000000000c04f000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000007d6204470> (a java.lang.ref.Reference$Lock)
at java.lang.Object.wait(Object.java:503)
at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:133)
- locked <0x00000007d6204470> (a java.lang.ref.Reference$Lock)
"VM Thread" prio=10 tid=0x000000000ada1800 nid=0x2f54 runnable
"GC task thread#0 (ParallelGC)" prio=6 tid=0x0000000002a76800 nid=0x4d04 runnable
"GC task thread#1 (ParallelGC)" prio=6 tid=0x0000000002a78000 nid=0x1bbc runnable
"GC task thread#2 (ParallelGC)" prio=6 tid=0x0000000002a7b000 nid=0x11a0 runnable
"GC task thread#3 (ParallelGC)" prio=6 tid=0x0000000002a7c800 nid=0x4b80 runnable
"VM Periodic Task Thread" prio=10 tid=0x000000000ae1e800 nid=0x5a4 waiting on condition
JNI global references: 184
3.2 线程组内处理异常
从上面的运行结果也可以看出线程组中的一个线程出现异常不会影响其它线程的运行。
1.线程组统一对异常进行处理,记录异常
继承ThreadGroup并且重写uncaughtException方法。
package cn.qlq.thread.seventeen;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Demo3 extends Thread {
private static final Logger LOGGER = LoggerFactory.getLogger(Demo3.class);
private Integer num;
public Demo3(Integer num, ThreadGroup threadGroup) {
super(threadGroup, num + "" + Math.random());
this.num = num;
}
@Override
public void run() {
int i = 1 / num;
while (true) {
try {
Thread.sleep(1 * 1000);
LOGGER.info("num = {}", num++);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
MyThreadGroup threadGroup = new MyThreadGroup(new ThreadGroup("myGroup"));
Thread t1 = new Demo3(0, threadGroup);
Thread t2 = new Demo3(0, threadGroup);
Thread t3 = new Demo3(1, threadGroup);
t1.start();
t2.start();
t3.start();
}
}
class MyThreadGroup extends ThreadGroup {
private static final Logger LOGGER = LoggerFactory.getLogger(MyThreadGroup.class);
public MyThreadGroup(ThreadGroup threadGroup) {
super(threadGroup, threadGroup.getName());
}
@Override
public void uncaughtException(Thread t, Throwable e) {
LOGGER.error("UncaughtException , threadName -> {},ThreadGroupName -> {}", t.getName(),
t.getThreadGroup().getName(), e);
}
}
结果:
2.线程组处理异常,一个线程异常停止组内所有异常
package cn.qlq.thread.seventeen;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Demo3 extends Thread {
private static final Logger LOGGER = LoggerFactory.getLogger(Demo3.class);
private Integer num;
public Demo3(Integer num, ThreadGroup threadGroup) {
super(threadGroup, num + "" + Math.random());
this.num = num;
}
@Override
public void run() {
int i = 1 / num;
while (!this.isInterrupted()) {
LOGGER.info("num = {}", num++);
}
}
public static void main(String[] args) {
MyThreadGroup threadGroup = new MyThreadGroup(new ThreadGroup("myGroup"));
Thread t1 = new Demo3(0, threadGroup);
Thread t2 = new Demo3(0, threadGroup);
Thread t3 = new Demo3(1, threadGroup);
t1.start();
t2.start();
t3.start();
}
}
class MyThreadGroup extends ThreadGroup {
private static final Logger LOGGER = LoggerFactory.getLogger(MyThreadGroup.class);
public MyThreadGroup(ThreadGroup threadGroup) {
super(threadGroup, threadGroup.getName());
}
@Override
public void uncaughtException(Thread t, Throwable e) {
LOGGER.error("UncaughtException , threadName -> {},ThreadGroupName -> {},向组发出中断信号", t.getName(),
t.getThreadGroup().getName(), e);
this.interrupt();
}
}
结果:
12:14:56 [cn.qlq.thread.seventeen.Demo3]-[INFO] num = 1
12:14:56 [cn.qlq.thread.seventeen.MyThreadGroup]-[ERROR] UncaughtException , threadName -> 00.03911857279295183,ThreadGroupName -> myGroup,向组发出中断信号
java.lang.ArithmeticException: / by zero
at cn.qlq.thread.seventeen.Demo3.run(Demo3.java:18)
12:14:56 [cn.qlq.thread.seventeen.MyThreadGroup]-[ERROR] UncaughtException , threadName -> 00.9116153677767221,ThreadGroupName -> myGroup,向组发出中断信号
java.lang.ArithmeticException: / by zero
at cn.qlq.thread.seventeen.Demo3.run(Demo3.java:18)
12:14:56 [cn.qlq.thread.seventeen.Demo3]-[INFO] num = 2
4.线程异常处理的传递
前面介绍了多种线程的处理方式,如果将多个异常处理方式放在一起运行,如果线程设置了异常处理首先生效的是线程的异常处理器;如果线程没有设置,生效的是线程组的异常处理器。
package cn.qlq.thread.seventeen;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Demo4 extends Thread {
private static final Logger LOGGER = LoggerFactory.getLogger(Demo4.class);
private Integer num;
public Demo4(Integer num, ThreadGroup threadGroup) {
super(threadGroup, num + "" + Math.random());
this.num = num;
}
@Override
public void run() {
int i = 1 / num;
while (!this.isInterrupted()) {
LOGGER.info("num = {}", num++);
}
}
public static void main(String[] args) {
MyThreadGroup2 threadGroup = new MyThreadGroup2(new ThreadGroup("myGroup"));
Thread t1 = new Demo4(0, threadGroup);
t1.setUncaughtExceptionHandler(new UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
LOGGER.error("线程中报错,threadName->{}", t.getName(), e);
}
});
t1.start();
Thread t2 = new Demo4(0, threadGroup);
t2.start();
}
}
class MyThreadGroup2 extends ThreadGroup {
private static final Logger LOGGER = LoggerFactory.getLogger(MyThreadGroup2.class);
public MyThreadGroup2(ThreadGroup threadGroup) {
super(threadGroup, threadGroup.getName());
}
@Override
public void uncaughtException(Thread t, Throwable e) {
LOGGER.error("UncaughtException , threadName -> {},ThreadGroupName -> {}", t.getName(),
t.getThreadGroup().getName(), e);
}
}
结果:(t1被线程处理器捕捉,t2被线程组处理器捕捉)
13:51:07 [cn.qlq.thread.seventeen.Demo4]-[ERROR] 线程中报错,threadName->00.8052783699913576
java.lang.ArithmeticException: / by zero
at cn.qlq.thread.seventeen.Demo4.run(Demo4.java:18)
13:51:07 [cn.qlq.thread.seventeen.MyThreadGroup2]-[ERROR] UncaughtException , threadName -> 00.6447075404863479,ThreadGroupName -> myGroup
java.lang.ArithmeticException: / by zero
at cn.qlq.thread.seventeen.Demo4.run(Demo4.java:18)
如果在组中调用原来父类的方法会继续在console中打印错误日志:
class MyThreadGroup2 extends ThreadGroup {
private static final Logger LOGGER = LoggerFactory.getLogger(MyThreadGroup2.class);
public MyThreadGroup2(ThreadGroup threadGroup) {
super(threadGroup, threadGroup.getName());
}
@Override
public void uncaughtException(Thread t, Throwable e) {
LOGGER.error("UncaughtException , threadName -> {},ThreadGroupName -> {}", t.getName(),
t.getThreadGroup().getName(), e);
super.uncaughtException(t, e);
}
}
结果:
6种方法帮你搞定SimpleDateFormat类不是线程安全的问题
摘要:本文主要讲述在高并发下SimpleDateFormat类为何会出现安全问题,以及如何解决SimpleDateFormat类的安全问题。
本文分享自华为云社区《【高并发】SimpleDateFormat类到底为啥不是线程安全的?》,作者:冰 河 。
首先问下大家:你使用的SimpleDateFormat类还安全吗?为什么说SimpleDateFormat类不是线程安全的?带着问题从本文中寻求答案。
提起SimpleDateFormat类,想必做过Java开发的童鞋都不会感到陌生。没错,它就是Java中提供的日期时间的转化类。这里,为什么说SimpleDateFormat类有线程安全问题呢?有些小伙伴可能会提出疑问:我们生产环境上一直在使用SimpleDateFormat类来解析和格式化日期和时间类型的数据,一直都没有问题啊!我的回答是:没错,那是因为你们的系统达不到SimpleDateFormat类出现问题的并发量,也就是说你们的系统没啥负载!
接下来,我们就一起看下在高并发下SimpleDateFormat类为何会出现安全问题,以及如何解决SimpleDateFormat类的安全问题。
重现SimpleDateFormat类的线程安全问题
为了重现SimpleDateFormat类的线程安全问题,一种比较简单的方式就是使用线程池结合Java并发包中的CountDownLatch类和Semaphore类来重现线程安全问题。
有关CountDownLatch类和Semaphore类的具体用法和底层原理与源码解析在【高并发专题】后文会深度分析。这里,大家只需要知道CountDownLatch类可以使一个线程等待其他线程各自执行完毕后再执行。而Semaphore类可以理解为一个计数信号量,必须由获取它的线程释放,经常用来限制访问某些资源的线程数量,例如限流等。
好了,先来看下重现SimpleDateFormat类的线程安全问题的代码,如下所示。
package io.binghe.concurrent.lab06;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
/**
* @author binghe
* @version 1.0.0
* @description 测试SimpleDateFormat的线程不安全问题
*/
public class SimpleDateFormatTest01 {
//执行总次数
private static final int EXECUTE_COUNT = 1000;
//同时运行的线程数量
private static final int THREAD_COUNT = 20;
//SimpleDateFormat对象
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
public static void main(String[] args) throws InterruptedException {
final Semaphore semaphore = new Semaphore(THREAD_COUNT);
final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < EXECUTE_COUNT; i++){
executorService.execute(() -> {
try {
semaphore.acquire();
try {
simpleDateFormat.parse("2020-01-01");
} catch (ParseException e) {
System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
e.printStackTrace();
System.exit(1);
}catch (NumberFormatException e){
System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
e.printStackTrace();
System.exit(1);
}
semaphore.release();
} catch (InterruptedException e) {
System.out.println("信号量发生错误");
e.printStackTrace();
System.exit(1);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println("所有线程格式化日期成功");
}
}
可以看到,在SimpleDateFormatTest01类中,首先定义了两个常量,一个是程序执行的总次数,一个是同时运行的线程数量。程序中结合线程池和CountDownLatch类与Semaphore类来模拟高并发的业务场景。其中,有关日期转化的代码只有如下一行。
simpleDateFormat.parse("2020-01-01");
当程序捕获到异常时,打印相关的信息,并退出整个程序的运行。当程序正确运行后,会打印“所有线程格式化日期成功”。
运行程序输出的结果信息如下所示。
Exception in thread "pool-1-thread-4" Exception in thread "pool-1-thread-1" Exception in thread "pool-1-thread-2" 线程:pool-1-thread-7 格式化日期失败
线程:pool-1-thread-9 格式化日期失败
线程:pool-1-thread-10 格式化日期失败
Exception in thread "pool-1-thread-3" Exception in thread "pool-1-thread-5" Exception in thread "pool-1-thread-6" 线程:pool-1-thread-15 格式化日期失败
线程:pool-1-thread-21 格式化日期失败
Exception in thread "pool-1-thread-23" 线程:pool-1-thread-16 格式化日期失败
线程:pool-1-thread-11 格式化日期失败
java.lang.ArrayIndexOutOfBoundsException
线程:pool-1-thread-27 格式化日期失败
at java.lang.System.arraycopy(Native Method)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:597)
at java.lang.StringBuffer.append(StringBuffer.java:367)
at java.text.DigitList.getLong(DigitList.java:191)线程:pool-1-thread-25 格式化日期失败
at java.text.DecimalFormat.parse(DecimalFormat.java:2084)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
线程:pool-1-thread-14 格式化日期失败
at java.text.DateFormat.parse(DateFormat.java:364)
at io.binghe.concurrent.lab06.SimpleDateFormatTest01.lambda$main$0(SimpleDateFormatTest01.java:47)
线程:pool-1-thread-13 格式化日期失败at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
java.lang.NumberFormatException: For input string: ""
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
线程:pool-1-thread-20 格式化日期失败at java.lang.Long.parseLong(Long.java:601)
at java.lang.Long.parseLong(Long.java:631)
at java.text.DigitList.getLong(DigitList.java:195)
at java.text.DecimalFormat.parse(DecimalFormat.java:2084)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at io.binghe.concurrent.lab06.SimpleDateFormatTest01.lambda$main$0(SimpleDateFormatTest01.java:47)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
java.lang.NumberFormatException: For input string: ""
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:601)
at java.lang.Long.parseLong(Long.java:631)
at java.text.DigitList.getLong(DigitList.java:195)
at java.text.DecimalFormat.parse(DecimalFormat.java:2084)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
Process finished with exit code 1
说明,在高并发下使用SimpleDateFormat类格式化日期时抛出了异常,SimpleDateFormat类不是线程安全的!!!
接下来,我们就看下,SimpleDateFormat类为何不是线程安全的。
SimpleDateFormat类为何不是线程安全的?
那么,接下来,我们就一起来看看真正引起SimpleDateFormat类线程不安全的根本原因。
通过查看SimpleDateFormat类的源码,我们得知:SimpleDateFormat是继承自DateFormat类,DateFormat类中维护了一个全局的Calendar变量,如下所示。
/**
* The {@link Calendar} instance used for calculating the date-time fields
* and the instant of time. This field is used for both formatting and
* parsing.
*
* <p>Subclasses should initialize this field to a {@link Calendar}
* appropriate for the {@link Locale} associated with this
* <code>DateFormat</code>.
* @serial
*/
protected Calendar calendar;
从注释可以看出,这个Calendar对象既用于格式化也用于解析日期时间。接下来,我们再查看parse()方法接近最后的部分。
@Override
public Date parse(String text, ParsePosition pos){
################此处省略N行代码##################
Date parsedDate;
try {
parsedDate = calb.establish(calendar).getTime();
// If the year value is ambiguous,
// then the two-digit year == the default start year
if (ambiguousYear[0]) {
if (parsedDate.before(defaultCenturyStart)) {
parsedDate = calb.addYear(100).establish(calendar).getTime();
}
}
}
// An IllegalArgumentException will be thrown by Calendar.getTime()
// if any fields are out of range, e.g., MONTH == 17.
catch (IllegalArgumentException e) {
pos.errorIndex = start;
pos.index = oldStart;
return null;
}
return parsedDate;
}
可见,最后的返回值是通过调用CalendarBuilder.establish()方法获得的,而这个方法的参数正好就是前面的Calendar对象。
接下来,我们再来看看CalendarBuilder.establish()方法,如下所示。
Calendar establish(Calendar cal) {
boolean weekDate = isSet(WEEK_YEAR)
&& field[WEEK_YEAR] > field[YEAR];
if (weekDate && !cal.isWeekDateSupported()) {
// Use YEAR instead
if (!isSet(YEAR)) {
set(YEAR, field[MAX_FIELD + WEEK_YEAR]);
}
weekDate = false;
}
cal.clear();
// Set the fields from the min stamp to the max stamp so that
// the field resolution works in the Calendar.
for (int stamp = MINIMUM_USER_STAMP; stamp < nextStamp; stamp++) {
for (int index = 0; index <= maxFieldIndex; index++) {
if (field[index] == stamp) {
cal.set(index, field[MAX_FIELD + index]);
break;
}
}
}
if (weekDate) {
int weekOfYear = isSet(WEEK_OF_YEAR) ? field[MAX_FIELD + WEEK_OF_YEAR] : 1;
int dayOfWeek = isSet(DAY_OF_WEEK) ?
field[MAX_FIELD + DAY_OF_WEEK] : cal.getFirstDayOfWeek();
if (!isValidDayOfWeek(dayOfWeek) && cal.isLenient()) {
if (dayOfWeek >= 8) {
dayOfWeek--;
weekOfYear += dayOfWeek / 7;
dayOfWeek = (dayOfWeek % 7) + 1;
} else {
while (dayOfWeek <= 0) {
dayOfWeek += 7;
weekOfYear--;
}
}
dayOfWeek = toCalendarDayOfWeek(dayOfWeek);
}
cal.setWeekDate(field[MAX_FIELD + WEEK_YEAR], weekOfYear, dayOfWeek);
}
return cal;
}
在CalendarBuilder.establish()方法中先后调用了cal.clear()与cal.set(),也就是先清除cal对象中设置的值,再重新设置新的值。由于Calendar内部并没有线程安全机制,并且这两个操作也都不是原子性的,所以当多个线程同时操作一个SimpleDateFormat时就会引起cal的值混乱。类似地, format()方法也存在同样的问题。
因此, SimpleDateFormat类不是线程安全的根本原因是:DateFormat类中的Calendar对象被多线程共享,而Calendar对象本身不支持线程安全。
那么,得知了SimpleDateFormat类不是线程安全的,以及造成SimpleDateFormat类不是线程安全的原因,那么如何解决这个问题呢?接下来,我们就一起探讨下如何解决SimpleDateFormat类在高并发场景下的线程安全问题。
解决SimpleDateFormat类的线程安全问题
解决SimpleDateFormat类在高并发场景下的线程安全问题可以有多种方式,这里,就列举几个常用的方式供参考,大家也可以在评论区给出更多的解决方案。
1.局部变量法
最简单的一种方式就是将SimpleDateFormat类对象定义成局部变量,如下所示的代码,将SimpleDateFormat类对象定义在parse(String)方法的上面,即可解决问题。
package io.binghe.concurrent.lab06;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
/**
* @author binghe
* @version 1.0.0
* @description 局部变量法解决SimpleDateFormat类的线程安全问题
*/
public class SimpleDateFormatTest02 {
//执行总次数
private static final int EXECUTE_COUNT = 1000;
//同时运行的线程数量
private static final int THREAD_COUNT = 20;
public static void main(String[] args) throws InterruptedException {
final Semaphore semaphore = new Semaphore(THREAD_COUNT);
final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < EXECUTE_COUNT; i++){
executorService.execute(() -> {
try {
semaphore.acquire();
try {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
simpleDateFormat.parse("2020-01-01");
} catch (ParseException e) {
System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
e.printStackTrace();
System.exit(1);
}catch (NumberFormatException e){
System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
e.printStackTrace();
System.exit(1);
}
semaphore.release();
} catch (InterruptedException e) {
System.out.println("信号量发生错误");
e.printStackTrace();
System.exit(1);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println("所有线程格式化日期成功");
}
}
此时运行修改后的程序,输出结果如下所示。
所有线程格式化日期成功
至于在高并发场景下使用局部变量为何能解决线程的安全问题,会在【JVM专题】的JVM内存模式相关内容中深入剖析,这里不做过多的介绍了。
当然,这种方式在高并发下会创建大量的SimpleDateFormat类对象,影响程序的性能,所以,这种方式在实际生产环境不太被推荐。
2.synchronized锁方式
将SimpleDateFormat类对象定义成全局静态变量,此时所有线程共享SimpleDateFormat类对象,此时在调用格式化时间的方法时,对SimpleDateFormat对象进行同步即可,代码如下所示。
package io.binghe.concurrent.lab06;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
/**
* @author binghe
* @version 1.0.0
* @description 通过Synchronized锁解决SimpleDateFormat类的线程安全问题
*/
public class SimpleDateFormatTest03 {
//执行总次数
private static final int EXECUTE_COUNT = 1000;
//同时运行的线程数量
private static final int THREAD_COUNT = 20;
//SimpleDateFormat对象
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
public static void main(String[] args) throws InterruptedException {
final Semaphore semaphore = new Semaphore(THREAD_COUNT);
final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < EXECUTE_COUNT; i++){
executorService.execute(() -> {
try {
semaphore.acquire();
try {
synchronized (simpleDateFormat){
simpleDateFormat.parse("2020-01-01");
}
} catch (ParseException e) {
System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
e.printStackTrace();
System.exit(1);
}catch (NumberFormatException e){
System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
e.printStackTrace();
System.exit(1);
}
semaphore.release();
} catch (InterruptedException e) {
System.out.println("信号量发生错误");
e.printStackTrace();
System.exit(1);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println("所有线程格式化日期成功");
}
}
此时,解决问题的关键代码如下所示。
synchronized (simpleDateFormat){
simpleDateFormat.parse("2020-01-01");
}
运行程序,输出结果如下所示。
所有线程格式化日期成功
需要注意的是,虽然这种方式能够解决SimpleDateFormat类的线程安全问题,但是由于在程序的执行过程中,为SimpleDateFormat类对象加上了synchronized锁,导致同一时刻只能有一个线程执行parse(String)方法。此时,会影响程序的执行性能,在要求高并发的生产环境下,此种方式也是不太推荐使用的。
3.Lock锁方式
Lock锁方式与synchronized锁方式实现原理相同,都是在高并发下通过JVM的锁机制来保证程序的线程安全。通过Lock锁方式解决问题的代码如下所示。
package io.binghe.concurrent.lab06;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author binghe
* @version 1.0.0
* @description 通过Lock锁解决SimpleDateFormat类的线程安全问题
*/
public class SimpleDateFormatTest04 {
//执行总次数
private static final int EXECUTE_COUNT = 1000;
//同时运行的线程数量
private static final int THREAD_COUNT = 20;
//SimpleDateFormat对象
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
//Lock对象
private static Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
final Semaphore semaphore = new Semaphore(THREAD_COUNT);
final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < EXECUTE_COUNT; i++){
executorService.execute(() -> {
try {
semaphore.acquire();
try {
lock.lock();
simpleDateFormat.parse("2020-01-01");
} catch (ParseException e) {
System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
e.printStackTrace();
System.exit(1);
}catch (NumberFormatException e){
System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
e.printStackTrace();
System.exit(1);
}finally {
lock.unlock();
}
semaphore.release();
} catch (InterruptedException e) {
System.out.println("信号量发生错误");
e.printStackTrace();
System.exit(1);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println("所有线程格式化日期成功");
}
}
通过代码可以得知,首先,定义了一个Lock类型的全局静态变量作为加锁和释放锁的句柄。然后在simpleDateFormat.parse(String)代码之前通过lock.lock()加锁。这里需要注意的一点是:为防止程序抛出异常而导致锁不能被释放,一定要将释放锁的操作放到finally代码块中,如下所示。
finally {
lock.unlock();
}
运行程序,输出结果如下所示。
所有线程格式化日期成功
此种方式同样会影响高并发场景下的性能,不太建议在高并发的生产环境使用。
4.ThreadLocal方式
使用ThreadLocal存储每个线程拥有的SimpleDateFormat对象的副本,能够有效的避免多线程造成的线程安全问题,使用ThreadLocal解决线程安全问题的代码如下所示。
package io.binghe.concurrent.lab06;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
/**
* @author binghe
* @version 1.0.0
* @description 通过ThreadLocal解决SimpleDateFormat类的线程安全问题
*/
public class SimpleDateFormatTest05 {
//执行总次数
private static final int EXECUTE_COUNT = 1000;
//同时运行的线程数量
private static final int THREAD_COUNT = 20;
private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>(){
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
public static void main(String[] args) throws InterruptedException {
final Semaphore semaphore = new Semaphore(THREAD_COUNT);
final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < EXECUTE_COUNT; i++){
executorService.execute(() -> {
try {
semaphore.acquire();
try {
threadLocal.get().parse("2020-01-01");
} catch (ParseException e) {
System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
e.printStackTrace();
System.exit(1);
}catch (NumberFormatException e){
System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
e.printStackTrace();
System.exit(1);
}
semaphore.release();
} catch (InterruptedException e) {
System.out.println("信号量发生错误");
e.printStackTrace();
System.exit(1);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println("所有线程格式化日期成功");
}
}
通过代码可以得知,将每个线程使用的SimpleDateFormat副本保存在ThreadLocal中,各个线程在使用时互不干扰,从而解决了线程安全问题。
运行程序,输出结果如下所示。
所有线程格式化日期成功
此种方式运行效率比较高,推荐在高并发业务场景的生产环境使用。
另外,使用ThreadLocal也可以写成如下形式的代码,效果是一样的。
package io.binghe.concurrent.lab06;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
/**
* @author binghe
* @version 1.0.0
* @description 通过ThreadLocal解决SimpleDateFormat类的线程安全问题
*/
public class SimpleDateFormatTest06 {
//执行总次数
private static final int EXECUTE_COUNT = 1000;
//同时运行的线程数量
private static final int THREAD_COUNT = 20;
private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>();
private static DateFormat getDateFormat(){
DateFormat dateFormat = threadLocal.get();
if(dateFormat == null){
dateFormat = new SimpleDateFormat("yyyy-MM-dd");
threadLocal.set(dateFormat);
}
return dateFormat;
}
public static void main(String[] args) throws InterruptedException {
final Semaphore semaphore = new Semaphore(THREAD_COUNT);
final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < EXECUTE_COUNT; i++){
executorService.execute(() -> {
try {
semaphore.acquire();
try {
getDateFormat().parse("2020-01-01");
} catch (ParseException e) {
System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
e.printStackTrace();
System.exit(1);
}catch (NumberFormatException e){
System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
e.printStackTrace();
System.exit(1);
}
semaphore.release();
} catch (InterruptedException e) {
System.out.println("信号量发生错误");
e.printStackTrace();
System.exit(1);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println("所有线程格式化日期成功");
}
}
5.DateTimeFormatter方式
DateTimeFormatter是Java8提供的新的日期时间API中的类,DateTimeFormatter类是线程安全的,可以在高并发场景下直接使用DateTimeFormatter类来处理日期的格式化操作。代码如下所示。
package io.binghe.concurrent.lab06;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
/**
* @author binghe
* @version 1.0.0
* @description 通过DateTimeFormatter类解决线程安全问题
*/
public class SimpleDateFormatTest07 {
//执行总次数
private static final int EXECUTE_COUNT = 1000;
//同时运行的线程数量
private static final int THREAD_COUNT = 20;
private static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
public static void main(String[] args) throws InterruptedException {
final Semaphore semaphore = new Semaphore(THREAD_COUNT);
final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < EXECUTE_COUNT; i++){
executorService.execute(() -> {
try {
semaphore.acquire();
try {
LocalDate.parse("2020-01-01", formatter);
}catch (Exception e){
System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
e.printStackTrace();
System.exit(1);
}
semaphore.release();
} catch (InterruptedException e) {
System.out.println("信号量发生错误");
e.printStackTrace();
System.exit(1);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println("所有线程格式化日期成功");
}
}
可以看到,DateTimeFormatter类是线程安全的,可以在高并发场景下直接使用DateTimeFormatter类来处理日期的格式化操作。
运行程序,输出结果如下所示。
所有线程格式化日期成功
使用DateTimeFormatter类来处理日期的格式化操作运行效率比较高,推荐在高并发业务场景的生产环境使用。
6.joda-time方式
joda-time是第三方处理日期时间格式化的类库,是线程安全的。如果使用joda-time来处理日期和时间的格式化,则需要引入第三方类库。这里,以Maven为例,如下所示引入joda-time库。
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.9.9</version>
</dependency>
引入joda-time库后,实现的程序代码如下所示。
package io.binghe.concurrent.lab06;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
/**
* @author binghe
* @version 1.0.0
* @description 通过DateTimeFormatter类解决线程安全问题
*/
public class SimpleDateFormatTest08 {
//执行总次数
private static final int EXECUTE_COUNT = 1000;
//同时运行的线程数量
private static final int THREAD_COUNT = 20;
private static DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("yyyy-MM-dd");
public static void main(String[] args) throws InterruptedException {
final Semaphore semaphore = new Semaphore(THREAD_COUNT);
final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < EXECUTE_COUNT; i++){
executorService.execute(() -> {
try {
semaphore.acquire();
try {
DateTime.parse("2020-01-01", dateTimeFormatter).toDate();
}catch (Exception e){
System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
e.printStackTrace();
System.exit(1);
}
semaphore.release();
} catch (InterruptedException e) {
System.out.println("信号量发生错误");
e.printStackTrace();
System.exit(1);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println("所有线程格式化日期成功");
}
}
这里,需要注意的是:DateTime类是org.joda.time包下的类,DateTimeFormat类和DateTimeFormatter类都是org.joda.time.format包下的类,如下所示。
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
运行程序,输出结果如下所示。
所有线程格式化日期成功
使用joda-time库来处理日期的格式化操作运行效率比较高,推荐在高并发业务场景的生产环境使用。
解决SimpleDateFormat类的线程安全问题的方案总结
综上所示:在解决解决SimpleDateFormat类的线程安全问题的几种方案中,局部变量法由于线程每次执行格式化时间时,都会创建SimpleDateFormat类的对象,这会导致创建大量的SimpleDateFormat对象,浪费运行空间和消耗服务器的性能,因为JVM创建和销毁对象是要耗费性能的。所以,不推荐在高并发要求的生产环境使用。
synchronized锁方式和Lock锁方式在处理问题的本质上是一致的,通过加锁的方式,使同一时刻只能有一个线程执行格式化日期和时间的操作。这种方式虽然减少了SimpleDateFormat对象的创建,但是由于同步锁的存在,导致性能下降,所以,不推荐在高并发要求的生产环境使用。
ThreadLocal通过保存各个线程的SimpleDateFormat类对象的副本,使每个线程在运行时,各自使用自身绑定的SimpleDateFormat对象,互不干扰,执行性能比较高,推荐在高并发的生产环境使用。
DateTimeFormatter是Java 8中提供的处理日期和时间的类,DateTimeFormatter类本身就是线程安全的,经压测,DateTimeFormatter类处理日期和时间的性能效果还不错。所以,推荐在高并发场景下的生产环境使用。
joda-time是第三方处理日期和时间的类库,线程安全,性能经过高并发的考验,推荐在高并发场景下的生产环境使用。
点击关注,第一时间了解华为云新鲜技术~
Java SimpleDateFormat 使用注意:不支持多线程中定义全局的(static)SimpleDateFormat
public class Test {
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(100);
for (int i = 0; i < 20; i++) {
service.execute(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10; j++) {
try {
System.out.println(sdf.parse("2019-01-02 09:45:59"));
} catch (ParseException e) {
e.printStackTrace();
}
}
}
});
}
}
}
运行结果:
Exception in thread "pool-1-thread-3" Exception in thread "pool-1-thread-5" java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1101)
at java.lang.Double.parseDouble(Double.java:540)
at java.text.DigitList.getDouble(DigitList.java:168)
at java.text.DecimalFormat.parse(DecimalFormat.java:1321)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2088)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1455)
at java.text.DateFormat.parse(DateFormat.java:355)
at com.adolph.org.Test$1.run(Test.java:91)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
at java.lang.Thread.run(Thread.java:744)
java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1101)
at java.lang.Double.parseDouble(Double.java:540)
at java.text.DigitList.getDouble(DigitList.java:168)
at java.text.DecimalFormat.parse(DecimalFormat.java:1321)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2088)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1455)
at java.text.DateFormat.parse(DateFormat.java:355)
at com.adolph.org.Test$1.run(Test.java:91)Tue Jan 02 09:45:59 CST 2019
Tue Jan 02 09:45:59 CST 2019
Tue Jan 02 09:45:59 CST 2019
Tue Jan 02 09:45:59 CST 2019
Thu Dec 20 09:45:59 CST 2019
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
at java.lang.Thread.run(Thread.java:744)
Exception in thread "pool-1-thread-7" java.lang.NumberFormatException: For input string: "E.4250918E4"
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1241)
at java.lang.Double.parseDouble(Double.java:540)
at java.text.DigitList.getDouble(DigitList.java:168)
at java.text.DecimalFormat.parse(DecimalFormat.java:1321)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1793)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1455)
Mon Dec 20 09:45:59 CST 1
at java.text.DateFormat.parse(DateFormat.java:355)
at com.adolph.org.Test$1.run(Test.java:91)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
at java.lang.Thread.run(Thread.java:744)
原因:SimpleDateFormat 为线程不安全类型,不支持多线程使用同一个 SimpleDateFormat 对象
Java SimpleDateFormat 非线程安全
多线程调用同一个SimpleDateFormat的parse方法,解析一个“正常”的日期字符串,频繁出现解析异常的情况。最终发现SimpleDateFormat是非线程安全的。以下是java docs中的说明:
Synchronization
Date formats are not synchronized. It is recommended to create separate format instances for each thread. If multiple threads access a format concurrently, it must be synchronized externally.
各位同学引以为鉴。
Java SimpleDateFormat线程安全问题原理详解
这篇文章主要介绍了Java SimpleDateFormat线程安全问题原理详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
今天百度一些资料偶然发现SimpleDateFormat居然不是线程安全的,平时使用时根本没有考虑,万幸今天发现了这个问题,得把写的代码得翻出来整理一下了。
一般我们使用的SimpleDateFormat一般是这样写的:
public void method() { ... DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); Date date = dateFormat.parse("2020-05-10 19:53:00"); ... }
这样写完全没有任何问题,但我们有时候会觉得重复创建SimpleDateFormat耗费性能,就想到把SimpleDateFormat对象做为类的静态成员变量,那么代码就是这样了:
private static final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public void method() { ... Date date = dateFormat.parse("2020-05-10 19:53:00"); ... }
我经常在Controller做日期转换的时候就是这么干的,但这样写很有问题,多线程通知执行容易出问题,要么转换后的结果不对,要么报错,我们测试一下:
public class DateUtils { private static final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static Date prase(String date) throws ParseException { return dateFormat.parse(date); } static class Job extends Thread { @Override public void run() { try { System.out.println(this.getName() + ":" + DateUtils.prase("2020-05-10 19:53:00")); } catch (ParseException e) { } } } public static void main(String[] args) { for (int i = 0; i
测试结果如下:
那有没有好的解决方案呢,既不用重复创建对象,又保证线程安全呢?答案是有。
方法一:使用ThreadLocal
public class MyController { private static ThreadLocal local = new ThreadLocal() { protected DateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); }; }; public void method() { ... Date date = local.get().parse("2020-05-10 19:53:00"); ... } }
方法二:使用第三方apache提供工具包commons-lang3
import org.apache.commons.lang3.time.FastDateFormat; public class MyController { public void method() { ... Date date = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss").parse("2020-05-10 19:53:00"); ... } }
推荐使用第二种,既快有方便。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持小编。
我们今天的关于【多线程补充】SimpleDateFormat非线程安全与线程中、线程组中异常的处理的分享就到这里,谢谢您的阅读,如果想了解更多关于6种方法帮你搞定SimpleDateFormat类不是线程安全的问题、Java SimpleDateFormat 使用注意:不支持多线程中定义全局的(static)SimpleDateFormat、Java SimpleDateFormat 非线程安全、Java SimpleDateFormat线程安全问题原理详解的相关信息,可以在本站进行搜索。
本文标签: