我是怎样测试Java类的线程安全性的
线程安全性是Java等语言/平台中类的一个重要标准,在Java中,我们经常在线程之间共享对象。由于缺乏线程安全性而导致的问题很难调试,因为它们是偶发的,而且几乎不可能有目的地重现。如何测试对象以确保它们是线程安全的?
假如有一个内存书架
package com.mzc.common.thread; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; /**
* <p class="detail">
* 功能: 内存书架
* </p>
*
* @author Moore
* @ClassName Books.
* @Version V1.0.
* @date 2019.12.10 14:00:13
*/
public class Books {
final Map<Integer, String> map = new ConcurrentHashMap<>(); /**
* <p class="detail">
* 功能: 存书,并返回书的id
* </p>
*
* @param title :
* @return int
* @author Moore
* @date 2019.12.10 14:00:16
*/
int add(String title) {
final Integer next = this.map.size() + 1;
this.map.put(next, title);
return next;
} /**
* <p class="detail">
* 功能: 根据书的id读取书名
* </p>
*
* @param id :
* @return string
* @author Moore
* @date 2019.12.10 14:00:16
*/
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,对吧?我们先来测试一下:
public 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):
/**
* <p class="detail">
* 功能: 多线程测试
* </p>
*
* @throws ExecutionException the execution exception
* @throws InterruptedException the interrupted exception
* @author Moore
* @date 2019.12.10 14:16:34
*/
@Test
public void addsAndRetrieves2() throws ExecutionException, InterruptedException {
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()提交10个Callable类型的对象。他们每个都会在书架上添加一本唯一的新书。所有这些将由池中的10个线程中的某些线程以某种不可预测的顺序执行。
然后,我通过Future类型的对象列表获取其执行者的结果。最后,我计算创建的唯一图书ID的数量。如果数字为10,则没有冲突。我使用Set集合来确保ID列表仅包含唯一元素。
我们看一下这样改造后的运行结果:

测试也通过了,但是,它不够强壮。这里的问题是它并没有真正从多个并行线程测试这些书。在两次调用commit()之间经过的时间足够长,可以完成books.add()的执行。这就是为什么实际上只有一个线程可以同时运行的原因。
我们可以通过修改一些代码再来检查它:
@Test
public void addsAndRetrieves3() {
Books books = new Books();
int threads = 10;
ExecutorService service = Executors.newFixedThreadPool(threads);
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));
}
看一下测试结果:

执行错误,说明插入的书和返回的id数量是不冲突的。
通过上面的代码,我试图了解线程之间的重叠频率以及并行执行的频率。但是基本上概率为0,所以这个测试还没有真正测到我想测的,还不是我们想要的,它只是把十本书一本一本地加到书架上。
再来:

