线程安全性是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类的线程安全性的的更多相关文章

  1. 分享和探讨——如何测试Java类的线程安全性?

    缺乏线程安全性导致的问题很难调试,因为它们是零星的,几乎不可能有意复制.你如何测试对象以确保它们是线程安全的? 我在最近的学习中和优锐课老师谈到了这个问题.现在,是时候以书面形式进行解释了.线程安全是 ...

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

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

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

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

  4. java容器的线程安全性

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

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

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

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

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

  7. jmeter测试java代码

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

  8. Java中各种集合(字符串类)的线程安全性!!!

    Java中各种集合(字符串类)的线程安全性!!! 一.概念: 线程安全:就是当多线程访问时,采用了加锁的机制:即当一个线程访问该类的某个数据时,会对这个数据进行保护,其他线程不能对其访问,直到该线程读 ...

  9. Java并发编程实战 第2章 线程安全性

    编写线程安全的 代码,核心在与对共享的和可变的对象的状态的访问. 如果多个线程访问一个可变的对象时没有使用同步,那么就会出现错误.在这种情况下,有3中方式可以修复这个问题: 不在线程之间共享该状态变量 ...

随机推荐

  1. Netty连接处理那些事

    编者注:Netty是Java领域有名的开源网络库,特点是高性能和高扩展性,因此很多流行的框架都是基于它来构建的,比如我们熟知的Dubbo.Rocketmq.Hadoop等,针对高性能RPC,一般都是基 ...

  2. linux 设置固定ip和dns

    目录 1. centos 1.1 ifconfig 查看网卡名称 1.2 设置固定ip和dns 1.3 重启网络 2. ubuntu 2.1 ifconfig 查看网卡名称 2.2 设置固定ip和dn ...

  3. Deepin 下 使用 Rider 开发 .NET Core

    Deepin 下 使用 Rider 开发 .NET Core 国产的 Deepin 不错,安利一下. Deepin 用了也有一两年,也只是玩玩,没用在开发上面.后来 Win10 不太清真了,就想着能不 ...

  4. 类型擦除真的能完全擦除一切信息吗?java 泛型揭秘

    背景 我们都知道泛型本质上是提供类型的"类型参数",它们也被称为参数化类型(parameterized type)或参量多态(parametric polymorphism).其实 ...

  5. Phone Code

    Polycarpus has n friends in Tarasov city. Polycarpus knows phone numbers of all his friends: they ar ...

  6. PHP 在Swoole中使用双IoC容器实现无污染的依赖注入

    简介: 容器(container)技术(可以理解为全局的工厂方法), 已经是现代项目的标配. 基于容器, 可以进一步实现控制反转, 依赖注入. Laravel 的巨大成功就是构建在它非常强大的IoC容 ...

  7. pycharm设置python脚本模板

    PyCharm PyCharm是一个有名的Python IDE,带有一整套可以帮助用户在使用Python语言开发时提高其效率的工具,比如调试.语法高亮.Project管理.代码跳转.智能提示.自动完成 ...

  8. C++学习第二天(打卡)

    C++ new 可以很方便的 分配一段内存. 比如 int *test= new int ; int n; cin>>n; int * test =new int [n]; 可以实现动态分 ...

  9. Java多线程——对象及变量的并发访问

    Java多线系列文章是Java多线程的详解介绍,对多线程还不熟悉的同学可以先去看一下我的这篇博客Java基础系列3:多线程超详细总结,这篇博客从宏观层面介绍了多线程的整体概况,接下来的几篇文章是对多线 ...

  10. NTP服务搭建详解一条龙

    说在前面:ntp和ntpdate区别 ①两个服务都是centos自带的(centos7中不自带ntp).ntp的安装包名是ntp,ntpdate的安装包是ntpdate.他们并非由一个安装包提供. ② ...