如何測(cè)試Java類(lèi)的線程安全性
這篇文章主要介紹了如何測(cè)試Java類(lèi)的線程安全性,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
線程安全性是Java等語(yǔ)言/平臺(tái)中類(lèi)的一個(gè)重要標(biāo)準(zhǔn),在Java中,我們經(jīng)常在線程之間共享對(duì)象。由于缺乏線程安全性而導(dǎo)致的問(wèn)題很難調(diào)試,因?yàn)樗鼈兪桥及l(fā)的,而且?guī)缀醪豢赡苡心康牡刂噩F(xiàn)。如何測(cè)試對(duì)象以確保它們是線程安全的?
假如有一個(gè)內(nèi)存書(shū)架
package com.mzc.common.thread;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* <p class="detail">
* 功能: 內(nèi)存書(shū)架
* </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">
* 功能: 存書(shū),并返回書(shū)的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ù)書(shū)的id讀取書(shū)名
* </p>
*
* @param id :
* @return string
* @author Moore
* @date 2019.12.10 14:00:16
*/
String title(int id) {
return this.map.get(id);
}
}
首先,我們把一本書(shū)放進(jìn)書(shū)架,書(shū)架會(huì)返回它的ID。然后,我們可以通過(guò)它的ID來(lái)讀取書(shū)名,像這樣:
Books books = new Books(); String title = "Elegant Objects"; int id = books.add(title); assert books.title(id).equals(title);
這個(gè)類(lèi)似乎是線程安全的,因?yàn)槲覀兪褂玫氖蔷€程安全的ConcurrentHashMap,而不是更原始和非線程安全的HashMap,對(duì)吧?我們先來(lái)測(cè)試一下:
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);
}
}
查看測(cè)試結(jié)果:

