【多线程补充】SimpleDateFormat非线程安全与线程中、线程组中异常的处理

时间:2022-03-06 23:05:02

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循环)。

【多线程补充】SimpleDateFormat非线程安全与线程中、线程组中异常的处理

查看线程信息

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);
}
}

结果:

【多线程补充】SimpleDateFormat非线程安全与线程中、线程组中异常的处理

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);
}
}

结果:

【多线程补充】SimpleDateFormat非线程安全与线程中、线程组中异常的处理