不安全在什么地方?
前段时间在做系统数据清洗过程中,因为用到多线程及simpeldateformat,一开始没注意,遇到了线程安全问题,就在此描述解决办法。
// 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);
....
}
private void subFormat(int patternCharIndex, int count,
FieldDelegate delegate, StringBuffer buffer,
boolean useDateFormatSymbols)
{
int maxIntCount = Integer.MAX_VALUE;
String current = null;
int beginOffset = buffer.length();
int field = PATTERN_INDEX_TO_CALENDAR_FIELD[patternCharIndex];
int value;
if (field == CalendarBuilder.WEEK_YEAR) {
if (calendar.isWeekDateSupported()) {
value = calendar.getWeekYear();//取值
} else {
// use calendar year 'y' instead
patternCharIndex = PATTERN_YEAR;
field = PATTERN_INDEX_TO_CALENDAR_FIELD[patternCharIndex];
value = calendar.get(field);
}
...
可以看到在format代码中,将要被格式化的date设置到calendar实例中,这个实例是simpledateforamt的一个局部变量。
将设A线程调用了format,此时将A线程的(假设是2018-01-05 00:00:00)这个时间set到calendar,线程刮起,线程B进来,调用该方法(假设要格式的时间是2017-12-38 10:00:05),将该时间set到calendar。此时B线程挂起,A线程执行,通过subFormat方法获取格式化的数据,但是此时的calender里的信息是B线程的,所以这种情况,format的结果均为B线程的数据。这就是该方法线程不安全的地方。
下面是个测试方法,来体现该类线程不安全
public class Main {
@Test
public void testSimpleDateFormatThreadSafe() throws ParseException, InterruptedException {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
long now = System.currentTimeMillis();
long one = 1000 * 60 * 60 * 24;
long[] times = { now, now - one, now - one * 2, now - one * 3, now - one * 4, now - one * 5, now - one * 6,
now - one * 7, now - one * 8, now - one * 9, now - one * 10, now - one * 11, now - one * 12,
now - one * 13, now - one * 14, now - one * 15, now - one * 16, now - one * 17, now - one * 18,
now - one * 19, now - one * 20, now - one * 21, now - one * 22, now - one * 23, now - one * 24,
now - one * 25, now - one * 26, now - one * 27, now - one * 28, now - one * 29 };
ExecutorService pool = Executors.newFixedThreadPool(5);
for (int i = 0; i < 30; i++) {
pool.execute(new Work(sdf, times[i]));
// System.out.println(sdf.format(new Date(times[i])));//07
}
Thread.sleep(10000);
}
public class Work implements Runnable {
SimpleDateFormat sdf;
long date;
public Work(SimpleDateFormat sdf, long date) {
this.sdf = sdf;
this.date = date;
}
@Override
public void run() {
System.out.println(sdf.format(new Date(date)));
}
}
}
如何让它变的安全呢?
可以使用java的ThreadLocal类,该类会为每个线程实例化一个类,这样多线程之间就没有竞争对象了。这个类的原理很简单,其内部维护了一个map,key是线程的名字,value就是实例化对象。
ThreadLocal<SimpleDateFormat> safe = new ThreadLocal() {
@Override
protected Object initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
pool.execute(new Work(safe.get(),times[i]));//线程安全
通过safe直接get得到的simpledateformat的对象,就是为每个线程独立创建的。这种做法在我们熟悉的tomcat为每个单独的访问保存其专有信息,就是使用的这种方法。
如果觉得为每个线程都实例化对象开销比较大或者造成gc,也可以维护一个对象池。
实例代码:https://github.com/yangzhenkun/learn/blob/master/src/main/java/com/yasin/threadsafe/Main.java