不安全在什么地方?
前段时间在做系统数据清洗过程中,因为用到多线程及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就是实例化对象。
ThreadLocalsafe = 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