如何测试Java类的线程安全性

本文探讨如何测试Java类的线程安全性,通过示例展示了线程安全问题的复杂性和测试方法,包括使用并发集合、CountDownLatch以及Cactoos的RunInThreads工具进行并行测试。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

我在最近的学习中和优锐课老师谈到了这个问题,然后写下了这篇文章。如果你有更深层次的见解,欢迎加入Java学习资料交流qq群:907135806与我们一起探讨。

线程安全是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 {
  @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));
  }
}

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

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

测试通过了我的笔记本电脑。但是,它不够坚固。这里的问题是它并没有真正从多个并行线程测试这些工作簿。在两次调用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));

通过此代码,我试图查看线程相互重叠的频率并并行执行某项操作。这永远不会发生,并且重叠等于零。因此,我们的测试尚未真正完成任何测试。它只是在书架上一一增加了十本书。如果我将线程数增加到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));

现在,每个线程在接触书本之前都要等待闩锁给出的许可。当我们通过submit()提交所有内容时,它们将保留并等待。然后,我们用countDown()释放闩锁,它们同时开始运行。现在,在我的笔记本电脑上,即使线程为10,重叠也等于3-5。

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

但是在修复该类之前,让我们简化测试。让我们使用来自Cactoos的RunInThreads,它与我们上面做的完全一样,但实际上是:

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的第一个参数)并返回布尔值。使用与上述相同的基于闩锁的方法,将在10个并行线程上执行此功能。

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

顺便说一句,为了使Books成为线程安全的,我们只需要向其方法add()中同步添加。或者,也许你可以提出更好的解决方案?加入Java学习资料交流qq群:907135806和我们一起探讨吧!

ps:有想要JVM、Mysql、Tomcat、Spring Boot、Spring Cloud、Zookeeper、Kafka、RabbitMQ、RockerMQ、Redis、ELK、Git等Java学习资料和视频课程的童鞋也可以加vx:ddmsiqi 领取啦,共同进步~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值