leftso 1460 0 2018-04-01 21:10:14

文章位置:左搜> 编程技术> 正文

引言

线程安全是像Java这样的语言/平台中的类的重要质量,我们经常在线程之间共享对象。由于缺乏线程安全性而导致的问题非常难以调试,因为它们零星且几乎不可能有意再现。你如何测试你的对象以确保它们是线程安全的?这是我如何做的。



这里我们用一个书(book)类来说明
public class Books {
  final Map<Integer, String> map =
    new ConcurrentHashMap<>();
  int add(String title) {
    final Integer next = this.map.size() + 1;
    this.map.put(next, title);
    return next;
  }
  String title(int id) {
    return this.map.get(id);
  }
}
首先,我们在那里放置一本书,书架返回它的ID。 然后我们可以通过它的ID读取书名:
Books books = new Books();
String title = "Elegant Objects";
int id = books.add(title);
assert books.title(id).equals(title);

这个类似乎是线程安全的,因为我们使用线程安全的ConcurrentHashMap而不是更原始的和非线程安全的HashMap,对吧? 我们来测试一下:
class BooksTest {
  @Test
  public void addsAndRetrieves() {
    Books books = new Books();
    String title = "Elegant Objects";
    int id = books.add(title);
    assert books.title(id).equals(title);
  }
}
测试通过,但它只是一个单线程测试。让我们尝试从几个并行线程(我使用Hamcrest)做同样的操作:
class BooksTest {
  @Test
  public void addsAndRetrieves() {
    Books books = new Books();
    int threads = 10;
    ExecutorService service =
      Executors.newFixedThreadPool(threads);
    Collection<Future<Integer>> futures =
      new ArrayList<>(threads);
    for (int t = 0; t < threads; ++t) {
      final String title = String.format("Book #%d", t);
      futures.add(service.submit(() -> books.add(title)));
    }
    Set<Integer> ids = new HashSet<>();
    for (Future<Integer> f : futures) {
      ids.add(f.get());
    }
    assertThat(ids.size(), equalTo(threads));
  }
}

首先,我通过创建一个线程池Executors。然后我提交类型十个对象Callable通过submit()。他们每个人都会在书架上添加一本新的独特书籍。所有这些都将以某种不可预知的顺序由池中的这10个线程中的一些执行。

然后,我通过类型对象列表获取其执行者的结果Future。最后,我计算创建的唯一书籍ID数量。如果数字是10,则不存在冲突。我正在使用该Set集合以确保ID列表仅包含唯一元素。

测试通过我的笔记本电脑。但是,它不够强大。这里的问题是,它不是真的Books从多个并行线程中进行测试。在我们的呼叫之间传递的时间submit()足够大,可以完成执行books.add()。这就是为什么现实中只有一个线程会同时运行。我们可以通过修改代码来检查一下:

AtomicBoolean running = new AtomicBoolean();
AtomicInteger overlaps = new AtomicInteger();
Collection<Future<Integer>> futures =
  new ArrayList<>(threads);
for (int t = 0; t < threads; ++t) {
  final String title = String.format("Book #%d", t);
  futures.add(
    service.submit(
      () -> {
        if (running.get()) {
          overlaps.incrementAndGet();
        }
        running.set(true);
        int id = books.add(title);
        running.set(false);
        return id;
      }
    )
  );
}
assertThat(overlaps.get(), greaterThan(0));

有了这段代码,我试着看看线程多长时间重复一次,并行执行一些操作。这绝不会发生,并且overlaps等于零。因此,我们的测试并没有真正测试任何东西。它只是将十本书逐个添加到书架上。如果我将线程数量增加到1000,它们有时会开始重叠。但是我们希望它们重叠,即使只有少数它们。为了解决这个问题,我们需要使用CountDownLatch
CountDownLatch latch = new CountDownLatch(1);
AtomicBoolean running = new AtomicBoolean();
AtomicInteger overlaps = new AtomicInteger();
Collection<Future<Integer>> futures =
  new ArrayList<>(threads);
for (int t = 0; t < threads; ++t) {
  final String title = String.format("Book #%d", t);
  futures.add(
    service.submit(
      () -> {
        latch.await();
        if (running.get()) {
          overlaps.incrementAndGet();
        }
        running.set(true);
        int id = books.add(title);
        running.set(false);
        return id;
      }
    )
  );
}
latch.countDown();
Set<Integer> ids = new HashSet<>();
for (Future<Integer> f : futures) {
  ids.add(f.get());
}
assertThat(overlaps.get(), greaterThan(0));
 

现在,每个线程在触摸书籍之前,都会等待给予的许可latch。当我们通过submit()他们全部提交他们并等待。然后我们释放闩锁,countDown()他们都开始同时进行。现在,在我的笔记本电脑中,overlaps即使threads是10 时,也等于3-5 。

最后一次assertThat()崩溃!我没有像以前那样获得10本书ID。这是7-9,但从来没有10.班,显然,是不是线程安全的!

但在我们修复课程之前,让我们来简化测试。让我们用RunInThreads从Cactoos,这确实是我们在前面已经做了完全一样的,但引擎盖下:

class BooksTest {
  @Test
  public void addsAndRetrieves() {
    Books books = new Books();
    MatcherAssert.assertThat(
      t -> {
        String title = String.format(
          "Book #%d", t.getAndIncrement()
        );
        int id = books.add(title);
        return books.title(id).equals(title);
      },
      new RunsInThreads<>(new AtomicInteger(), 10)
    );
  }
}
 

第一个参数assertThat()Func(一个功能接口)的一个实例,接受AtomicInteger(的第一个参数RunsInThreads)并返回Boolean。该函数将在10个并行线程上执行,使用上面演示的相同的基于锁存器的方法。

RunInThreads似乎是紧凑和方便的,我已经在一些项目中使用它。

顺便说一句,为了使Books线程安全,我们只需要添加synchronized到它的方法add()