測(cè)試通過(guò)了,但這只是一個(gè)單線程測(cè)試。讓我們嘗試從幾個(gè)并行線程中進(jìn)行相同的操作(我使用的是Hamcrest):
/**
* <p class="detail">
* 功能: 多線程測(cè)試
* </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));
}
首先,我通過(guò)執(zhí)行程序創(chuàng)建線程池。然后,我通過(guò)Submit()提交10個(gè)Callable類(lèi)型的對(duì)象。他們每個(gè)都會(huì)在書(shū)架上添加一本唯一的新書(shū)。所有這些將由池中的10個(gè)線程中的某些線程以某種不可預(yù)測(cè)的順序執(zhí)行。
然后,我通過(guò)Future類(lèi)型的對(duì)象列表獲取其執(zhí)行者的結(jié)果。最后,我計(jì)算創(chuàng)建的唯一圖書(shū)ID的數(shù)量。如果數(shù)字為10,則沒(méi)有沖突。我使用Set集合來(lái)確保ID列表僅包含唯一元素。
我們看一下這樣改造后的運(yùn)行結(jié)果:

測(cè)試也通過(guò)了,但是,它不夠強(qiáng)壯。這里的問(wèn)題是它并沒(méi)有真正從多個(gè)并行線程測(cè)試這些書(shū)。在兩次調(diào)用commit()之間經(jīng)過(guò)的時(shí)間足夠長(zhǎng),可以完成books.add()的執(zhí)行。這就是為什么實(shí)際上只有一個(gè)線程可以同時(shí)運(yùn)行的原因。
我們可以通過(guò)修改一些代碼再來(lái)檢查它:
@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));
}
看一下測(cè)試結(jié)果:

執(zhí)行錯(cuò)誤,說(shuō)明插入的書(shū)和返回的id數(shù)量是不沖突的。
通過(guò)上面的代碼,我試圖了解線程之間的重疊頻率以及并行執(zhí)行的頻率。但是基本上概率為0,所以這個(gè)測(cè)試還沒(méi)有真正測(cè)到我想測(cè)的,還不是我們想要的,它只是把十本書(shū)一本一本地加到書(shū)架上。
再來(lái):

可以看到,如果我把線程數(shù)增加到1000,它們會(huì)開(kāi)始重疊或者并行運(yùn)行。
但是我希望即使線程數(shù)只有10個(gè)的時(shí)候,也會(huì)出現(xiàn)重疊并行的情況。怎么辦呢?為了解決這個(gè)問(wèn)題,我使用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è)線程在接觸書(shū)本之前都要等待鎖權(quán)限。當(dāng)我們通過(guò)Submit()提交所有內(nèi)容時(shí),它們將保留并等待。然后,我們用countDown()釋放鎖,它們才同時(shí)開(kāi)始運(yùn)行。
查看運(yùn)行結(jié)果:

通過(guò)運(yùn)行結(jié)果可以知道,現(xiàn)在線程數(shù)還是為10,但是線程的重疊數(shù)是大于0的,所以assertTrue執(zhí)行通過(guò),ids也不等于10了,也就是沒(méi)有像以前那樣得到10個(gè)圖書(shū)ID。顯然,Books類(lèi)不是線程安全的!
在修復(fù)優(yōu)化該類(lèi)之前,教大家一個(gè)簡(jiǎn)化測(cè)試的方法,使用來(lái)自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看起來(lái)非常緊湊,用起來(lái)也很方便,推薦給大家,可以用起來(lái)的。只需要在你的項(xiàng)目中添加一個(gè)依賴(lài):
<dependency>
<groupId>org.llorllale</groupId>
<artifactId>cactoos-matchers</artifactId>
<version>0.18</version>
</dependency>
最后,為了使Books類(lèi)成為線程安全的,我們只需要向其方法add()中同步添加就可以了?;蛘?,聰明的碼小伙伴們,你們有更好的方案嗎?歡迎留言,大家一起討論。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Java實(shí)現(xiàn)順序表和鏈表結(jié)構(gòu)
大家好,本篇文章主要講的是Java實(shí)現(xiàn)順序表和鏈表結(jié)構(gòu),感興趣的同學(xué)趕快來(lái)看一看吧,對(duì)你有幫助的話(huà)記得收藏一下2022-02-02
Spring Cloud gateway 網(wǎng)關(guān)如何攔截Post請(qǐng)求日志
這篇文章主要介紹了Spring Cloud gateway 網(wǎng)關(guān)如何攔截Post請(qǐng)求日志的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07
java搭建一個(gè)Socket服務(wù)器響應(yīng)多用戶(hù)訪問(wèn)
本篇文章主要介紹了java搭建一個(gè)Socket服務(wù)器響應(yīng)多用戶(hù)訪問(wèn),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-02-02
詳解Java的MyBatis框架中動(dòng)態(tài)SQL的基本用法
這篇文章主要介紹了詳解Java的MyBatis框架中動(dòng)態(tài)SQL的基本用法,文中列出了一些常用SQL動(dòng)態(tài)語(yǔ)句進(jìn)行講解,需要的朋友可以參考下2016-04-04
SpringBoot使用druid配置多數(shù)據(jù)源問(wèn)題
這篇文章主要介紹了SpringBoot使用druid配置多數(shù)據(jù)源問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-03-03
java實(shí)現(xiàn)簡(jiǎn)單的推箱子小游戲
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)簡(jiǎn)單的推箱子小游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-05-05
maven如何動(dòng)態(tài)統(tǒng)一修改版本號(hào)的方法步驟
這篇文章主要介紹了maven如何動(dòng)態(tài)統(tǒng)一修改版本號(hào)的方法步驟,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-12-12
15道非常經(jīng)典的Java面試題 附詳細(xì)答案
這篇文章主要為大家推薦了15道非常經(jīng)典的Java面試題,附詳細(xì)答案,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-10-10
java獲取當(dāng)前時(shí)間的四種方法代碼實(shí)例
這篇文章主要介紹了java獲取當(dāng)前時(shí)間的四種方法代碼實(shí)例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-09-09

