如何对同步方法进行单元测试?

11

假设我有这样一个方法:

synchronized void incrementIndex() {
      index++;
}

我想对这个方法进行单元测试,以查看如果多个线程同时尝试递增索引,索引的最终值是否被正确设置。假设我不知道该方法声明中的 “synchronized” 关键字(我只知道该方法的契约),我该如何进行测试?

p.s. 如果有帮助的话,我正在使用 Mockito 编写测试用例。


我不确定这个方法是否可测试,因为你的incrementIndex方法是原子的。同步只有在至少需要执行两个步骤时才很重要,例如两个写操作、两个读操作或者最常见的读写操作(经典的测试和设置问题)。任何测试如何捕捉到你的incrementIndex()方法未能同步? - candied_orange
3个回答

8
您可以通过多线程执行该方法并断言结果是否符合预期来测试它。但是我对此的有效性和可靠性存在疑虑。多线程代码测试往往十分困难,大部分情况下需要精心设计。我强烈建议添加测试,以确保您期望同步的方法实际上具有同步修饰符。以下是两种方法的示例:
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.junit.Assert.assertThat;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import org.junit.Test;

public class SyncTest {
  private final static int NUM_THREADS = 10;
  private final static int NUM_ITERATIONS = 1000;

  @Test
  public void testSynchronized() throws InterruptedException {
    // This test will likely perform differently on different platforms.
    ExecutorService executor = Executors.newFixedThreadPool(NUM_THREADS);
    final Counter sync = new Counter();
    final Counter notSync = new Counter();

    for (int i = 0; i < NUM_THREADS; i++) {
      executor.submit(new Runnable() {
        @Override
        public void run() {
          for (int i = 0; i < NUM_ITERATIONS; i++) {
            sync.incSync();
            notSync.inc();
          }
        }
      });
    }

    executor.shutdown();
    executor.awaitTermination(5, TimeUnit.SECONDS);
    assertThat(sync.getValue(), is(NUM_THREADS * NUM_ITERATIONS));
    assertThat(notSync.getValue(), is(not(NUM_THREADS * NUM_ITERATIONS)));
  }

  @Test
  public void methodIncSyncHasSynchronizedModifier() throws Exception {
    Method m = Counter.class.getMethod("incSync");
    assertThat(Modifier.isSynchronized(m.getModifiers()), is(true)); 
  }

  private static class Counter {
    private int value = 0;

    public synchronized void incSync() {
      value++;
    }

    public void inc() {
      value++;
    }

    public int getValue() {
      return value;
    }
  }
}

1
在调用 executor.awaitTermination() 方法之前,你应该先调用 executor.shutdown() 方法。否则,你可能会等待很长时间,因为 awaitTermination 并不会真正关闭你的 executor。(https://dev59.com/7WMl5IYBdhLWcg3wa2XV) - Mcmil
如果方法只包含同步块,因此它没有同步作为修饰符,那该怎么办?除了断言“预期行为”之外,还有其他测试方法吗? - Toni Nagy
@ToniNagy 我认为断言行为是你唯一的选择。既然你总是先测试代码的正确性,我会默认测试预期的行为而不是 synchronized 修饰符的存在。 - LeffeBrune

4

CandiedOrange在他对你的问题的评论中是正确的。换句话说,鉴于你提到的方法,你不必担心threadA在threadB调用该方法的同时调用该方法,因为两个调用都会写入index。如果是这样的情况:

void incrementIndex() {
     index++;
     System.out.println(index); // threadB might have written to index
                                // before this statement is executed in threadA
}

当threaA调用该方法时,它会在第一条语句中增加index值,然后尝试在第二条语句中读取index的值,此时threadB可能已经调用该方法并在threadA读取并打印之前增加了index。这就是需要使用synchronized以避免出现这种情况。

现在,如果您仍想测试同步,并且可以访问方法代码(或者可能可以做类似的原型),您可以考虑以下示例,其中演示多线程如何处理带有同步方法的代码:

public void theMethod(long value, String caller) {
    System.out.println("thread" + caller + " is calling...");
    System.out.println("thread" + caller + " is going to sleep...");

    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    System.out.println("thread" + caller + " woke up!");
}

这应该输出:

threadA is calling...
threadA is going to sleep...
threadA woke up!
threadB is calling...
threadB is going to sleep...
threadB woke up!

没有使用 synchronized 关键字,输出将会是:
threadA is calling...
threadA is going to sleep...
threadB is calling...
threadB is going to sleep...
threadA woke up!
threadB woke up!

-2

是的。i++是原子操作吗?

不是。

因此,如果您关心程序的正确性,则同步是合理的。

但测试很难。

视觉检查告诉我们,非原子增量操作受到保护并变为原子操作,就我们所知一切都很好,但我们对系统的其余状态一无所知。

可以通过副作用来测试函数是否同步。有一种可测试的模式,可以组织代码,使您注入同步而不是使用Java内置功能,但如果只有您最初的问题,那么我会依靠视觉检查和明显的正确性。


https://dev59.com/ZV8e5IYBdhLWcg3w-OYn - Julien

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接