1. Introduction
1.绪论
In this tutorial, we’ll cover some of the basics of testing a concurrent program. We’ll primarily focus on thread-based concurrency and the problems it presents in testing.
在本教程中,我们将介绍一些测试并发程序的基础知识。我们将主要关注基于线程的并发,以及它在测试中带来的问题。
We’ll also understand how can we solve some of these problems and test multi-threaded code effectively in Java.
我们还将了解如何才能解决其中的一些问题,在Java中有效地测试多线程代码。
2. Concurrent Programming
2.并行编程
Concurrent programming refers to programming where we break down a large piece of computation into smaller, relatively independent computations.
并发编程指的是编程时,我们将一个大的计算分解成较小的、相对独立的计算。
The intent of this exercise is to run these smaller computations concurrently, possibly even in parallel. While there are several ways to achieve this, the objective invariably is to run the program faster.
这个练习的目的是并发地运行这些较小的计算,甚至可能是并行的。虽然有几种方法可以实现这一点,但目的无一例外都是为了更快地运行程序。
2.1. Threads and Concurrent Programming
2.1.线程和并发编程
With processors packing more cores than ever, concurrent programming is at the forefront to harness them efficiently. However, the fact remains that concurrent programs are much more difficult to design, write, test, and maintain. So if we can, after all, write effective and automated test cases for concurrent programs, we can solve a large chunk of these problems.
随着处理器的内核越来越多,并发式编程成为有效利用这些内核的最前沿。然而,事实是,并发程序的设计、编写、测试和维护要困难得多。因此,如果我们毕竟可以为并发程序编写有效的自动化测试案例,我们就可以解决这些问题中的一大块。
So, what makes writing tests for concurrent code so difficult? To understand that, we must understand how we achieve concurrency in our programs. One of the most popular concurrent programming techniques involves using threads.
那么,是什么让为并发代码编写测试如此困难?要了解这一点,我们必须了解我们如何在程序中实现并发。最流行的并发编程技术之一涉及使用线程。
Now, threads can be native, in which case they’re scheduled by the underlying operating systems. We can also use what are known as green threads, which are scheduled by a runtime directly.
现在,线程可以是原生的,在这种情况下,它们是由底层操作系统调度的。我们也可以使用所谓的绿色线程,它是由运行时直接调度的。
2.2. Difficulty in Testing Concurrent Programs
2.2.测试并发程序的困难
Irrespective of what type of threads we use, what makes them difficult to use is thread communication. If we do indeed manage to write a program that involves threads but no thread communication, there is nothing better! More realistically, threads will usually have to communicate. There are two ways to achieve this — shared memory and message passing.
不管我们使用什么类型的线程,使它们难以使用的是线程通信。如果我们确实能够写出一个涉及线程但没有线程通信的程序,那就没有什么比这更好的了!这就是线程。更现实的是,线程通常要进行通信。有两种方法可以实现这一点–共享内存和消息传递。
The bulk of the problem associated with concurrent programming arises out of using native threads with shared memory. Testing such programs is difficult for the same reasons. Multiple threads with access to shared memory generally require mutual exclusion. We typically achieve this through some guarding mechanism using locks.
与并发编程相关的大部分问题产生于使用共享内存的本地线程。由于同样的原因,测试这类程序也很困难。访问共享内存的多个线程通常需要相互排斥。我们通常通过使用锁的一些保护机制来实现这一点。
But this may still lead to a host of problems like race conditions, live locks, deadlocks, and thread starvation, to name a few. Moreover, these problems are intermittent, as thread scheduling in the case of native threads is completely non-deterministic.
但这仍然可能导致一系列问题,如竞赛条件、活锁、死锁和线程饥饿,仅举几例。此外,这些问题都是间歇性的,因为在本地线程的情况下,线程调度是完全非确定性的。
Hence, writing effective tests for concurrent programs that can detect these issues in a deterministic manner is indeed a challenge!
因此,为并发程序编写有效的测试,能够以确定的方式检测这些问题,确实是一个挑战!
2.3. Anatomy of Thread Interleaving
2.3.线程交织剖析
We know that native threads can be scheduled by operating systems unpredictably. In case these threads access and modify shared data, it gives rise to interesting thread interleaving. While some of these interleavings may be completely acceptable, others may leave the final data in an undesirable state.
我们知道,本机线程可以被操作系统不可预知地调度。在这些线程访问和修改共享数据的情况下,会产生有趣的线程交错。虽然其中一些交错可能是完全可以接受的,但其他的交错可能会使最终的数据处于不理想的状态。
Let’s take an example. Suppose we have a global counter that is incremented by every thread. By the end of processing, we’d like the state of this counter to be exactly the same as the number of threads that have executed:
让我们举个例子。假设我们有一个全局计数器,每个线程都会递增。在处理结束时,我们希望这个计数器的状态与已经执行的线程数完全相同。
private int counter;
public void increment() {
counter++;
}
Now, to increment a primitive integer in Java is not an atomic operation. It consists of reading the value, increasing it, and finally saving it. While multiple threads are doing the same operation, it may give rise to many possible interleavings:
现在,在Java中增加一个原始整数并不是一个原子操作。它包括读取数值,增加数值,最后保存数值。当多个线程在做同样的操作时,可能会产生许多可能的交织。
While this particular interleaving produces completely acceptable results, how about this one:
虽然这种特殊的交织方式产生了完全可以接受的结果,但这种方式如何呢?
This is not what we expected. Now, imagine hundreds of threads running code that’s much more complex than this. This will give rise to unimaginable ways that the threads will interleave.
这不是我们所期望的。现在,想象一下数百个线程运行比这更复杂的代码。这将产生难以想象的线程交错方式。
There are several ways to write code that avoids this problem, but that is not the subject of this tutorial. Synchronization using a lock is one of the common ones, but it has its problems related to race conditions.
有几种方法可以写出避免这个问题的代码,但这不是本教程的主题。使用锁进行同步是常见的方法之一,但它也有与竞赛条件有关的问题。
3. Testing Multi-Threaded Code
3.测试多线程代码
Now that we understand the basic challenges in testing multi-threaded code, we’ll see how to overcome them. We’ll build a simple use case and try to simulate as many problems related to concurrency as possible.
现在我们了解了测试多线程代码的基本挑战,我们将看看如何克服它们。我们将建立一个简单的用例,并尝试尽可能多地模拟与并发有关的问题。
Let’s begin by defining a simple class that keeps a count of possibly anything:
让我们首先定义一个简单的类,它可以保存一个可能是任何东西的计数。
public class MyCounter {
private int count;
public void increment() {
int temp = count;
count = temp + 1;
}
// Getter for count
}
This is a seemingly harmless piece of code, but it’s not difficult to understand that it’s not thread-safe. If we happen to write a concurrent program with this class, it’s bound to be defective. The purpose of testing here is to identify such defects.
这是一段看似无害的代码,但不难理解,它不是线程安全的。如果我们碰巧用这个类写了一个并发程序,它肯定会有缺陷。这里测试的目的就是要找出这种缺陷。
3.1. Testing Non-Concurrent Parts
3.1.测试非同期零件
As a rule of thumb, it’s always advisable to test code by isolating it from any concurrent behavior. This is to reasonably ascertain that there’s no other defect in the code that isn’t related to concurrency. Let’s see how can we do that:
作为一个经验法则,测试代码时最好将其与任何并发行为隔离开来。这是为了合理地确定代码中没有其他与并发行为无关的缺陷。让我们看看如何才能做到这一点。
@Test
public void testCounter() {
MyCounter counter = new MyCounter();
for (int i = 0; i < 500; i++) {
counter.increment();
}
assertEquals(500, counter.getCount());
}
While there’s nothing much going here, this test gives us the confidence that it works at least in the absence of concurrency.
虽然这里没有什么进展,但这个测试给了我们信心,至少在没有并发的情况下,它是有效的。
3.2. First Attempt at Testing With Concurrency
3.2.首次尝试使用并发性测试
Let’s move on to test the same code again, this time in a concurrent setup. We’ll try to access the same instance of this class with multiple threads and see how it behaves:
让我们继续测试同样的代码,这次是在一个并发设置中。我们将尝试用多个线程访问这个类的同一个实例,看看它的表现如何。
@Test
public void testCounterWithConcurrency() throws InterruptedException {
int numberOfThreads = 10;
ExecutorService service = Executors.newFixedThreadPool(10);
CountDownLatch latch = new CountDownLatch(numberOfThreads);
MyCounter counter = new MyCounter();
for (int i = 0; i < numberOfThreads; i++) {
service.execute(() -> {
counter.increment();
latch.countDown();
});
}
latch.await();
assertEquals(numberOfThreads, counter.getCount());
}
This test is reasonable, as we’re trying to operate on shared data with several threads. As we keep the number of threads low, like 10, we will notice that it passes almost all the time. Interestingly, if we start increasing the number of threads, say to 100, we will see that the test starts to fail most of the time.
这个测试是合理的,因为我们正试图用几个线程来操作共享数据。当我们保持较低的线程数,比如10个,我们会注意到它几乎每次都能通过。有趣的是,如果我们开始增加线程的数量,比如说增加到100个,我们会发现测试开始在大多数时候失败。
3.3. A Better Attempt at Testing With Concurrency
3.3.使用并发性测试的更好尝试
While the previous test did reveal that our code isn’t thread-safe, there’s a problem with this test. This test isn’t deterministic because the underlying threads interleave in a non-deterministic manner. We really can’t rely on this test for our program.
虽然之前的测试确实揭示了我们的代码不是线程安全的,但这个测试有一个问题。这个测试不是确定性的,因为底层线程是以非确定性的方式交错的。对于我们的程序,我们真的不能依赖这个测试。
What we need is a way to control the interleaving of threads so that we can reveal concurrency issues in a deterministic manner with much fewer threads. We’ll begin by tweaking the code we are testing a little bit:
我们需要的是一种控制线程交错的方法,这样我们就可以用更少的线程以一种确定的方式揭示并发性问题。我们将首先对我们正在测试的代码进行一些调整。
public synchronized void increment() throws InterruptedException {
int temp = count;
wait(100);
count = temp + 1;
}
Here, we’ve made the method synchronized and introduced a wait between the two steps within the method. The synchronized keyword ensures that only one thread can modify the count variable at a time, and the wait introduces a delay between each thread execution.
在这里,我们使方法synchronized,并在方法的两个步骤之间引入了一个等待。synchronized关键字确保每次只有一个线程可以修改count变量,而等待则在每个线程执行之间引入了一个延迟。
Please note that we don’t necessarily have to modify the code we intend to test. However, since there aren’t many ways we can affect thread scheduling, we’re resorting to this.
请注意,我们不一定要修改我们打算测试的代码。但是,由于我们能影响线程调度的方法不多,所以我们要采用这种方式。
In a later section, we’ll see how we can do this without altering the code.
在后面的章节中,我们将看到我们如何在不改变代码的情况下做到这一点。
Now, let’s similarly test this code as we did earlier:
现在,让我们像先前那样类似地测试一下这段代码。
@Test
public void testSummationWithConcurrency() throws InterruptedException {
int numberOfThreads = 2;
ExecutorService service = Executors.newFixedThreadPool(10);
CountDownLatch latch = new CountDownLatch(numberOfThreads);
MyCounter counter = new MyCounter();
for (int i = 0; i < numberOfThreads; i++) {
service.submit(() -> {
try {
counter.increment();
} catch (InterruptedException e) {
// Handle exception
}
latch.countDown();
});
}
latch.await();
assertEquals(numberOfThreads, counter.getCount());
}
Here, we’re running this just with just two threads, and the chances are that we’ll be able to get the defect we’ve been missing. What we’ve done here is to try achieving a specific thread interleaving, which we know can affect us. While good for the demonstration, we may not find this useful for practical purposes.
在这里,我们只用两个线程来运行这个,有可能我们会得到我们一直缺少的缺陷。我们在这里所做的是尝试实现特定的线程交错,我们知道这可能会影响我们。虽然对演示来说是好事,但我们可能会发现这对实际的目的没有用处。
4. Testing Tools Available
4.可用的测试工具
As the number of threads grows, the possible number of ways they may interleave grows exponentially. It’s just not possible to figure out all such interleavings and test for them. We have to rely on tools to undertake the same or similar effort for us. Fortunately, there are a couple of them available to make our lives easier.
随着线程数量的增加,它们可能的交错方式也呈指数级增长。要弄清所有这些交错方式并对其进行测试是不可能的。我们必须依靠工具来为我们进行同样或类似的工作。幸运的是,有几个工具可以让我们的生活更轻松。
There are two broad categories of tools available to us for testing concurrent code. The first enables us to produce reasonably high stress on the concurrent code with many threads. Stress increases the likelihood of rare interleaving and, thus, increases our chances of finding defects.
有两大类工具可供我们用来测试并发代码。第一类是使我们能够对有许多线程的并发代码产生合理的高压力。压力增加了罕见的交织的可能性,因此,增加了我们发现缺陷的机会。
The second enables us to simulate specific thread interleaving, thereby helping us find defects with more certainty.
第二种使我们能够模拟特定的线程交错,从而帮助我们更有把握地发现缺陷。
4.1. tempus-fugit
4.1. Tempus-fugit
The tempus-fugit Java library helps us to write and test concurrent code with ease. We’ll just focus on the test part of this library here. We saw earlier that producing stress on code with multiple threads increases the chances of finding defects related to concurrency.
tempus-fugitJava库可以帮助我们轻松地编写和测试并发代码。我们在这里只关注这个库的测试部分。我们在前面看到,对具有多线程的代码产生压力会增加发现与并发性有关的缺陷的机会。
While we can write utilities to produce the stress ourselves, tempus-fugit provides convenient ways to achieve the same.
虽然我们可以自己编写实用程序来产生压力,但tempus-fugit提供了方便的方法来实现同样的目的。
Let’s revisit the same code we tried to produce stress for earlier and understand how can we achieve the same using tempus-fugit:
让我们重新审视一下我们先前试图产生压力的那段代码,了解如何使用tempus-fugit实现同样的目标。
public class MyCounterTests {
@Rule
public ConcurrentRule concurrently = new ConcurrentRule();
@Rule
public RepeatingRule rule = new RepeatingRule();
private static MyCounter counter = new MyCounter();
@Test
@Concurrent(count = 10)
@Repeating(repetition = 10)
public void runsMultipleTimes() {
counter.increment();
}
@AfterClass
public static void annotatedTestRunsMultipleTimes() throws InterruptedException {
assertEquals(counter.getCount(), 100);
}
}
Here, we are using two of the Rules available to us from tempus-fugit. These rules intercept the tests and help us apply the desired behaviors, like repetition and concurrency. So, effectively, we are repeating the operation under test ten times each from ten different threads.
在这里,我们使用tempus-fugit提供给我们的两个Rules。这些规则拦截测试,帮助我们应用所需的行为,如重复和并发。因此,实际上,我们正在从十个不同的线程中重复测试的操作十次。
As we increase the repetition and concurrency, our chances of detecting defects related to concurrency will increase.
当我们增加重复性和并发性时,我们检测到与并发性有关的缺陷的机会就会增加。
4.2. Thread Weaver
4.2.螺纹织机
Thread Weaver is essentially a Java framework for testing multi-threaded code. We’ve seen previously that thread interleaving is quite unpredictable, and hence, we may never find certain defects through regular tests. What we effectively need is a way to control the interleaves and test all possible interleaving. This has proven to be quite a complex task in our previous attempt.
Thread Weaver本质上是一个用于测试多线程代码的Java框架。我们之前已经看到,线程交织是相当不可预测的,因此,我们可能永远无法通过常规测试发现某些缺陷。我们实际上需要的是一种控制交织的方法,并测试所有可能的交织。在我们之前的尝试中,这已经被证明是一项相当复杂的任务。
Let’s see how Thread Weaver can help us here. Thread Weaver allows us to interleave the execution of two separate threads in a large number of ways, without having to worry about how. It also gives us the possibility of having fine-grained control over how we want the threads to interleave.
让我们看看Thread Weaver在这里如何帮助我们。Thread Weaver允许我们以大量的方式交错执行两个独立的线程,而不必担心如何交错。它还为我们提供了对线程交错方式进行细粒度控制的可能性。
Let’s see how can we improve upon our previous, naive attempt:
让我们看看如何能在我们以前的天真尝试的基础上有所改进。
public class MyCounterTests {
private MyCounter counter;
@ThreadedBefore
public void before() {
counter = new MyCounter();
}
@ThreadedMain
public void mainThread() {
counter.increment();
}
@ThreadedSecondary
public void secondThread() {
counter.increment();
}
@ThreadedAfter
public void after() {
assertEquals(2, counter.getCount());
}
@Test
public void testCounter() {
new AnnotatedTestRunner().runTests(this.getClass(), MyCounter.class);
}
}
Here, we’ve defined two threads that try to increment our counter. Thread Weaver will try to run this test with these threads in all possible interleaving scenarios. Possibly in one of the interleaves, we will get the defect, which is quite obvious in our code.
在这里,我们定义了两个线程,试图增加我们的计数器。Thread Weaver将尝试用这些线程在所有可能的交织情况下运行这个测试。可能在其中一个交织场景中,我们会得到缺陷,这在我们的代码中是很明显的。
4.3. MultithreadedTC
4.3.多线程的TC
MultithreadedTC is yet another framework for testing concurrent applications. It features a metronome that is used to provide fine control over the sequence of activities in multiple threads. It supports test cases that exercise a specific interleaving of threads. Hence, we should ideally be able to test every significant interleaving in a separate thread deterministically.
MultithreadedTC是另一个用于测试并发应用程序的框架。它有一个节拍器,用于对多个线程中的活动顺序进行精细控制。它支持行使特定线程交错的测试案例。因此,我们最好能够在一个单独的线程中确定性地测试每一个重要的交织。
Now, a complete introduction to this feature-rich library is beyond the scope of this tutorial. But, we can certainly see how to quickly set up tests that provide us the possible interleavings between executing threads.
现在,对这个功能丰富的库的完整介绍已经超出了本教程的范围。但是,我们当然可以看到如何快速设置测试,为我们提供执行线程之间的可能交织。
Let’s see how can we test our code more deterministically with MultithreadedTC:
让我们看看如何用多线程TC更确定地测试我们的代码。
public class MyTests extends MultithreadedTestCase {
private MyCounter counter;
@Override
public void initialize() {
counter = new MyCounter();
}
public void thread1() throws InterruptedException {
counter.increment();
}
public void thread2() throws InterruptedException {
counter.increment();
}
@Override
public void finish() {
assertEquals(2, counter.getCount());
}
@Test
public void testCounter() throws Throwable {
TestFramework.runManyTimes(new MyTests(), 1000);
}
}
Here, we are setting up two threads to operate on the shared counter and increment it. We’ve configured MultithreadedTC to execute this test with these threads for up to a thousand different interleavings until it detects one which fails.
在这里,我们设置了两个线程来操作共享计数器并使其增量。我们配置了多线程TC来执行这个测试,这些线程可以进行多达一千次不同的交错操作,直到检测到其中一次失败。
4.4. Java jcstress
4.4 Javajcstress
OpenJDK maintains Code Tool Project to provide developer tools for working on the OpenJDK projects. There are several useful tools under this project, including the Java Concurrency Stress Tests (jcstress). This is being developed as an experimental harness and suite of tests to investigate the correctness of concurrency support in Java.
OpenJDK维护代码工具项目,以提供开发人员在OpenJDK项目上工作的工具。这个项目下有几个有用的工具,包括Java并发压力测试(jcstress)。这正在被开发为一个实验性的线束和测试套件,以调查Java中并发支持的正确性。
Although this is an experimental tool, we can still leverage this to analyze concurrent code and write tests to fund defects related to it. Let’s see how we can test the code that we’ve been using so far in this tutorial. The concept is pretty similar from a usage perspective:
尽管这是一个实验性的工具,我们仍然可以利用它来分析并发的代码,并编写测试来资助与之相关的缺陷。让我们看看我们如何测试本教程中到目前为止一直在使用的代码。从使用的角度来看,这个概念是非常相似的。
@JCStressTest
@Outcome(id = "1", expect = ACCEPTABLE_INTERESTING, desc = "One update lost.")
@Outcome(id = "2", expect = ACCEPTABLE, desc = "Both updates.")
@State
public class MyCounterTests {
private MyCounter counter;
@Actor
public void actor1() {
counter.increment();
}
@Actor
public void actor2() {
counter.increment();
}
@Arbiter
public void arbiter(I_Result r) {
r.r1 = counter.getCount();
}
}
Here, we’ve marked the class with an annotation State, which indicates that it holds data that is mutated by multiple threads. Also, we’re using an annotation Actor, which marks the methods that hold the actions done by different threads.
在这里,我们用注解State标记了这个类,这表明它持有被多个线程变异的数据。此外,我们还使用了一个注解Actor,该注解标记了持有不同线程所做动作的方法。
Finally, we have a method marked with an annotation Arbiter, which essentially only visits the state once all Actors have visited it. We have also used annotation Outcome to define our expectations.
最后,我们有一个标有注解Arbiter的方法,它基本上只在所有的Actor都访问过该状态后才访问。我们还使用注解Outcome来定义我们的期望。
Overall, the setup is quite simple and intuitive to follow. We can run this using a test harness, given by the framework, that finds all classes annotated with JCStressTest and executes them in several iterations to obtain all possible interleavings.
总的来说,这个设置相当简单,也很直观,可以遵循。我们可以使用框架给出的测试线束来运行,它可以找到所有用JCStressTest注解的类,并在几次迭代中执行它们,以获得所有可能的交织。
5. Other Ways to Detect Concurrency Issues
5.检测并发性问题的其他方法
Writing tests for concurrent code is difficult but possible. We’ve seen the challenges and some of the popular ways to overcome them. However, we may not be able to identify all possible concurrency issues through tests alone — especially when the incremental costs of writing more tests start to outweigh their benefits.
为并发代码编写测试是困难的,但也是可能的。我们已经看到了挑战和一些流行的方法来克服它们。然而,我们可能无法仅通过测试来识别所有可能的并发问题–尤其是当编写更多测试的增量成本开始超过其收益时。
Hence, together with a reasonable number of automated tests, we can employ other techniques to identify concurrency issues. This will boost our chances of finding concurrency issues without getting too much deeper into the complexity of automated tests. We’ll cover some of these in this section.
因此,连同合理数量的自动化测试,我们可以采用其他技术来识别并发问题。这将提高我们发现并发性问题的机会,而不至于太深入自动化测试的复杂性。我们将在本节中介绍其中的一些内容。
5.1. Static Analysis
5.1.静态分析
Static analysis refers to the analysis of a program without actually executing it. Now, what good can such an analysis do? We will come to that, but let’s first understand how it contrasts with dynamic analysis. The unit tests we’ve written so far need to be run with actual execution of the program they test. This is the reason they are part of what we largely refer to as dynamic analysis.
静态分析指的是在不实际执行程序的情况下对其进行分析。现在,这样的分析有什么用?我们将讨论这个问题,但首先让我们了解它与动态分析的区别。到目前为止,我们所写的单元测试需要在实际执行它们所测试的程序的情况下运行。这就是为什么它们是我们通常所说的动态分析的一部分。
Please note that static analysis is in no way any replacement for dynamic analysis. However, it provides an invaluable tool to examine the code structure and identify possible defects long before we even execute the code. The static analysis makes use of a host of templates that are curated with experience and understanding.
请注意,静态分析决不是动态分析的替代。然而,它提供了一个宝贵的工具来检查代码结构,并在我们执行代码之前很久就识别可能的缺陷。静态分析利用了大量的模板,这些模板是用经验策划的和理解。
While it’s quite possible to just look through the code and compare against the best practices and rules we’ve curated, we must admit that it’s not plausible for larger programs. There are, however, several tools available to perform this analysis for us. They are fairly mature, with a vast chest of rules for most of the popular programming languages.
虽然很有可能只是看一下代码,并与我们策划的最佳实践和规则进行比较,但我们必须承认,这对较大的程序来说是不可行的。然而,有几个工具可以为我们进行这种分析。它们是相当成熟的,对大多数流行的编程语言都有大量的规则库。
A prevalent static analysis tool for Java is FindBugs. FindBugs looks for instances of “bug patterns”. A bug pattern is a code idiom that is quite often an error. This may arise due to several reasons like difficult language features, misunderstood methods, and misunderstood invariants.
一个流行的Java静态分析工具是FindBugs>。FindBugs可以查找 “错误模式 “的实例。错误模式是一种代码习语,通常是一种错误。这可能是由于几个原因造成的,如困难的语言特性、被误解的方法和被误解的不变量。
FindBugs inspects the Java bytecode for occurrences of bug patterns without actually executing the bytecode. This is quite convenient to use and fast to run. FindBugs reports bugs belonging to many categories like conditions, design, and duplicated code.
FindBugs检查Java字节码是否存在错误模式,而无需实际执行字节码。这在使用上相当方便,运行速度也很快。FindBugs报告属于许多类别的bug,如条件、设计和重复的代码。
It also includes defects related to concurrency. It must, however, be noted that FindBugs can report false positives. These are fewer in practice but must be correlated with manual analysis.
它还包括与并发性有关的缺陷。然而,必须注意的是,FindBugs可以报告假阳性。这些在实践中比较少,但必须与人工分析相关联。
5.2. Model Checking
5.2.模型检查
Model Checking is a method of checking whether a finite-state model of a system meets a given specification. Now, this definition may sound too academic, but bear with it for a while!
模型检查是一种检查系统的有限状态模型是否符合给定规范的方法。现在,这个定义可能听起来太学术化了,但请先忍耐一下吧!
We can typically represent a computational problem as a finite-state machine. Although this is a vast area in itself, it gives us a model with a finite set of states and rules of transition between them with clearly defined start and end states.
我们通常可以把一个计算问题表示为一个有限状态机。尽管这本身就是一个巨大的领域,但它给我们提供了一个具有有限状态集的模型,以及在这些状态之间的转换规则,并明确定义了起始和结束状态。
Now, the specification defines how a model should behave for it to be considered as correct. Essentially, this specification holds all the requirements of the system that the model represents. One of the ways to capture specifications is using the temporal logic formula, developed by Amir Pnueli.
现在,规范定义了一个模型应该如何表现才能被认为是正确的。从本质上讲,这个规范持有模型所代表的系统的所有要求。捕获规范的方法之一是使用时间逻辑公式,由Amir Pnueli开发。
While it’s logically possible to perform model checking manually, it’s quite impractical. Fortunately, there are many tools available to help us here. One such tool available for Java is Java PathFinder (JPF). JPF was developed with years of experience and research at NASA.
虽然从逻辑上讲,手动执行模型检查是可能的,但这是很不现实的。幸运的是,有许多工具可以在这里帮助我们。其中一个可用于Java的工具是Java PathFinder(JPF)。JPF是在NASA多年的经验和研究下开发的。
Specifically, JPF is a model checker for Java bytecode. It runs a program in all possible ways, thereby checking for property violations like deadlock and unhandled exceptions along all possible execution paths. It can, therefore, prove to be quite useful in finding defects related to concurrency in any program.
具体来说,JPF是一个针对Java字节码的模型检查器。它以所有可能的方式运行程序,从而沿着所有可能的执行路径检查诸如死锁和未处理的异常等属性违规。因此,它可以证明在任何程序中找到与并发有关的缺陷是相当有用的。
6. Afterthoughts
6.事后感想
By now, it shouldn’t be a surprise to us that it’s best to avoid complexities related to multi-threaded code as much as possible. Developing programs with simpler designs, which are easier to test and maintain, should be our prime objective. We have to agree that concurrent programming is often necessary for modern-day applications.
到现在为止,我们不应该感到惊讶,最好尽可能地避免与多线程代码相关的复杂性。开发设计更简单的程序,更容易测试和维护,应该是我们的首要目标。我们不得不同意,对于现代的应用程序来说,并发式编程往往是必要的。
However, we can adopt several best practices and principles while developing concurrent programs that can make our life easier. In this section, we will go through some of these best practices, but we should keep in mind that this list is far from complete!
然而,我们可以在开发并发程序时采用一些最佳实践和原则,这可以使我们的生活更轻松。在本节中,我们将介绍其中的一些最佳实践,但我们应该记住,这个清单还远远不够完整
6.1. Reduce Complexity
6.1.降低复杂度
Complexity is a factor that can make testing a program difficult even without any concurrent elements. This just compounds in the face of concurrency. It’s not difficult to understand why simpler and smaller programs are easier to reason about and, hence, to test effectively. There are several best patterns that can help us here, like SRP (Single Responsibility Pattern) and KISS (Keep It Stupid Simple), to just name a few.
复杂性是一个因素,即使没有任何并发元素也会使测试程序变得困难。这只是在面对并发的时候变得更加复杂。这就不难理解为什么更简单、更小的程序更容易推理,从而更容易有效测试。有几种最佳模式可以帮助我们,比如SRP(Single Responsibility Pattern)和KISS(Keep It Stupid Simple),仅举几例。
Now, while these do not address the issue of writing tests for concurrent code directly, they make the job easier to attempt.
现在,虽然这些并没有直接解决为并发代码编写测试的问题,但它们使这项工作更容易尝试。
6.2. Consider Atomic Operations
6.2.考虑原子操作
Atomic operations are operations that run completely independently of each other. Hence, the difficulties of predicting and testing interleaving can be simply avoided. Compare-and-swap is one such widely-used atomic instruction. Simply put, it compares the contents of a memory location with a given value and, only if they are the same, modifies the contents of that memory location.
原子操作是完全独立运行的操作。因此,可以简单地避免预测和测试交织的困难。比较和交换就是这样一条广泛使用的原子指令。简单地说,它将一个内存位置的内容与一个给定的值进行比较,只有当它们相同时,才修改该内存位置的内容。
Most modern microprocessors offer some variant of this instruction. Java offers a range of atomic classes like AtomicInteger and AtomicBoolean, offering the benefits of compare-and-swap instructions underneath.
大多数现代微处理器都提供这种指令的某种变体。Java提供了一系列的原子类,如AtomicInteger和AtomicBoolean,在下面提供了比较和交换指令的好处。
6.3. Embrace Immutability
6.3.拥抱不变性
In multi-threaded programming, shared data that can be altered always leaves room for errors. Immutability refers to the condition where a data structure cannot be modified after instantiation. This is a match made in heaven for concurrent programs. If the state of an object can’t be altered after its creation, competing threads do not have to apply for mutual exclusion on them. This greatly simplifies writing and testing concurrent programs.
在多线程编程中,可以改变的共享数据总是留下错误的空间。不变性指的是数据结构在实例化后不能被修改的情况。这对并发程序来说是天作之合。如果一个对象的状态在其创建后不能被改变,那么竞争的线程就不必对其申请互斥。这大大简化了并发程序的编写和测试。
However, please note that we may not always have the liberty to choose immutability, but we must opt for it when it’s possible.
然而,请注意,我们可能并不总是有选择不变性的自由,但我们必须在有可能的时候选择不变性。
6.4. Avoid Shared Memory
6.4.避免共享内存
Most of the issues related to multi-threaded programming can be attributed to the fact that we have shared memory between competing threads. What if we could just get rid of them! Well, we still need some mechanism for threads to communicate.
大多数与多线程编程有关的问题都可以归因于这样一个事实,即我们在竞争的线程之间有共享内存。如果我们能摆脱它们呢!?好吧,我们仍然需要一些机制让线程进行通信。
There are alternate design patterns for concurrent applications that offer us this possibility. One of the popular ones is the Actor Model, which prescribes the actor as the basic unit of concurrency. In this model, actors interact with each other by sending messages.
有一些并发应用程序的替代设计模式为我们提供了这种可能性。其中一个流行的模式是演员模型,它规定演员是并发的基本单位。在这个模式中,行为体通过发送消息来相互作用。
Akka is a framework written in Scala that leverages the Actor Model to offer better concurrency primitives.
Akka是一个用Scala编写的框架,它利用Actor Model来提供更好的并发原语。
7. Conclusion
7.结语
In this tutorial, we covered some of the basics related to concurrent programming. We discussed multi-threaded concurrency in Java in particular detail. We went through the challenges it presents to us while testing such code, especially with shared data. Furthermore, we went through some of the tools and techniques available to test concurrent code.
在本教程中,我们介绍了与并发编程有关的一些基础知识。我们特别详细地讨论了Java中的多线程并发性。我们经历了在测试这类代码时所面临的挑战,特别是在共享数据方面。此外,我们还介绍了一些可用来测试并发代码的工具和技术。
We also discussed other ways to avoid concurrency issues, including tools and techniques besides automated tests. Finally, we went through some of the programming best practices related to concurrent programming.
我们还讨论了避免并发问题的其他方法,包括除了自动测试之外的工具和技术。最后,我们回顾了与并发编程有关的一些编程最佳实践。
The source code for this article can be found over on GitHub.
这篇文章的源代码可以在GitHub上找到over。