可以看到,如果我把线程数增加到1000,它们会开始重叠或者并行运行。
但是我希望即使线程数只有10个的时候,也会出现重叠并行的情况。怎么办呢?为了解决这个问题,我使用CountDownLatch:
@Test
public void addsAndRetrieves4() throws ExecutionException, InterruptedException {
Books books = new Books();
int threads = 10;
ExecutorService service = Executors.newFixedThreadPool(threads);
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,但是线程的重叠数是大于0的,所以assertTrue执行通过,ids也不等于10了,也就是没有像以前那样得到10个图书ID。显然,Books类不是线程安全的!
在修复优化该类之前,教大家一个简化测试的方法,使用来自Cactoos的RunInThreads,它与我们上面所做的完全一样,但代码是这样的:
@Test
public void addsAndRetrieves5() {
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(RunsThreads的第一个参数)并返回布尔值。此函数将在10个并行线程上执行,使用与上述相同的基于锁的方法。
这个RunInThreads看起来非常紧凑,用起来也很方便,推荐给大家,可以用起来的。只需要在你的项目中添加一个依赖:
<dependency>
<groupId>org.llorllale</groupId>
<artifactId>cactoos-matchers</artifactId>
<version>0.18</version>
</dependency>
最后,为了使Books类成为线程安全的,我们只需要向其方法add()中同步添加就可以了。或者,聪明的码小伙伴们,你们有更好的方案吗?欢迎留言,大家一起讨论。
文章同步公众号:码之初,每天推送Java技术文章,期待您的关注!
原创不易,转载请注明出处,谢谢!
我是怎样测试Java类的线程安全性的的更多相关文章
- 分享和探讨——如何测试Java类的线程安全性?
缺乏线程安全性导致的问题很难调试,因为它们是零星的,几乎不可能有意复制.你如何测试对象以确保它们是线程安全的? 我在最近的学习中和优锐课老师谈到了这个问题.现在,是时候以书面形式进行解释了.线程安全是 ...
- Java 并发基础——线程安全性
当线程安全:多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协调,这个类都能表现出正确的行为,那么久称这个类是线程安全的. 在线程 ...
- 测试 Java 类的非公有成员变量和方法
引言 对于软件开发人员来说,单元测试是一项必不可少的工作.它既可以验证程序的有效性,又可以在程序出现 BUG 的时候,帮助开发人员快速的定位问题所在.但是,在写单元测试的过程中,开发人员经常要访问类的 ...
- java容器的线程安全性
参考:https://www.cnblogs.com/yjd_hycf_space/p/7760248.html 线程安全的: Vector HashTable StringBuffer 线程不安全的 ...
- Java并发编程实战 之 线程安全性
1.什么是线程安全性 当多个线程访问某个类时,不管运行时环境采用何种调用方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全 ...
- 【基础】java类的各种成员初始化顺序
父子类继承时的静态代码块,普通代码块,静态方法,构造方法,等先后顺序 前言: 普通代码块:在方法或语句中出现的{}就称为普通代码块.普通代码块和一般的语句执行顺序由他们在代码中出现的次序决定--“先出 ...
- jmeter测试java代码
有时候总是要写代码的,不得不说你也得会,这不往下看 java请求了,就的写代码,那么先来实现一个类, package com.company.jemeters; public class Hello ...
- Java中各种集合(字符串类)的线程安全性!!!
Java中各种集合(字符串类)的线程安全性!!! 一.概念: 线程安全:就是当多线程访问时,采用了加锁的机制:即当一个线程访问该类的某个数据时,会对这个数据进行保护,其他线程不能对其访问,直到该线程读 ...
- Java并发编程实战 第2章 线程安全性
编写线程安全的 代码,核心在与对共享的和可变的对象的状态的访问. 如果多个线程访问一个可变的对象时没有使用同步,那么就会出现错误.在这种情况下,有3中方式可以修复这个问题: 不在线程之间共享该状态变量 ...
随机推荐
- 用Java实现简单的区块链
用 Java 实现简单的区块链 1. 概述 本文中,我们将学习区块链技术的基本概念.也将根据概念使用 Java 来实现一个基本的应用程序. 进一步,我们将讨论一些先进的概念以及该技术的实际应用. 2. ...
- Java性能分析神器--VisualVM Launcher[1]
Java性能分析神器1--VisualVM Launcher VisualVM 当你日复一日敲代码的时候,当你把各种各样的框架集成到一起的时候,看着大功告成成功运行的日志,有没有那么一丝丝迷茫和惆怅: ...
- PHP路径指定web路径的方法
PHP路径指定web路径的方法直接在/前面加.就是代表web路径了 不是按照文件路径来算了 <pre>./Public/uploads/suolutu/' . $suijishu . '_ ...
- Ansible之系列命令详解
ansible系列命令有:ansible.ansible-doc.ansible-playbook.ansible-vault.ansible-console.ansible-galaxy.ansib ...
- docker swarm 过滤器affinity 限制副本不会出现在同一个节点上
affinity:container!=容器服务名称(可以是正则) 举个例子:stack_ds.yaml # cat stack_dsc.yaml version: '3.0' services: t ...
- 【科创人·独家】MegaEase左耳朵耗子陈皓复盘创业:第一年盈利被当骗子,线下广阔天地大有可为
[科创人·独家]MegaEase左耳朵耗子陈皓复盘创业:第一年盈利被当骗子,线下广阔天地大有可为 原创: babayage CTO科创圈 与上百位科技创业者共同关注科创人的成长心路. 文末有彩蛋:& ...
- 2019年PHP面试题附答案(实战经验)
出于一些原因近期做了一次工作变动,在职交接近一个半月时间大概面试了十五家公司,并且得到了自己比较满意的offer,最后基本上无缝衔接了新工作.总体来说,虽然准备的很充分,但面试期间还是暴露了许多问题, ...
- 【前端知识体系-JS相关】JS基础知识总结
1 变量类型和计算 1.1 值类型和引用类型的区别? 值类型:每个变量都会存储各自的值.不会相互影响 引用类型:不同变量的指针执行了同一个对象(数组,对象,函数) 1.2 typeof可以及检测的数据 ...
- 安装Fedora后
更新操作系统版本: https://fedoraproject.org/wiki/DNF_system_upgrade 靠谱: 设置ssh:ssh生成公钥私钥.默认root(.ssh/confi ...
- Chocolatey初体验
新电脑安装Nodejs时发现安装包提示是否自动安装Chocolatey,之前没看到过这个名词,于是搜索了下,发现Chocolatey是Windows平台的包管理工具,类似于Linux的yum/apt- ...