如何在Java中测试类是否是线程安全的

时间:2022-08-07 18:57:02

通过优锐课的java核心笔记中,我们可以看到关于如何在java中测试类是否线程安全的一些知识点汇总,分享给大家学习参考。

线程安全性测试与典型的单线程测试不同。为了测试一个方法是否是线程安全的,我们需要从多个线程中并行调用该方法。我们需要对所有潜在的线程交织进行此操作。然后,我们需要检查结果是否正确。

这三个测试要求导致了一种特殊的线程安全测试,该测试不同于典型的单线程测试。由于我们要测试所有线程交错,因此我们的测试必须是可重复的并自动运行。而且由于这些方法并行运行,因此潜在的结果是不同结果的组合。

让我们看一个例子,看看实际情况。

测试线程安全

假设我们要测试表示地址的以下类是否是线程安全的。它提供一种更新街道和城市的方法,一种方法更新以及一种读取完整地址的方法,该方法是   toString:

public class MutableAddress {

    private volatile String street;

    private volatile String city;

    private volatile String phoneNumber;

    public MutableAddress(String street, String city,

        String phoneNumber) {

        this.street = street;

        this.city = city;

        this.phoneNumber = phoneNumber;

    }

    public void update(String street ,String city ) {

        this.street = street;

        this.city = city;

    }

    public String toString() {

        return "street=" + street + ",city=" + city + ",

        phoneNumber=" + phoneNumber;

    }

}

  

我在第2行到第4行中使用了volatile字段,以确保线程始终看到当前值,如此处更详细地解释。你可以从GitHub下载所有示例的源代码。

现在,让我们首先看看toStringand和update 的组合   是否是线程安全的。这是测试:

import com.vmlens.api.AllInterleavings;

public class TestToStringAndUpdate {

    @Test

    public void testMutableAddress() throws InterruptedException {

        try (AllInterleavings allInterleavings =

            new AllInterleavings("TestToStringAndUpdate_Not_Thread_Safe");) {

            while (allInterleavings.hasNext()) {

                MutableAddress address = new MutableAddress("E. Bonanza St.",

                     "South Park", "456 77 99");

                String readAddress = null;

                Thread first = new Thread(() -> {

                    address.update("Evergreen Terrace", "Springfield");

                });

                first.start();

                readAddress = address.toString();

                first.join();

                assertTrue("readAddress:" + readAddress,readAddress.equals(

        "street=E. Bonanza St.,city=South Park,phoneNumber=456 77 99")

                    || readAddress.equals(

        "street=Evergreen Terrace,city=Springfield,phoneNumber=456 77 99"));

            }

        }

}

  

该测试从两个线程并行执行两个方法。为了测试所有线程交织,我们使用来自vmlens的第7行的AllInterleavings类,将完整的测试放在while循环中,对所有线程交织进行迭代。要查看该类是否线程安全,我们将结果与潜在结果进行比较,更新前和更新后第17至20行的值。

运行测试会导致以下错误:

java.lang.AssertionError: readAddress:street=Evergreen Terrace

        ,city=South Park,phoneNumber=456 77 99

    at com.vmlens.tutorialCopyOnWrite.TestToStringAndUpdate.

        testMutableAddress(TestToStringAndUpdate.java:22)

  

问题在于,对于其中一个线程与线程ID为30的线程进行交织,首先更新街道名称,然后主线程(线程ID 1)读取街道和城市名称。因此,主线程读取了导致错误的部分更新地址。

为了使地址类具有线程安全性,我们在每次更新地址时都会复制地址值。这是使用此技术的线程安全实现。它由两个类组成:一个不变值和一个可变容器。

首先,不变值类:

public class AddressValue {

    private final String street;

    private final String city;

    private final String phoneNumber;

    public AddressValue(String street, String city,

                String phoneNumber) {

        super();

        this.street = street;

        this.city = city;

        this.phoneNumber = phoneNumber;

    }

    public String getStreet() {

        return street;

    }

    public String getCity() {

        return city;

    }

    public String getPhoneNumber() {

        return phoneNumber;

    }

}

  

其次是可变容器类:

public class AddressUsingCopyOnWrite {

    private volatile AddressValue addressValue;

    private final Object LOCK = new Object();

    public AddressUsingCopyOnWrite(String street,

            String city, String phone) {

        this.addressValue = new AddressValue( street,

                city,  phone);

    }

    public void update(String street ,String city ) {

        synchronized(LOCK){

            addressValue = new AddressValue(  street,  city,  

                    addressValue.getPhoneNumber() );

        }

    }

    public String toString() {

        AddressValue local = addressValue;

        return "street=" + local.getStreet()

        + ",city=" +    local.getCity() +

        ",phoneNumber=" + local.getPhoneNumber();

    }

}

  

AddressUsingCopyOnWrite每次更新变量时,该类   都会创建一个新的地址值   addressValue。这样可以确保我们始终读取一致的地址,无论是更新前后的值。

如果我们使用这两个类运行测试,则测试成功。

我们需要测试什么?

到目前为止,我们测试了的组合toString和   update线程安全性。为了测试一个类是否是线程安全的,我们需要测试所有修改方法组合以及只读方法与修改方法的所有组合。因此,对于我们的示例类,我们需要测试以下两种组合:

  1. update 和 update
  2. toString 和 update

由于只读方法的组合是自动线程安全的,因此我们不需要测试toString方法与其自身的组合。

数据竞赛

到目前为止,我们使用易失性字段来避免数据争用。让我们看看当我们使用普通字段时会发生什么。因此,在我们的线程安全类中   AddressUsingCopyOnWrite,我们删除了volatile修饰符并重新运行测试。现在,vmlens在文件target / interleave / issues.html中报告数据争用

数据争用是对线程可能读取陈旧值的字段的访问。如果线程确实读取了一个过时的值,则取决于外部因素,例如编译器正在使用的优化,JVM正在运行的硬件体系结构以及线程正在哪个内核上运行。为了使始终能够检测到与那些外部因素无关的数据竞争,vmlens在测试运行的执行跟踪中搜索数据竞争。如果vmlens在示例中找到了一个,它将在问题报告中报告它们。

摘要

线程安全性测试与典型的单线程测试不同。要测试两种方法$$ anonymous $$ nd b的组合是否是线程安全的,请从两个不同的线程中调用它们。放在一个while循环迭代完整的测试了从类帮助所有线程的交错  AllInterleavings来自vmlens。测试结果是b之后还是a之后b。为了测试一个类是否是线程安全的,请测试修改方法的所有组合以及只读方法和修改方法的所有组合。

> 喜欢这篇文章的可以点个赞,欢迎大家留言评论,记得关注我,每天持续更新技术干货、职场趣事、海量面试资料等等
 > 如果你对java技术很感兴趣也可以交流学习,共同学习进步。 
> 不要再用"没有时间“来掩饰自己思想上的懒惰!趁年轻,使劲拼,给未来的自己一个交代

文章写道这里,欢迎完善交流。最后奉上近期整理出来的一套完整的java架构思维导图,分享给大家对照知识点参考学习。有更多JVM、Mysql、Tomcat、Spring Boot、Spring Cloud、Zookeeper、Kafka、RabbitMQ、RockerMQ、Redis、ELK、Git等Java干货

如何在Java中测试类是否是线程安全的