在多线程情况下使用SimpleDateFormat要尤其注意,它不是线程安全的。问题是一个老问题,但是经常出现。
在java的api中这样描述:
“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.”
这个类一个典型的场景是按照指定的格式,将日期类型数据转换为字符串类型,或者将字符串类型转换为日期类型。
(1)多线程场景,在方法体内部new DateFormat对象,但是这种情况对于内存开销比较大,同时对于一些可以复用的格式,重复来拷贝代码看起来有点恶心;
(2)通过synchronize机制,但是线程同步同样会造成性能下降;
(3)到通过静态工厂方法例如“getDateInstance”来创建DateFormat的对象,这种方法看起来很有效,但是由于在DateFormat中缺乏同步,所以在多线程获取同一个对象的时候会出现问题,典型的异常有NumberFormatExcetion和ArrayIndexOutOfBoundsException;
(4)通过ThreadLocal来搞定;
(5)使用第三方工具包,例如apache-common的DateFormatUtils;
A、首先模拟一个线程安全问题
在用到format的时候,我们经常在方法中new一个对象,SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd");但是有时候,我们为了省事和复用,把这个对象作为静态对象来抽取出来,但是这样是有问题的,线程不安全。
下面这个例子,SimpleDateFormat的对象是静态类,所以执行结果的时候就出现错乱的情况了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
import
java.text.SimpleDateFormat;
import
java.util.Date;
import
java.util.concurrent.ExecutorService;
import
java.util.concurrent.Executors;
import
org.junit.Test;
public
class
SimpleTest {
public
static
void
main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(
10
);
for
(
int
i=
0
;i<
30
;i++){
threadPool.submit(
new
TestSimpleThread(
new
Date(
2012
,
11
,i),i));
}
}
}
class
TestSimpleThread
implements
Runnable{
private
static
SimpleDateFormat sf =
new
SimpleDateFormat(
"yyyy-MM-dd"
);
private
Date date;
private
int
temp;
public
TestSimpleThread(Date d,
int
t){
date = d;
temp = t;
}
@Override
public
void
run() {
System.out.println(Thread.currentThread().getName()+
"---"
+temp+
"---"
+sf.format(date));
}
}
|
按照我们的设计,两列值如果线程安全,那应该是相同的,但是执行的时候出现了错误。
B、上面SimpleDateFormat是类的静态属性,通过加锁能够解决这个问题,下面例子跑出来的结果是正常的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
import
java.text.SimpleDateFormat;
import
java.util.Date;
import
java.util.concurrent.ExecutorService;
import
java.util.concurrent.Executors;
import
org.junit.Test;
public
class
SimpleTest {
public
static
void
main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(
10
);
for
(
int
i=
0
;i<
30
;i++){
threadPool.submit(
new
TestSimpleThread(
new
Date(
2012
,
11
,i),i));
}
}
}
class
TestSimpleThread
implements
Runnable{
private
static
SimpleDateFormat sf =
new
SimpleDateFormat(
"yyyy-MM-dd"
);
public
String convertDateToString(Date d)
throws
Exception {
String result;
synchronized
(sf) {
result = sf.format(d);
}
return
result;
}
private
Date date;
private
int
temp;
public
TestSimpleThread(Date d,
int
t){
date = d;
temp = t;
}
@Override
public
void
run() {
try
{
System.out.println(Thread.currentThread().getName()+
"---"
+temp+
"---"
+convertDateToString(date));
}
catch
(Exception e) {
//
}
}
}
|
在进行format的时候加了同步:
C、通过 SimpleDateFormat.getDateInstance()来获取对象进行操作,没有还原上面说的场景,但是发现一个问题,就是里面的参数是int类型,如果想灵活的进行自定义格式,用起来不是很方便。
D、采用ThreadLocal的方式来解决线程安全问题
下面这个是通过覆盖initialValue方法,
1
2
3
4
5
6
7
8
9
10
11
12
|
public
class
ThreadLocalSF {
private
static
ThreadLocal<DateFormat> threadDateFormatDefault =
new
ThreadLocal<DateFormat>(){
@Override
protected
DateFormat initialValue() {
return
new
SimpleDateFormat(
"yyyy-MM-dd"
);
}
};
public
static
String convertDateToString(Date d) {
return
threadDateFormatDefault.get().format(d);
}
}
|
还有一中就是 获取线程的变量副本,如果不覆盖initialValue,第一次get返回null,故需要初始化一个SimpleDateFormat,并set到threadLocal中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
public class DateUtil {
private static final String DATE_FORMAT =
"yyyy-MM-dd HH:mm:ss"
;
// 第一次调用get将返回null
private static ThreadLocal threadLocal =
new
ThreadLocal();
// 获取线程的变量副本,如果不覆盖initialValue,第一次get返回null,故需要初始化一个SimpleDateFormat,并set到threadLocal中
public static DateFormat getDateFormat() {
DateFormat df = (DateFormat) threadLocal.get();
if
(df ==
null
) {
df =
new
SimpleDateFormat(DATE_FORMAT);
threadLocal.set(df);
}
return
df;
}
}
|
网上有张图对于三种方式的TPS做了对比,性能最好的是ThreadLocal的方式。
看了一些公共的DateUtil类,基本上采用的最简单的模式:
写一个静态类DateUtil,里面搞一个静态方法,每个方法内部new一个SimpleDateFormat的类,然后进行调用,这种最简单呵呵。
参考:http://www.javacodegeeks.com/2010/07/java-best-practices-dateformat-in.html