分享和探讨——如何测试Java类的线程安全性?

缺少线程安全性致使的问题很难调试,由于它们是零星的,几乎不可能有意复制。你如何测试对象以确保它们是线程安全的?html

我在最近的学习中和优锐课老师谈到了这个问题。如今,是时候以书面形式进行解释了。线程安全是Java等语言/平台中类的重要素质,咱们常常在线程之间共享对象。缺少线程安全性致使的问题很难调试,由于它们是零星的,几乎不可能有意复制。你如何测试对象以确保它们是线程安全的?这就是个人作法。java

假设有一个简单的内存书架:api

 1 class Books {
 2   final Map<Integer, String> map =
 3     new ConcurrentHashMap<>();
 4   int add(String title) {
 5     final Integer next = this.map.size() + 1;
 6     this.map.put(next, title);
 7     return next;
 8   }
 9   String title(int id) {
10     return this.map.get(id);
11   }
12 }

 

首先,咱们将一本书放在那里,书架返回其ID。而后,咱们能够经过ID读取该书的标题:安全

1 Books books = new Books();
2 String title = "Elegant Objects";
3 int id = books.add(title);
4 assert books.title(id).equals(title);

 

该相似乎是线程安全的,由于咱们使用的是线程安全的ConcurrentHashMap而不是更原始的和非线程安全的HashMap,对吗? 让咱们尝试测试一下:oracle

1 class BooksTest {
2   @Test
3   public void addsAndRetrieves() {
4     Books books = new Books();
5     String title = "Elegant Objects";
6     int id = books.add(title);
7     assert books.title(id).equals(title);
8   }
9 }

 

测试经过了,但这只是一个单线程测试。让咱们尝试从几个并行线程中进行相同的操做(我正在使用Hamcrest):学习

 1 class BooksTest {
 2   @Test
 3   public void addsAndRetrieves() {
 4     Books books = new Books();
 5     int threads = 10;
 6     ExecutorService service =
 7       Executors.newFixedThreadPool(threads);
 8     Collection<Future<Integer>> futures =
 9       new ArrayList<>(threads);
10     for (int t = 0; t < threads; ++t) {
11       final String title = String.format("Book #%d", t);
12       futures.add(service.submit(() -> books.add(title)));
13     }
14     Set<Integer> ids = new HashSet<>();
15     for (Future<Integer> f : futures) {
16       ids.add(f.get());
17     }
18     assertThat(ids.size(), equalTo(threads));
19   }
20 }

 

首先,我经过执行程序建立线程池。而后,我经过submit()提交十个Callable类型的对象。 他们每一个人都会在书架上添加一本独特的新书。全部这些将由池中的那十个线程中的某些线程以某种不可预测的顺序执行。测试

而后,我经过Future类型的对象列表获取其执行者的结果。最后,我计算建立的惟一图书ID的数量。若是数字为10,则没有冲突。 我使用Setcollection来确保ID列表仅包含惟一元素。this

测试经过了个人笔记本电脑。可是,它不够坚固。这里的问题是它并无真正从多个并行线程测试这些工做簿。在两次调用submit()之间通过的时间足够长,能够完成books.add()的执行。这就是为何实际上只有一个线程能够同时运行的缘由。咱们能够经过修改一些代码来检查它:atom

 1 AtomicBoolean running = new AtomicBoolean();
 2 AtomicInteger overlaps = new AtomicInteger();
 3 Collection<Future<Integer>> futures =
 4   new ArrayList<>(threads);
 5 for (int t = 0; t < threads; ++t) {
 6   final String title = String.format("Book #%d", t);
 7   futures.add(
 8     service.submit(
 9       () -> {
10         if (running.get()) {
11           overlaps.incrementAndGet();
12         }
13         running.set(true);
14         int id = books.add(title);
15         running.set(false);
16         return id;
17       }
18     )
19   );
20 }
21 assertThat(overlaps.get(), greaterThan(0));

 

经过此代码,我试图查看线程相互重叠的频率并并行执行某项操做。这永远不会发生,而且重叠等于零。所以,咱们的测试还没有真正完成任何测试。它只是在书架上一一增长了十本书。若是我将线程数增长到1000,它们有时会开始重叠。可是,即便它们数量不多,咱们也但愿它们重叠。为了解决这个问题,咱们须要使用CountDownLatch:spa

 1 CountDownLatch latch = new CountDownLatch(1);
 2 AtomicBoolean running = new AtomicBoolean();
 3 AtomicInteger overlaps = new AtomicInteger();
 4 Collection<Future<Integer>> futures =
 5   new ArrayList<>(threads);
 6 for (int t = 0; t < threads; ++t) {
 7   final String title = String.format("Book #%d", t);
 8   futures.add(
 9     service.submit(
10       () -> {
11         latch.await();
12         if (running.get()) {
13           overlaps.incrementAndGet();
14         }
15         running.set(true);
16         int id = books.add(title);
17         running.set(false);
18         return id;
19       }
20     )
21   );
22 }
23 latch.countDown();
24 Set<Integer> ids = new HashSet<>();
25 for (Future<Integer> f : futures) {
26   ids.add(f.get());
27 }
28 assertThat(overlaps.get(), greaterThan(0));

 

如今,每一个线程在接触书本以前都要等待闩锁给出的许可。当咱们经过submit()提交全部内容时,它们将保留并等待。而后,咱们用countDown()释放闩锁,它们同时开始运行。如今,在个人笔记本电脑上,即便线程为10,重叠也等于3-5。

 

最后一个assertThat()如今崩溃了!我没有像之前那样获得10个图书ID。它是7-9,但毫不是10。显然,该类不是线程安全的!

 

可是在修复该类以前,让咱们简化测试。让咱们使用来自Cactoos的RunInThreads,它与咱们上面作的彻底同样,但其实是:

 1 class BooksTest {
 2   @Test
 3   public void addsAndRetrieves() {
 4     Books books = new Books();
 5     MatcherAssert.assertThat(
 6       t -> {
 7         String title = String.format(
 8           "Book #%d", t.getAndIncrement()
 9         );
10         int id = books.add(title);
11         return books.title(id).equals(title);
12       },
13       new RunsInThreads<>(new AtomicInteger(), 10)
14     );
15   }
16 }

 

assertThat()的第一个参数是Func(功能接口)的实例,它接受AtomicIntegerRunsInThreads的第一个参数)并返回布尔值。使用与上述相同的基于闩锁的方法,将在10个并行线程上执行此功能。

RunsInThreads彷佛紧凑且方便,我已经在一些项目中使用它。

顺便说一句,为了使Books成为线程安全的,咱们只须要向其方法add()中同步添加。或者,也许你能够提出更好的解决方案?

相关文章
相关标签/搜索