我是怎样测试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 Stream函数式编程第三篇:管道流结果处理
一.Java Stream管道数据处理操作 在本号之前写过的文章中,曾经给大家介绍过 Java Stream管道流是用于简化集合类元素处理的java API.在使用的过程中分为三个阶段.在开始本文之前 ...
- python学习之【第十七篇】:Python中的面向对象(类和对象)
1.什么是类和类的对象? 类是一种数据结构,我们可以用它来定义对象,后者把数据值和行为特性融合在一起,类是现实世界的抽象的实体以编程形式出现.实例是这些对象的具体化.类是用来描述一类事物,类的对象指的 ...
- introduce new products
Today's the day. I'm giving you the heads up. Our company is rolling up its new line of cell phones. ...
- inline-block元素间隙产生及去除方法
当我们把元素属性display设置成inline-block时,元素之间就会产生间隙 <!DOCTYPE html><html> <head> <meta c ...
- C#同级catch块和finally块中全都抛出异常,上一级捕获哪一个?
C#同级catch块和finally块中全都抛出异常,上一级优先捕获finally块中的异常. 测试代码: using System; namespace test { class Program { ...
- 瞎折腾实录:构建 Armel 版本的 .NET Core 教程和资料资源
目录 首先我要说明,我失败了~ 我把我的进度和经验放出来,希望能够帮助别人完成编译工作~ 背景:最近接手一个华为某型号的嵌入式设备,需要在上面搭建 .NET Core 环境. 设备是 Armel 架构 ...
- MySQL-配置环境变量及修改密码(附-mysql安装教程)
MySQL-配置环境变量和修改密码 mysql的安装教程:链接:https://pan.baidu.com/s/1rrPT2X0yRF58kN8jZZx-Mg 密码:55dh 一. 闪退问题 1.1. ...
- Pandas进阶笔记 (一) Groupby 重难点总结
如果Pandas只是能把一些数据变成 dataframe 这样优美的格式,那么Pandas绝不会成为叱咤风云的数据分析中心组件.因为在数据分析过程中,描述数据是通过一些列的统计指标实现的,分析结果也需 ...
- JDBD连接MySQL中的驱动与时区问题
1.在进行jdbc与mysql连接的时候应注意,加载驱动的方式根据MySQL版本内容来说 有变化 5.7版本之前: String driver= "com.mysql.jdbc.Driver ...
- Spring框架学习笔记(7)——Spring Boot 实现上传和下载
最近忙着都没时间写博客了,做了个项目,实现了下载功能,没用到上传,写这篇文章也是顺便参考学习了如何实现上传,上传和下载做一篇笔记吧 下载 主要有下面的两种方式: 通过ResponseEntity实现 ...