您好,登錄后才能下訂單哦!
Java中怎么測試線程的安全性,很多新手對(duì)此不是很清楚,為了幫助大家解決這個(gè)難題,下面小編將為大家詳細(xì)講解,有這方面需求的人可以來學(xué)習(xí)下,希望你能有所收獲。
線程安全性是Java等語言/平臺(tái)中類的一個(gè)重要標(biāo)準(zhǔn),在Java中,我們經(jīng)常在線程之間共享對(duì)象。由于缺乏線程安全性而導(dǎo)致的問題很難調(diào)試,因?yàn)樗鼈兪桥及l(fā)的,而且?guī)缀醪豢赡苡心康牡刂噩F(xiàn)。如何測試對(duì)象以確保它們是線程安全的?
假如有一個(gè)內(nèi)存書架
package com.mzc.common.thread; import java.util.Map;import java.util.concurrent.ConcurrentHashMap; /** * <p class="detail"> * 功能: 內(nèi)存書架 * </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"> * 功能: 根據(jù)書的id讀取書名 * </p> * * @param id : * @return string * @author Moore * @date 2019.12.10 14:00:16 */ String title(int id) { return this.map.get(id); }}
首先,我們把一本書放進(jìn)書架,書架會(huì)返回它的ID。然后,我們可以通過它的ID來讀取書名,像這樣:
Books books = new Books();String title = "Elegant Objects";int id = books.add(title);assert books.title(id).equals(title);
這個(gè)類似乎是線程安全的,因?yàn)槲覀兪褂玫氖蔷€程安全的ConcurrentHashMap,而不是更原始和非線程安全的HashMap,對(duì)吧?我們先來測試一下:
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); }}
查看測試結(jié)果:
測試通過了,但這只是一個(gè)單線程測試。讓我們嘗試從幾個(gè)并行線程中進(jìn)行相同的操作(我使用的是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)); }
首先,我通過執(zhí)行程序創(chuàng)建線程池。然后,我通過Submit()提交10個(gè)Callable類型的對(duì)象。他們每個(gè)都會(huì)在書架上添加一本唯一的新書。所有這些將由池中的10個(gè)線程中的某些線程以某種不可預(yù)測的順序執(zhí)行。
然后,我通過Future類型的對(duì)象列表獲取其執(zhí)行者的結(jié)果。最后,我計(jì)算創(chuàng)建的唯一圖書ID的數(shù)量。如果數(shù)字為10,則沒有沖突。我使用Set集合來確保ID列表僅包含唯一元素。
我們看一下這樣改造后的運(yùn)行結(jié)果:
測試也通過了,但是,它不夠強(qiáng)壯。這里的問題是它并沒有真正從多個(gè)并行線程測試這些書。在兩次調(diào)用commit()之間經(jīng)過的時(shí)間足夠長,可以完成books.add()的執(zhí)行。這就是為什么實(shí)際上只有一個(gè)線程可以同時(shí)運(yùn)行的原因。
我們可以通過修改一些代碼再來檢查它:
@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)); }
看一下測試結(jié)果:
執(zhí)行錯(cuò)誤,說明插入的書和返回的id數(shù)量是不沖突的。
通過上面的代碼,我試圖了解線程之間的重疊頻率以及并行執(zhí)行的頻率。但是基本上概率為0,所以這個(gè)測試還沒有真正測到我想測的,還不是我們想要的,它只是把十本書一本一本地加到書架上。
再來:
可以看到,如果我把線程數(shù)增加到1000,它們會(huì)開始重疊或者并行運(yùn)行。
但是我希望即使線程數(shù)只有10個(gè)的時(shí)候,也會(huì)出現(xiàn)重疊并行的情況。怎么辦呢?為了解決這個(gè)問題,我使用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)); }
現(xiàn)在,每個(gè)線程在接觸書本之前都要等待鎖權(quán)限。當(dāng)我們通過Submit()提交所有內(nèi)容時(shí),它們將保留并等待。然后,我們用countDown()釋放鎖,它們才同時(shí)開始運(yùn)行。
查看運(yùn)行結(jié)果:
通過運(yùn)行結(jié)果可以知道,現(xiàn)在線程數(shù)還是為10,但是線程的重疊數(shù)是大于0的,所以assertTrue執(zhí)行通過,ids也不等于10了,也就是沒有像以前那樣得到10個(gè)圖書ID。顯然,Books類不是線程安全的!
在修復(fù)優(yōu)化該類之前,教大家一個(gè)簡化測試的方法,使用來自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()的第一個(gè)參數(shù)是Func(一個(gè)函數(shù)接口)的實(shí)例,接受AtomicInteger(RunsThreads的第一個(gè)參數(shù))并返回布爾值。此函數(shù)將在10個(gè)并行線程上執(zhí)行,使用與上述相同的基于鎖的方法。
這個(gè)RunInThreads看起來非常緊湊,用起來也很方便,推薦給大家,可以用起來的。只需要在你的項(xiàng)目中添加一個(gè)依賴:
<dependency> <groupId>org.llorllale</groupId> <artifactId>cactoos-matchers</artifactId> <version>0.18</version> </dependency>
看完上述內(nèi)容是否對(duì)您有幫助呢?如果還想對(duì)相關(guān)知識(shí)有進(jìn)一步的了解或閱讀更多相關(guān)文章,請(qǐng)關(guān)注億速云行業(yè)資訊頻道,感謝您對(duì)億速云的支持。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。