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

我在最近的学习中和优锐课老师谈到了这个问题。现在,是时候以书面形式进行解释了。线程安全是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类的线程安全性?的更多相关文章

  1. 我是怎样测试Java类的线程安全性的

    线程安全性是Java等语言/平台中类的一个重要标准,在Java中,我们经常在线程之间共享对象.由于缺乏线程安全性而导致的问题很难调试,因为它们是偶发的,而且几乎不可能有目的地重现.如何测试对象以确保它 ...

  2. 测试 Java 类的非公有成员变量和方法

    引言 对于软件开发人员来说,单元测试是一项必不可少的工作.它既可以验证程序的有效性,又可以在程序出现 BUG 的时候,帮助开发人员快速的定位问题所在.但是,在写单元测试的过程中,开发人员经常要访问类的 ...

  3. Java 并发基础——线程安全性

    当线程安全:多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协调,这个类都能表现出正确的行为,那么久称这个类是线程安全的. 在线程 ...

  4. java容器的线程安全性

    参考:https://www.cnblogs.com/yjd_hycf_space/p/7760248.html 线程安全的: Vector HashTable StringBuffer 线程不安全的 ...

  5. 【基础】java类的各种成员初始化顺序

    父子类继承时的静态代码块,普通代码块,静态方法,构造方法,等先后顺序 前言: 普通代码块:在方法或语句中出现的{}就称为普通代码块.普通代码块和一般的语句执行顺序由他们在代码中出现的次序决定--“先出 ...

  6. jmeter测试java代码

    有时候总是要写代码的,不得不说你也得会,这不往下看 java请求了,就的写代码,那么先来实现一个类, package com.company.jemeters; public class Hello ...

  7. 刨根问底系列(3)——关于socket api的原子操作性和线程安全性的探究和实验测试(多线程同时send,write)

    多个线程对同一socket同时进行send操作的结果 1. 概览 1.1 起因 自己写的项目里,为了保证连接不中断,我起一个线程专门发送心跳包保持连接,那这个线程在send发送数据时,可能会与主线程中 ...

  8. Java并发编程实战 之 线程安全性

    1.什么是线程安全性 当多个线程访问某个类时,不管运行时环境采用何种调用方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全 ...

  9. Java基础-继承-编写一个Java应用程序,设计一个汽车类Vehicle,包含的属性有车轮个数 wheels和车重weight。小车类Car是Vehicle的子类,其中包含的属性有载人数 loader。卡车类Truck是Car类的子类,其中包含的属性有载重量payload。每个 类都有构造方法和输出相关数据的方法。最后,写一个测试类来测试这些类的功 能。

    #29.编写一个Java应用程序,设计一个汽车类Vehicle,包含的属性有车轮个数 wheels和车重weight.小车类Car是Vehicle的子类,其中包含的属性有载人数 loader.卡车类T ...

随机推荐

  1. 分组取topN

    假设有这样一个文件,文件内容如下 class1 class2 class1 class1 class2 class2 class1 class2 class1 class2 要求按照班级分组取出每个班 ...

  2. 🔥「课代表」帮你总结了全网最全的Redis知识点

    你知道的越多,你不知道的越多 点赞再看,养成习惯 GitHub上已经开源 https://github.com/JavaFamily 有一线大厂面试点脑图.个人联系方式和人才交流群,欢迎Star和指教 ...

  3. 通过myclipse建立一个简单的Hibernate项目(PS:在单元测试中实现数据的向表的插入)

    Hibernate的主要功能及用法: Ⅰ.Hibernate封装了JDBC,使Java程序员能够以面向对象的思想对数据库进行操作 Ⅱ.Hibernate可以应用于EJB的J2EE架构,完成数据的持久化 ...

  4. pat 1015 Reversible Primes(20 分)

    1015 Reversible Primes(20 分) A reversible prime in any number system is a prime whose "reverse& ...

  5. nyoj 53-不高兴的小明 (遍历)

    53-不高兴的小明 内存限制:64MB 时间限制:3000ms Special Judge: No accepted:28 submit:89 题目描述:    小明又出问题了.妈妈认为聪明的小明应该 ...

  6. Docker从入门到实践(2)

    二.基本概念 Docker 镜像 我们都知道,操作系统分为内核和用户空间.对于 Linux 而言,内核启动后,会挂载 root 文件系统为其提供用户空间支持.而 Docker 镜像(Image),就相 ...

  7. opencv 2 Opencv数据结构与基本绘图

    基础图像容器Mat Mat 是一个类,又两个数据部分组成:矩阵头(包含矩阵尺寸,存储方法,存储地址等信息)和一个指向存储所有像素值的矩阵(根据所选存储方法不同,矩阵可以是不同的维数)的指针.矩阵头的尺 ...

  8. 关闭Eslint检查

    Eslint是一个JavaScript的校验插件,通常用来校验语法或代码的书写风格.不过有点过于严格,通常缩减不同也会报错... 这会影响工作效率啊,最好就是在创建项目的时候不要ESlint(选择No ...

  9. 达梦"记录超长"警告

    出现"记录超长"背景介绍: 导入数据库时,出现数据库记录超长警告,导致数据无法正常导入! 1.重新建库,把页大小改大 这种方式是在建立数据库实例的时候进行的 修改[页大小] 2.把 ...

  10. ViewPage+Fragment的使用用法

    一.概述 从前面几篇文章,我们知道,实现ViewPager是要有适配器的,我们前面用的适配器是PagerAdapter,而对于fragment,它所使用的适配器是:FragmentPagerAdapt ...