如何测试Java类的线程安全性
我在最近的一次網絡研討會中談到了這個問題,現在是時候以書面形式進行解釋了。 線程安全是Java等語言/平臺中類的重要品質,我們經常在線程之間共享對象。 缺乏線程安全性導致的問題很難調試,因為它們是零星的并且幾乎不可能有意復制。 您如何測試對象以確保它們是線程安全的? 這就是我的做法。
女人的香氣(1992),馬丁·布雷斯特(Martin Brest)
我們說有一個簡單的內存書架:
class Books {final Map<Integer, String> map =new ConcurrentHashMap<>();int add(String title) {final Integer next = this.map.size() + 1;this.map.put(next, title);return next;}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 ,對吧? 讓我們嘗試測試一下:
class BooksTest {@Testpublic void addsAndRetrieves() {Books books = new Books();String title = "Elegant Objects";int id = books.add(title);assert books.title(id).equals(title);} }測試通過了,但這只是一個單線程測試。 讓我們嘗試從幾個并行線程中進行相同的操作(我正在使用Hamcrest ):
class BooksTest {@Testpublic void addsAndRetrieves() {Books books = new Books();int threads = 10;ExecutorService service =Executors.newFixedThreadPool(threads);Collection<Future<Integer>> futures =new LinkedList<>();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));} }首先,我通過Executors創建線程池。 然后,我通過submit()提交10個Callable類型的對象。 他們每個人都會在書架上添加一本獨特的新書。 所有這些將由池中的那十個線程中的某些線程以某種不可預測的順序執行。
然后,我通過Future類型的對象列表獲取其執行者的結果。 最后,我計算創建的唯一圖書ID的數量。 如果數字為10,則沒有沖突。 我使用Set集合來確保ID列表僅包含唯一元素。
測試通過了我的筆記本電腦。 但是,它不夠堅固。 這里的問題是它并沒有真正從多個并行線程測試這些工作Books 。 我們調用之間經過的時間submit()是足夠大的,完成的執行books.add() 這就是為什么實際上只有一個線程可以同時運行的原因。 我們可以通過修改一些代碼來檢查它:
AtomicBoolean running = new AtomicBoolean(); AtomicInteger overlaps = new AtomicInteger(); Collection<Future<Integer>> futures = new LinkedList<>(); 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));通過此代碼,我試圖查看線程相互重疊的頻率并并行執行某些操作。 這永遠不會發生,并且overlaps等于零。 因此,我們的測試尚未真正完成任何測試。 它只是在書架上一一增加了十本書。 如果我將線程數量增加到1000,它們有時會開始重疊。 但是,即使它們數量很少,我們也希望它們重疊。 為了解決這個問題,我們需要使用CountDownLatch :
CountDownLatch latch = new CountDownLatch(1); AtomicBoolean running = new AtomicBoolean(); AtomicInteger overlaps = new AtomicInteger(); Collection<Future<Integer>> futures = new LinkedList<>(); 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));現在,每個線程在接觸書本之前,都要等待latch給予的許可。 當我們通過submit()提交所有內容時,它們將保持等待狀態。 然后,我們使用countDown()釋放閂鎖,它們同時開始運行。 現在,在我的筆記本電腦上,即使threads為10, overlaps也等于3-5。
最后一個assertThat()現在崩潰了! 我沒有像以前那樣得到10個圖書ID。 它是7-9,但絕不是10。顯然,該類不是線程安全的!
但是在修復該類之前,讓我們簡化測試。 讓我們用RunInThreads從Cactoos ,這確實是我們在前面已經做了完全一樣的,但引擎蓋下:
class BooksTest {@Testpublic void addsAndRetrieves() {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 ( RunsInThreads的第一個參數)并返回Boolean 。 使用與上述相同的基于閂鎖的方法,將在10個并行線程上執行此功能。
該RunInThreads似乎緊湊且方便,我已經在一些項目中使用它。
順便說一句,為了使Books線程安全性,我們只需要向其方法add()添加synchronized 。 或者,也許您可??以提出更好的解決方案?
翻譯自: https://www.javacodegeeks.com/2018/03/how-i-test-my-java-classes-for-thread-safety.html
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的如何测试Java类的线程安全性的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: phpnow服务器安装步骤图解
- 下一篇: Java代码样式运算符换行格式