我是怎樣測試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()中同步添加就可以了。或者,聰明的碼小伙伴們,你們有更好的方案嗎?歡迎留言,大家一起討論。

 

文章同步公眾號:碼之初,每天推送Java技術文章,期待您的關注!

原創不易,轉載請注明出處,謝謝!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM