3

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

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测试来重现这个问题?

4

7 回答 7

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)
于 2012-12-21T11:12:22.760 回答
3

您的测试有几个问题:

  • 每个线程只执行测试方法 100 次,应该增加线程交织场景的数量
  • 您调用System.out.printlnwhich is synchronized => 您正在重新同步您的代码,这可能会消除问题

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

你可以:

  • 使用 CountDownLatch 同时启动所有线程并增加交错
  • 删除打印语句
  • 让每个作业多次运行测试方法
于 2012-12-21T10:46:10.153 回答
2

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

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

try{
    //do stuff
catch(RuntimeException exception){
    fail();
}
于 2012-12-21T10:39:12.810 回答
2

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

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

于 2012-12-21T10:42:07.330 回答
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");
        }
    }
}
于 2012-12-21T10:47:04.123 回答
1

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

于 2012-12-21T10:39:04.790 回答
1

不是正确同步以防止可能的错误发生,而是可能会同步以产生错误,例如:

创建多个线程:

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

然后打电话sync.notifyAll()。这可能会增加看到问题的机会。

于 2012-12-21T11:00:29.310 回答