我在最近的一次网络研讨会中谈到了这个问题,现在是时候以书面形式进行解释了。 线程安全是Java等语言/平台中类的重要品质,我们经常在线程之间共享对象。 缺乏线程安全性导致的问题很难调试,因为它们是零星的并且几乎不可能有意复制。 您如何测试对象以确保它们是线程安全的? 这就是我的做法。
我们说有一个简单的内存书架:
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 {@Testpublic void addsAndRetrieves() {Books books = new Books();String title = "Elegant Objects";int id = books.add(title);assert books.title(id).equals(title);}
}
测试通过了,但这只是一个单线程测试。 让我们尝试从几个并行线程中进行相同的操作(我正在使用Hamcrest ):
class BooksTest {@Testpublic void addsAndRetrieves() {Books books = new Books();int threads = 10;ExecutorService service =Executors.newFixedThreadPool(threads);Collection<Future<Integer>> futures =new LinkedList<>();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
创建线程池。 然后,我通过submit()
提交10个Callable
类型的对象。 他们每个人都会在书架上添加一本独特的新书。 所有这些将由池中的那十个线程中的某些线程以某种不可预测的顺序执行。
然后,我通过Future
类型的对象列表获取其执行者的结果。 最后,我计算创建的唯一图书ID的数量。 如果数字为10,则没有冲突。 我使用Set
集合来确保ID列表仅包含唯一元素。
测试通过了我的笔记本电脑。 但是,它不够坚固。 这里的问题是它并没有真正从多个并行线程测试这些工作Books
。 我们调用之间经过的时间submit()
是足够大的,完成的执行books.add()
这就是为什么实际上只有一个线程可以同时运行的原因。 我们可以通过修改一些代码来检查它:
AtomicBoolean running = new AtomicBoolean();
AtomicInteger overlaps = new AtomicInteger();
Collection<Future<Integer>> futures = new LinkedList<>();
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 LinkedList<>();
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()
释放闩锁,它们同时开始运行。 现在,在我的笔记本电脑上,即使threads
为10, overlaps
也等于3-5。
最后一个assertThat()
现在崩溃了! 我没有像以前那样得到10个图书ID。 它是7-9,但绝不是10。显然,该类不是线程安全的!
但是在修复该类之前,让我们简化测试。 让我们用RunInThreads
从Cactoos ,这确实是我们在前面已经做了完全一样的,但引擎盖下:
class BooksTest {@Testpublic 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
线程安全性,我们只需要向其方法add()
添加synchronized
。 或者,也许您可以提出更好的解决方案?
翻译自: https://www.javacodegeeks.com/2018/03/how-i-test-my-java-classes-for-thread-safety.html