The following text is a partial translation of the original English article, performed by ChatGPT (gpt-3.5-turbo) and this Jekyll plugin:
我在最近的一个网络研讨会中提到过这个问题,现在是时候用文字来解释了。在像Java这样的语言/平台中,线程安全是类的一个重要品质,因为我们经常在线程之间共享对象。由于缺乏线程安全而引起的问题非常难以调试,因为它们是间歇性的,几乎不可能有意地再现。你如何测试你的对象以确保它们是线程安全的?以下是我的做法。
假设有一个简单的内存书架:
首先,我们将一本书放在那里,书架会返回它的ID。然后我们可以通过它的ID读取书的标题。
这个类似乎是线程安全的,因为我们使用了线程安全的 ConcurrentHashMap
而不是更原始和非线程安全的 HashMap
,对吗?让我们来试试测试一下:
测试通过,但这只是一个单线程测试。让我们尝试从几个并行线程执行相同的操作(我使用Hamcrest)。
首先,我通过Executors
创建了一个线程池。然后,我通过submit()
方法提交了十个Callable
类型的对象。每个对象都会向书架中添加一本新的唯一图书。所有这些对象将由线程池中的十个线程中的某些线程以某种不可预测的顺序执行。
然后,我通过Future
对象的列表获取它们的执行结果。最后,我计算创建的唯一图书ID的数量。如果数量为10,表示没有冲突。为了确保ID列表只包含唯一元素,我使用了Set
集合。
这个测试在我的笔记本电脑上通过了。然而,它的测试力度还不够强。问题在于它并没有真正测试多个并行线程中的Books
。我们在调用submit()
之间经过的时间足够长,以致于books.add()
的执行已经完成。这就是为什么实际上只有一个线程会同时运行的原因。我们可以通过稍微修改代码来验证这一点:
通过这段代码,我试图观察线程重叠的频率,并并行执行某些操作。然而,这种情况从未发生,overlaps
始终等于零。因此,我们的测试实际上还没有测试任何内容。它只是逐一向书架添加十本书。如果我将线程数量增加到1000,它们有时会发生重叠。但是,即使只有很少的线程,我们也希望它们发生重叠。为了解决这个问题,我们需要使用 CountDownLatch
。
现在,每个线程在触碰书籍之前都会等待latch
给出的许可。当我们通过submit()
提交它们时,它们会保持等待状态。然后,我们使用countDown()
释放latch
,它们同时开始执行。现在,在我的笔记本上,即使threads
为10,overlaps
也等于3-5。
而最后的assertThat()
现在崩溃了!我没有得到10个图书ID,就像以前一样。它是7-9,但从未是10。显然,这个类不是线程安全的!
但在我们修复这个类之前,让我们简化我们的测试。让我们使用Cactoos中的RunInThreads
(在最新版本中更名为Threads
),它实际上与我们上面所做的完全相同,但在底层实现。
assertThat()
的第一个参数是Func
的一个实例(一个函数式接口),它接受一个AtomicInteger
(RunsInThreads
的第一个参数)并返回Boolean
。这个函数将在10个并行线程上执行,使用与上面演示的相同的栅栏(latch)方法。
这个RunInThreads
看起来很简洁方便,我已经在一些项目中使用它了。
顺便说一下,为了使Books
线程安全,我们只需要在它的add()
方法中添加synchronized
关键字。或者也许你可以提供一个更好的解决方案?
附言:我从Goetz等人的《Java并发编程实战》(https://amzn.to/2c7sVS1)中学到了所有这些知识。
Translated by ChatGPT gpt-3.5-turbo/42 on 2023-12-27 at 04:44