线程安全是像Java这样的语言/平台中的类的重要质量,我们经常在线程之间共享对象。由于缺乏线程安全性而导致的问题非常难以调试,因为它们零星且几乎不可能有意再现。你如何测试你的对象以确保它们是线程安全的?这是我如何做的。
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);
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()
。
https://www.leftso.com/article/376.html