分享和探討——如何測試Java類的線程安全性?

缺少線程安全性致使的問題很難調試,由於它們是零星的,幾乎不可能有意複製。你如何測試對象以確保它們是線程安全的?html

我在最近的學習中和優銳課老師談到了這個問題。如今,是時候以書面形式進行解釋了。線程安全是Java等語言/平臺中類的重要素質,咱們常常在線程之間共享對象。缺少線程安全性致使的問題很難調試,由於它們是零星的,幾乎不可能有意複製。你如何測試對象以確保它們是線程安全的?這就是個人作法。java

假設有一個簡單的內存書架:api

 1 class Books {
 2   final Map<Integer, String> map =
 3     new ConcurrentHashMap<>();
 4   int add(String title) {
 5     final Integer next = this.map.size() + 1;
 6     this.map.put(next, title);
 7     return next;
 8   }
 9   String title(int id) {
10     return this.map.get(id);
11   }
12 }

 

首先,咱們將一本書放在那裏,書架返回其ID。而後,咱們能夠經過ID讀取該書的標題:安全

1 Books books = new Books();
2 String title = "Elegant Objects";
3 int id = books.add(title);
4 assert books.title(id).equals(title);

 

該相似乎是線程安全的,由於咱們使用的是線程安全的ConcurrentHashMap而不是更原始的和非線程安全的HashMap,對嗎? 讓咱們嘗試測試一下:oracle

1 class BooksTest {
2   @Test
3   public void addsAndRetrieves() {
4     Books books = new Books();
5     String title = "Elegant Objects";
6     int id = books.add(title);
7     assert books.title(id).equals(title);
8   }
9 }

 

測試經過了,但這只是一個單線程測試。讓咱們嘗試從幾個並行線程中進行相同的操做(我正在使用Hamcrest):學習

 1 class BooksTest {
 2   @Test
 3   public void addsAndRetrieves() {
 4     Books books = new Books();
 5     int threads = 10;
 6     ExecutorService service =
 7       Executors.newFixedThreadPool(threads);
 8     Collection<Future<Integer>> futures =
 9       new ArrayList<>(threads);
10     for (int t = 0; t < threads; ++t) {
11       final String title = String.format("Book #%d", t);
12       futures.add(service.submit(() -> books.add(title)));
13     }
14     Set<Integer> ids = new HashSet<>();
15     for (Future<Integer> f : futures) {
16       ids.add(f.get());
17     }
18     assertThat(ids.size(), equalTo(threads));
19   }
20 }

 

首先,我經過執行程序建立線程池。而後,我經過submit()提交十個Callable類型的對象。 他們每一個人都會在書架上添加一本獨特的新書。全部這些將由池中的那十個線程中的某些線程以某種不可預測的順序執行。測試

而後,我經過Future類型的對象列表獲取其執行者的結果。最後,我計算建立的惟一圖書ID的數量。若是數字爲10,則沒有衝突。 我使用Setcollection來確保ID列表僅包含惟一元素。this

測試經過了個人筆記本電腦。可是,它不夠堅固。這裏的問題是它並無真正從多個並行線程測試這些工做簿。在兩次調用submit()之間通過的時間足夠長,能夠完成books.add()的執行。這就是爲何實際上只有一個線程能夠同時運行的緣由。咱們能夠經過修改一些代碼來檢查它:atom

 1 AtomicBoolean running = new AtomicBoolean();
 2 AtomicInteger overlaps = new AtomicInteger();
 3 Collection<Future<Integer>> futures =
 4   new ArrayList<>(threads);
 5 for (int t = 0; t < threads; ++t) {
 6   final String title = String.format("Book #%d", t);
 7   futures.add(
 8     service.submit(
 9       () -> {
10         if (running.get()) {
11           overlaps.incrementAndGet();
12         }
13         running.set(true);
14         int id = books.add(title);
15         running.set(false);
16         return id;
17       }
18     )
19   );
20 }
21 assertThat(overlaps.get(), greaterThan(0));

 

經過此代碼,我試圖查看線程相互重疊的頻率並並行執行某項操做。這永遠不會發生,而且重疊等於零。所以,咱們的測試還沒有真正完成任何測試。它只是在書架上一一增長了十本書。若是我將線程數增長到1000,它們有時會開始重疊。可是,即便它們數量不多,咱們也但願它們重疊。爲了解決這個問題,咱們須要使用CountDownLatch:spa

 1 CountDownLatch latch = new CountDownLatch(1);
 2 AtomicBoolean running = new AtomicBoolean();
 3 AtomicInteger overlaps = new AtomicInteger();
 4 Collection<Future<Integer>> futures =
 5   new ArrayList<>(threads);
 6 for (int t = 0; t < threads; ++t) {
 7   final String title = String.format("Book #%d", t);
 8   futures.add(
 9     service.submit(
10       () -> {
11         latch.await();
12         if (running.get()) {
13           overlaps.incrementAndGet();
14         }
15         running.set(true);
16         int id = books.add(title);
17         running.set(false);
18         return id;
19       }
20     )
21   );
22 }
23 latch.countDown();
24 Set<Integer> ids = new HashSet<>();
25 for (Future<Integer> f : futures) {
26   ids.add(f.get());
27 }
28 assertThat(overlaps.get(), greaterThan(0));

 

如今,每一個線程在接觸書本以前都要等待閂鎖給出的許可。當咱們經過submit()提交全部內容時,它們將保留並等待。而後,咱們用countDown()釋放閂鎖,它們同時開始運行。如今,在個人筆記本電腦上,即便線程爲10,重疊也等於3-5。

 

最後一個assertThat()如今崩潰了!我沒有像之前那樣獲得10個圖書ID。它是7-9,但毫不是10。顯然,該類不是線程安全的!

 

可是在修復該類以前,讓咱們簡化測試。讓咱們使用來自Cactoos的RunInThreads,它與咱們上面作的徹底同樣,但其實是:

 1 class BooksTest {
 2   @Test
 3   public void addsAndRetrieves() {
 4     Books books = new Books();
 5     MatcherAssert.assertThat(
 6       t -> {
 7         String title = String.format(
 8           "Book #%d", t.getAndIncrement()
 9         );
10         int id = books.add(title);
11         return books.title(id).equals(title);
12       },
13       new RunsInThreads<>(new AtomicInteger(), 10)
14     );
15   }
16 }

 

assertThat()的第一個參數是Func(功能接口)的實例,它接受AtomicIntegerRunsInThreads的第一個參數)並返回布爾值。使用與上述相同的基於閂鎖的方法,將在10個並行線程上執行此功能。

RunsInThreads彷佛緊湊且方便,我已經在一些項目中使用它。

順便說一句,爲了使Books成爲線程安全的,咱們只須要向其方法add()中同步添加。或者,也許你能夠提出更好的解決方案?

相關文章
相關標籤/搜索