在测试中重现SimpleDateFormat非线程安全性

时间:2012-12-21 10:33:00

标签: java multithreading junit

我有一个应用程序,它不时在日志中有这个堆栈跟踪:

java.lang.ArrayIndexOutOfBoundsException: 514
        at sun.util.calendar.BaseCalendar.getCalendarDateFromFixedDate(BaseCalendar.java:436)
        at java.util.GregorianCalendar.computeFields(GregorianCalendar.java:2081)
        at java.util.GregorianCalendar.computeFields(GregorianCalendar.java:1996)
        at java.util.Calendar.complete(Calendar.java:1312)
        at java.util.Calendar.get(Calendar.java:1093)
        at java.text.SimpleDateFormat.subFormat(SimpleDateFormat.java:917)
        at java.text.SimpleDateFormat.format(SimpleDateFormat.java:824)
        at java.text.SimpleDateFormat.format(SimpleDateFormat.java:796)
        at java.text.DateFormat.format(DateFormat.java:314)
        at me.myself.i.Message.toString(Message.java:203)
        at java.lang.String.valueOf(String.java:2615)
        at java.lang.StringBuilder.append(StringBuilder.java:116)

我认为这个问题可能存在于某些方面:

public class Message{
private transient DateFormat logDateFormat;
    @Override
    public String toString() {
        final StringBuilder result = new StringBuilder(getClass().getSimpleName());
        result.append("Time=").append(logDateFormat.format(new Date(getExpireTime())));     
        return result.toString();
    }
}

我认为多个线程同时调用toString(),但是我在本地机器上复制它时遇到了麻烦:

  @Before
  public void setUp() {
    message = new Message();
    pool = Executors.newFixedThreadPool(numOfThreads);
 }

  @Test
  public void multiThreadTest() {
        for (int i=0; i<numOfThreads; i++) {
            TestJob j = new TestJob(message);
            pool.submit(j);
        }
        pool.shutdown();
        while(!pool.isTerminated()){            
        }
    }

    class TestJob implements Runnable{

        private Message message;
        private int n=100;

        public TestJob(Message message) {
            this.message= message;
        }

        public void run() {
            for (int i=0; i<n; i++) {
                try{
                    System.out.println(message.toString());
                } catch(Exception e){
                    e.printStackTrace();
                }
            }
        }
    }

如何编写正确的junit测试来重现此问题?

7 个答案:

答案 0 :(得分:8)

由于我的第一次测试没有重现您的问题,请试试这个

final SimpleDateFormat f = new SimpleDateFormat("yyyy-MM-dd");
ExecutorService ex = Executors.newFixedThreadPool(1000);
for (;;) {
    ex.execute(new Runnable() {
        public void run() {
            try {
                f.format(new Date(new Random().nextLong()));
            } catch (Exception e) {
                e.printStackTrace();
                System.exit(1);
            }
        };
    });
}

需要时间,但最后我得到了

java.lang.ArrayIndexOutOfBoundsException: 3144942
    at sun.util.calendar.BaseCalendar.getCalendarDateFromFixedDate(BaseCalendar.java:454)
    at java.util.GregorianCalendar.computeFields(GregorianCalendar.java:2333)
    at java.util.GregorianCalendar.computeFields(GregorianCalendar.java:2248)
    at java.util.Calendar.complete(Calendar.java:1560)
    at java.util.Calendar.get(Calendar.java:1162)
    at java.text.SimpleDateFormat.subFormat(SimpleDateFormat.java:1093)
    at java.text.SimpleDateFormat.format(SimpleDateFormat.java:978)
    at java.text.SimpleDateFormat.format(SimpleDateFormat.java:948)
    at java.text.DateFormat.format(DateFormat.java:336)
    at Test1$1.run(Test1.java:17)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1110)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:603)
    at java.lang.Thread.run(Thread.java:722)

答案 1 :(得分:3)

您的测试有几个问题:

  • 每个线程仅执行100次测试方法,应增加该次数以增加线程交错场景的数量
  • 您致电System.out.println,即同步=&gt;您正在重新同步可能会删除问题的代码

另请注意,SimpleDateFormat在内部使用同步的StringBuffer,因此获得并发问题并不容易。

你可以:

  • 使用CountDownLatch同时启动所有线程并递增交错
  • 删除打印声明
  • 让每个作业多次运行测试方法

答案 2 :(得分:2)

测试线程安全(和性能)的一般方法是尝试多次。这与单元测试应该是可重现的意义相冲突(意味着每次运行具有相同的结果)。这样做的原因是,如果线程安全很多,则涉及机会。

为了使测试用例在异常时失败,每个调用toString()的线程都应该捕获异常并且如果抛出则失败(一个junit函数)。

try{
    //do stuff
catch(RuntimeException exception){
    fail();
}

答案 3 :(得分:2)

100次是不太可能的。我建议使用至少10,00并使用比cpu更多的线程来使机器超载。例如机器上有32个线程,有8个cpus。

无论你运行多久都无法通过测试确定代码是线程安全的,因为你只能确定你没有看到它。

答案 4 :(得分:2)

试试我的测试,

public class Test1 {

    public static void main(String[] args) throws Exception {
        final SimpleDateFormat f = new SimpleDateFormat("yyyy-MM-dd");
        final Date d1 = f.parse("2001-01-01");
        final Date d2 = f.parse("2012-12-12");
        for (int i = 0; i < 100; i++) {
            System.out.print(i + " ");
            final int j = i;
            new Thread() {

                void test(String s, Date expected) throws ParseException {
                    //synchronized (Test1.class) {
                        Date d = f.parse(s);
                        if (!d.equals(expected)) {
                            System.out.println(d + " != " + expected);
                            System.exit(1);
                        }
                    //}
                }

                public void run() {
                    try {
                        if (j % 2 == 0) {
                            test("2001-01-01", d1);
                        } else {
                            test("2012-12-12", d2);
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                        System.exit(1);
                    }
                };
            }.start();
            System.out.println("OK");
        }
    }
}

答案 5 :(得分:1)

for循环替换为while(true)并等待一段时间。

答案 6 :(得分:1)

可能会同步生成错误,而不是正确同步可能发生的错误而不是,例如:

创建多个线程:

while(...)
    synchronized(sync) {
        sync.wait();
    }
    toString();
}

然后致电sync.notifyAll()可能会增加查看问题的机会。