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

这篇文章主要介绍了如何测试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()中同步添加就可以了。或者,聪明的码小伙伴们,你们有更好的方案吗?欢迎留言,大家一起讨论。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Java局部变量线程安全原理分析

    这篇文章主要介绍了Java局部变量线程安全原理分析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 方法调用栈结构: 每个线程都有自己独立的方法调用栈: 这种局部变量不共享,从而保证线程安全的技术,称为线程封闭技术. 案例:数据库连接池.采用线程封闭技术,线程获取的数据库连接connection,是独立的,在这个线程在关闭获取的这个connection之前,不会再分配给其他线程. 思考:递归调用太深,可能导致栈溢出. 栈溢出原因: 因为每调用一个

  • Java多线程模拟售票程序和线程安全问题

    Java中线程部分知识中,售票程序非常经典.程序中也有一些问题存在! 需求:模拟3个窗口同时在售100张票. 问题1:为什么100张票被卖出了300张票? 原因:因为tickets是非静态的,非静态的成员变量数据是在每个对象中都会维护一份数据的,三个线程对象就会有三份. 解决方案:把tickets票数共享出来给三个线程对象使用.使用static修饰. 问题2: 出现了线程安全问题 ? 线程安全问题的解决方案:sun提供了线程同步机制让我们解决这类问题的. java线程同步机制的方式: 方式一:同

  • Java线程之程安全与不安全代码示例

    作为一个Java web开发人员,很少也不需要去处理线程,因为服务器已经帮我们处理好了.记得大一刚学Java的时候,老师带着我们做了一个局域网聊天室,用到了AWT.Socket.多线程.I/O,编写的客户端和服务器,当时做出来很兴奋,回学校给同学们演示,感觉自己好NB,呵呵,扯远了.上次在百度开发者大会上看到一个提示语,自己写的代码,6个月不看也是别人的代码,自己学的知识也同样如此,学完的知识如果不使用或者不常常回顾,那么还不是自己的知识.大学零零散散搞了不到四年的Java,我相信很多人都跟我一

  • Java线程安全的计数器简单实现代码示例

    前几天工作中一段业务代码需要一个变量每天从1开始递增.为此自己简单的封装了一个线程安全的计数器,可以让一个变量每天从1开始递增.当然了,如果项目在运行中发生重启,即便日期还是当天,还是会从1开始重新计数.所以把计数器的值存储在数据库中会更靠谱,不过这不影响这段代码的价值,现在贴出来,供有需要的人参考. package com.hikvision.cms.rvs.common.util; import java.text.SimpleDateFormat; import java.util.Arr

  • 完美解决Java中的线程安全问题

    给出一个问题,如下: 解决方案如下: public class Demo_5 { public static void main(String[] args) { //创建一个窗口 TicketWindow tw1=new TicketWindow(); //使用三个线程同时启动 Thread t1=new Thread(tw1); Thread t2=new Thread(tw1); Thread t3=new Thread(tw1); t1.start(); t2.start(); t3.s

  • Java线程安全与非线程安全解析

    ArrayList和Vector有什么区别?HashMap和HashTable有什么区别?StringBuilder和StringBuffer有什么区别?这些都是Java面试中常见的基础问题.面对这样的问题,回答是:ArrayList是非线程安全的,Vector是线程安全的:HashMap是非线程安全的,HashTable是线程安全的:StringBuilder是非线程安全的,StringBuffer是线程安全的.因为这是昨晚刚背的<Java面试题大全>上面写的.此时如果继续问:什么是线程安全

  • Java多线程编程安全退出线程方法介绍

    线程停止 Thread提供了一个stop()方法,但是stop()方法是一个被废弃的方法.为什么stop()方法被废弃而不被使用呢?原因是stop()方法太过于暴力,会强行把执行一半的线程终止.这样会就不会保证线程的资源正确释放,通常是没有给与线程完成资源释放工作的机会,因此会导致程序工作在不确定的状态下 那我们该使用什么来停止线程呢 Thread.interrupt(),我们可以用他来停止线程,他是安全的,可是使用他的时候并不会真的停止了线程,只是会给线程打上了一个记号,至于这个记号有什么用呢

  • java安全停止线程的方法详解

    Thread.stop()是一个被废弃的方法,不被推荐使用的原因是stop方法太过于暴力,强行把执行到一半的线程终止,并且会立即释放这个线程所有的锁.会破坏了线程中引用对象的一致性. 使用判断标志位的方法中断线程 interrupt() //线程中断 (标志位设置为true) isInterrupted() //判断是否被中断 interrupted() //判断是否中断,并清除当前中断状态(标志位改为false) public static class TestThread extends T

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

    这篇文章主要介绍了如何测试Java类的线程安全性,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 线程安全性是Java等语言/平台中类的一个重要标准,在Java中,我们经常在线程之间共享对象.由于缺乏线程安全性而导致的问题很难调试,因为它们是偶发的,而且几乎不可能有目的地重现.如何测试对象以确保它们是线程安全的? 假如有一个内存书架 package com.mzc.common.thread; import java.util.Map; impo

  • Java并发编程之线程安全性

    目录 1.什么是线程安全性 2.原子性 2.1 竞争条件 2.2 复合操作 3.加锁机制 3.1 内置锁 3.2 重入 4.用锁保护状态 5.活跃性与性能 1.什么是线程安全性 当多个线程访问某个类时,不管运行时环境采用何种调用方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的. 无状态的对象一定是线程安全的,比如:Servlet. 2.原子性 2.1 竞争条件 由于不恰当的执行时序而出现不正确的结果的情况,就是竞争

  • java 可重启线程及线程池类的设计(详解)

    了解JAVA多线程编程的人都知道,要产生一个线程有两种方法,一是类直接继承Thread类并实现其run()方法:二是类实现Runnable接口并实现其run()方法,然后新建一个以该类为构造方法参数的Thread,类似于如下形式: Thread t=new Thread(myRunnable).而最终使线程启动都是执行Thread类的start()方法. 在JAVA中,一个线程一旦运行完毕,即执行完其run()方法,就不可以重新启动了.此时这个线程对象也便成了无用对象,等待垃圾回收器的回收.下次

  • Java Atomic类及线程同步新机制原理解析

    一.为什么要使用Atomic类? 看一下下面这个小程序,模拟计数,创建10个线程,共同访问这个int count = 0 :每个线程给count往上加10000,这个时候你需要加锁,如果不加锁会出现线程安全问题,但是使用AtomicInteger之后就不用再做加锁的操作了,因为AtomicInteger内部使用了CAS操作,直接无锁往上递增,有人会问问什么会出现无锁操作,答案只有一个:那就是快呗: 下面是AtomicInteger的使用方法: package com.example.demo.t

  • JAVA多线程线程安全性基础

    目录 线程安全性 什么是线程安全的代码 什么是线程安全性 总结 线程安全性 一个对象是否需要是线程安全的,取决于它是否被多个线程访问,而不取决于对象要实现的功能 什么是线程安全的代码 核心:对 共享的 和 可变的 状态的访问进行管理.防止对数据发生不受控的并发访问. 何为对象的状态? 状态是指存储在对象的状态变量(例如实例或静态域)中的数据.还可能包括 其他依赖对象 的域. eg:某个HashMap的状态不仅存储在HashMap对象本身,还存储在许多Map.Entry对象中. 总而言之,在对象的

  • java多线程编程之使用thread类创建线程

    在Java中创建线程有两种方法:使用Thread类和使用Runnable接口.在使用Runnable接口时需要建立一个Thread实例.因此,无论是通过Thread类还是Runnable接口建立线程,都必须建立Thread类或它的子类的实例.Thread类的构造方法被重载了八次,构造方法如下: 复制代码 代码如下: public Thread( );public Thread(Runnable target);public Thread(String name);public Thread(Ru

  • java线程之用Thread类创建线程的方法

    在Java中创建线程有两种方法:使用Thread类和使用Runnable接口.在使用Runnable接口时需要建立一个Thread实例.因此,无论是通过Thread类还是Runnable接口建立线程,都必须建立Thread类或它的子类的实例.Thread类的构造方法被重载了八次,构造方法如下: 复制代码 代码如下: public Thread( ); public Thread(Runnable target); public Thread(String name); public Thread

  • Java 集合中的类关于线程安全

    Java集合中那些类是线程安全的 线程安全类 在集合框架中,有些类是线程安全的,这些都是jdk1.1中的出现的.在jdk1.2之后,就出现许许多多非线程安全的类. 下面是这些线程安全的同步的类: vector:就比arraylist多了个同步化机制(线程安全),因为效率较低,现在已经不太建议使用.在web应用中,特别是前台页面,往往效率(页面响应速度)是优先考虑的. statck:堆栈类,先进后出 hashtable:就比hashmap多了个线程安全 enumeration:枚举,相当于迭代器

  • Java继承Thread类创建线程类示例

    本文实例讲述了Java继承Thread类创建线程类.分享给大家供大家参考,具体如下: 一 点睛 通过继承Thread类创建线程并启动多线程的步骤: 1 定义Thread的子类,并重写该类的run()方法,该run()方法的方法体代表了线程需要完成的任务.因此run()方法称为线程执行体. 2 创建Thread子类的实例,即创建子线程对象. 3 调用线程对象的start()方法来启动该线程. 二 代码 // 通过继承Thread类来创建线程类 public class FirstThread ex

  • java高并发ThreadPoolExecutor类解析线程池执行流程

    目录 摘要 核心逻辑概述 execute(Runnable)方法 addWorker(Runnable, boolean)方法 addWorkerFailed(Worker)方法 拒绝策略 摘要 ThreadPoolExecutor是Java线程池中最核心的类之一,它能够保证线程池按照正常的业务逻辑执行任务,并通过原子方式更新线程池每个阶段的状态. 今天,我们通过ThreadPoolExecutor类的源码深度解析线程池执行任务的核心流程,小伙伴们最好是打开IDEA,按照冰河说的步骤,调试下Th

随机推荐