线程同步,线程不同步_同步多线程集成测试
線程同步,線程不同步
測試線程非常困難,這使得為被測多線程系統編寫良好的集成測試非常困難。 這是因為在JUnit中,測試代碼,被測對象和任何線程之間沒有內置的同步。 這意味著,當您必須為創建并運行線程的方法編寫測試時,通常會出現問題。 該領域中最常見的場景之一是調用被測方法,該方法在返回之前啟動新線程的運行。 在將來某個時刻完成線程的工作時,您需要斷言一切都很好。 這種情況的示例可能包括異步地從套接字讀取數據或對數據庫執行冗長而復雜的一組操作。
例如,下面的ThreadWrapper類包含一個公共方法: doWork() 。 調用doWork()會使情況doWork()并且在將來某個時候,由JVM決定,一個線程會運行,將數據添加到數據庫中。
此代碼的直接測試是調用doWork()方法,然后在數據庫中檢查結果。 問題是,由于使用了線程,被測對象,測試對象與線程之間沒有協調。 編寫此類測試時,實現某種協調的一種常見方法是在被測方法的調用與檢查數據庫中的結果之間放置某種延遲,如下所示:
public class ThreadWrapperTest {@Testpublic void testDoWork() throws InterruptedException {ThreadWrapper instance = new ThreadWrapper();instance.doWork();Thread.sleep(10000);boolean result = getResultFromDatabase();assertTrue(result);}/*** Dummy database method - just return true*/private boolean getResultFromDatabase() {return true;} }在上面的代碼中,兩個方法調用之間有一個簡單的Thread.sleep(10000) 。 這種技術的優點是簡單易行。 但是它也非常危險。 這是因為它在測試和工作線程之間引入了競爭條件,因為JVM無法保證線程何時運行。 通常,它只能在開發人員的計算機上工作,而在構建計算機上始終失敗。 即使可以在構建機器上運行,它也會從表面上延長測試的持續時間; 請記住,快速構建很重要。 正確執行此操作的唯一肯定方法是同步兩個不同的線程,而執行此操作的一種技術是將一個簡單的CountDownLatch注入到被測實例中。 在下面的示例中,我修改了ThreadWrapper類的doWork()方法,將CountDownLatch添加為參數。
public class ThreadWrapper {/*** Start the thread running so that it does some work.*/public void doWork(final CountDownLatch latch) {Thread thread = new Thread() {/*** Run method adding data to a fictitious database*/@Overridepublic void run() {System.out.println("Start of the thread");addDataToDB();System.out.println("End of the thread method");countDown();}private void addDataToDB() {try {Thread.sleep(4000);} catch (InterruptedException e) {e.printStackTrace();}}private void countDown() {if (isNotNull(latch)) {latch.countDown();}}private boolean isNotNull(Object obj) {return latch != null;}};thread.start();System.out.println("Off and running...");} }Javadoc API將倒數鎖存器描述為:同步輔助,它允許一個或多個線程等待,直到在其他線程中執行的一組操作完成為止。 使用給定的計數初始化CountDownLatch。 由于對countDown()方法的調用,當前的await方法將阻塞,直到當前計數達到零為止,此后,所有等待線程都將被釋放,并且所有隨后的await調用將立即返回。 這是一種一次性現象,無法重置計數。 如果需要用于重置計數的版本,請考慮使用CyclicBarrier。
CountDownLatch是一種多功能的同步工具,可以用于多種用途。 以1計數初始化的CountDownLatch用作簡單的開/關閂鎖或門:所有調用await的線程在門處等待,直到被調用countDown()的線程打開為止。 初始化為N的CountDownLatch可以用于使一個線程等待,直到N個線程完成某項操作或某項操作已完成N次。 CountDownLatch的一個有用屬性是,它不需要調用countDown的線程在繼續進行操作之前就無需等待計數達到零,它只是防止任何線程經過等待狀態,直到所有線程都可以通過。
這里的想法是,測試代碼將永遠不會檢查數據庫的結果,直到工作線程的run()方法調用latch.countdown() 。 這是因為測試代碼線程阻塞了對latch.await()的調用。 閂鎖latch.countdown()減少閂鎖的計數,并且一旦它為零,阻塞調用閂鎖latch.await()將返回并且測試代碼將繼續執行,這是安全的, latch.await()是應知道數據庫中應有任何結果。 然后,測試可以檢索這些結果并做出有效的斷言。 顯然,以上代碼僅偽造了數據庫連接和操作。 問題是您可能不想或不需要直接將CountDownLatch插入代碼中。 畢竟它沒有在生產中使用,而且看起來也不是特別干凈或優雅。 解決此問題的一種快速方法是簡單地將doWork(CountDownLatch latch)方法包設為私有,并通過公共doWork()方法公開它。
public class ThreadWrapper {/*** Start the thread running so that it does some work.*/public void doWork() {doWork(null);}@VisibleForTestingvoid doWork(final CountDownLatch latch) {Thread thread = new Thread() {/*** Run method adding data to a fictitious database*/@Overridepublic void run() {System.out.println("Start of the thread");addDataToDB();System.out.println("End of the thread method");countDown();}private void addDataToDB() {try {Thread.sleep(4000);} catch (InterruptedException e) {e.printStackTrace();}}private void countDown() {if (isNotNull(latch)) {latch.countDown();}}private boolean isNotNull(Object obj) {return latch != null;}};thread.start();System.out.println("Off and running...");} }上面的代碼使用Google的Guava @VisibleForTesting批注來告訴我們,出于測試目的,已經稍微放松了doWork(CountDownLatch latch)方法的可見性。
現在,我意識到,將一個方法調用包私有化以用于測試目的是非常有爭議的; 有些人討厭這個主意,而另一些人則無所不在。 我可以就這個主題寫一個整個博客(可能一天),但是對我來說,在別無選擇的情況下(例如,當您為遺留代碼編寫特性測試時)應謹慎使用。 如果可能,應避免使用它,但決不能排除。 畢竟,經過測試的代碼比未經測試的代碼更好。
考慮到這一點, ThreadWrapper的下一次迭代將設計出標記為@VisibleForTesting的方法,以及將CountDownLatch注入生產代碼的需求。 這里的想法是使用策略模式并將Runnable實現與Thread分開。 因此,我們有一個非常簡單的ThreadWrapper
public class ThreadWrapper {/*** Start the thread running so that it does some work.*/public void doWork(Runnable job) {Thread thread = new Thread(job);thread.start();System.out.println("Off and running...");} }和一個單獨的工作:
public class DatabaseJob implements Runnable {/*** Run method adding data to a fictitious database*/@Overridepublic void run() {System.out.println("Start of the thread");addDataToDB();System.out.println("End of the thread method");}private void addDataToDB() {try {Thread.sleep(4000);} catch (InterruptedException e) {e.printStackTrace();}} }您會注意到DatabaseJob類不使用CountDownLatch 。 如何同步? 答案就在下面的測試代碼中……
public class ThreadWrapperTest {@Testpublic void testDoWork() throws InterruptedException {ThreadWrapper instance = new ThreadWrapper();CountDownLatch latch = new CountDownLatch(1);DatabaseJobTester tester = new DatabaseJobTester(latch);instance.doWork(tester);latch.await();boolean result = getResultFromDatabase();assertTrue(result);}/*** Dummy database method - just return true*/private boolean getResultFromDatabase() {return true;}private class DatabaseJobTester extends DatabaseJob {private final CountDownLatch latch;public DatabaseJobTester(CountDownLatch latch) {super();this.latch = latch;}@Overridepublic void run() {super.run();latch.countDown();}} }上面的測試代碼包含一個內部類DatabaseJobTester ,該類擴展了DatabaseJob 。 在此類中,在通過調用super.run()更新了我們的虛假數據庫之后,將run()方法重寫為包括對latch.countDown()的調用。 之所以doWork(Runnable job) ,是因為測試將DatabaseJobTester實例傳遞給doWork(Runnable job)方法,并添加了所需的線程測試功能。 我曾在我的一篇有關測試技術的博客中提到過將被測試對象分類的想法,這是一種非常強大的技術。
因此,得出以下結論:
- 測試線程很難。
- 測試匿名內部類幾乎是不可能的。
- 使用Thead.sleep(...)是一個冒險的想法,應避免使用。
- 您可以使用策略模式來重構這些問題。
- 編程是做出正確決策的藝術
…放松測試方法的可視性可能是一個好主意,也許不是一個好主意,但稍后會更多……
上面的代碼可在unit-testing-threads項目下的隊長調試存儲庫(git://github.com/roghughe/captaindebug.git)中的Github上找到。
參考: Captain Debug的Blog博客上的JCG合作伙伴 Roger Hughes的同步多線程集成測試 。
翻譯自: https://www.javacodegeeks.com/2013/02/synchronising-multithreaded-integration-tests.html
線程同步,線程不同步
總結
以上是生活随笔為你收集整理的线程同步,线程不同步_同步多线程集成测试的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 使用Spring Data的Apache
- 下一篇: 连接Linux系统的软件(连接linux