單元測試多線程解決之道
遇到問題
曾今在開發的過程遇到一個問題,當時有一個服務是群發郵件的,由於一次發送幾十個上百個,所以就使用了多線程來操作。
在單元測試的時候,我調了這個方法測試下郵件發送,結果總是出現莫名其妙的問題,每次都沒有全部發送成功。
後來我感覺到啟動的子線程都被殺掉了,好像測試方法一走完就over了,試著在測試方法末尾讓線程睡眠個幾秒,結果就能正常發送郵件。
分析解決
感覺這個Junit有點貓膩,就上網查了一下,再跟蹤下源碼,果然發現了問題所在。
TestRunner的main方法:
public static void main(String[] args) { TestRunner aTestRunner = new TestRunner(); try { TestResult r = aTestRunner.start(args); if (!r.wasSuccessful()) { System.exit(1); } System.exit(0); } catch (Exception var3) { System.err.println(var3.getMessage()); System.exit(2); } }
上面顯示了,不管成功與否,都會調用 System.exit() 方法關閉程序,這個方法是用來結束當前正在運行中的java虛擬機。
System.exit(0) 是正常退出程序,而 System.exit(1) 或者說非0表示非正常退出程序。
由此可見,junit 並不適合用來測試多線程程序呢,但是也不是沒有方法,根據其原理可以嘗試讓主線程阻塞一下,等待其他子線程執行完畢再繼續。
最簡單的方法就是讓主線程睡眠個幾秒鐘:
TimeUnit.SECONDS.sleep(5);
回顧復盤
除了讓主線程睡眠以外,其實還有很多其他的工具可以幫我們解決這個問題。今天想起來了,就來試試吧。
來個數據庫連接池相關的測試:
public class MultipleConnectionTest{ private HikariDataSource ds; @Before public void setup() { HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:mysql://127.0.0.1:3306/design"); config.setDriverClassName("com.mysql.jdbc.Driver"); config.setUsername("root"); config.setPassword("fengcs"); config.setMinimumIdle(1); config.setMaximumPoolSize(5); ds = new HikariDataSource(config); } @After public void teardown() { ds.close(); } @Test public void testMulConnection() { ConnectionThread connectionThread = new ConnectionThread(); Thread thread = null; for (int i = 0; i < 5; i++) { thread = new Thread(connectionThread, "thread-con-" + i); thread.start(); } // TimeUnit.SECONDS.sleep(5); (1) } private class ConnectionThread implements Runnable{ @Override public void run() { Connection connection = null; try { connection = ds.getConnection(); Statement statement = connection.createStatement(); ResultSet resultSet = statement.executeQuery("select id from tb_user"); String firstValue; System.out.println("<============="); System.out.println("==============>"+Thread.currentThread().getName() + ":"); while (resultSet.next()) { firstValue = resultSet.getString(1); System.out.print(firstValue); } } catch (SQLException e) { e.printStackTrace(); } finally { try { if (connection != null) { connection.close(); } } catch (SQLException e) { e.printStackTrace(); } } } } }
這個代碼一跑起來就會報錯:
java.sql.SQLException: HikariDataSource HikariDataSource (HikariPool-1) has been closed.
1、使用 join 方法
根據上面的代碼,直接加個 join 試試:
@Test
public void testMulConnection() {
ConnectionThread connectionThread = new ConnectionThread();
Thread thread = null;
for (int i = 0; i < 5; i++) {
thread = new Thread(connectionThread, "thread-con-" + i);
thread.start();
thread.join();
}
}
這樣雖然可以成功執行,但仔細一看,和單個線程執行沒有什麽區別。對於主線程來說,start一個就join一個,開始阻塞等待子線程完成,然後循環開始第二個操作。
正確的操作應該類似這樣:
Thread threadA = new Thread(connectionThread);
Thread threadB = new Thread(connectionThread);
threadA.start();
threadB.start();
threadA.join();
threadB.join();
這樣多個線程可以一起執行。不過線程多了,這樣寫比較麻煩。
2、閉鎖 - CountDownLatch
CountDownLatch 允許一個或多個線程等待其他線程完成操作。
CountDownLatch 的構造函數接收一個int類型的參數作為計數器,如果你想等待N個點完成,這裏就傳入N。
那麽在這裏,很明顯主線程應該等待其他五個線程完成查詢後再關閉。那麽加上(1)和(2)處的代碼,讓主線程阻塞等待。
private static CountDownLatch latch = new CountDownLatch(5); // (1)
@Test
public void testMulConnection() throws InterruptedException {
ConnectionThread connectionThread = new ConnectionThread();
Thread thread = null;
for (int i = 0; i < 5; i++) {
thread = new Thread(connectionThread, "thread-con-"+i);
thread.start();
}
latch.await(); // (2)
}
當我們調用CountDownLatch的countDown方法時,N就會減1,CountDownLatch的await方法
會阻塞當前線程,直到N變成零。增加(3)處代碼,每個線程完成查詢後就將計數器減一。
private class ConnectionThread implements Runnable{
@Override
public void run() {
Connection connection = null;
try {
connection = ds.getConnection();
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("select id from tb_user");
String firstValue;
System.out.println("<=============");
System.out.println("==============>"+Thread.currentThread().getName() + ":");
while (resultSet.next()) {
firstValue = resultSet.getString(1);
System.out.print(firstValue);
}
latch.countDown(); // (3)
} catch (SQLException e) {
e.printStackTrace();
} finally {
try {
if (connection != null) {
connection.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
測試一下,完全滿足要求。
3、柵欄- CyclicBarrier
CyclicBarrier 的字面意思是可循環使用(Cyclic)的屏障(Barrier)。它要做的事情是,讓一
組線程到達一個屏障(也可以叫同步點)時被阻塞,直到最後一個線程到達屏障時,屏障才會
開門,所有被屏障攔截的線程才會繼續運行。
這裏和 CountDownLatch 有所不同,但是主線程需要阻塞,依然在main方法末尾處加上一個同步點:
private static CyclicBarrier cyclicBarrier = new CyclicBarrier(6); // (1)
@Test
public void testMulConnection() throws BrokenBarrierException, InterruptedException {
ConnectionThread connectionThread = new ConnectionThread();
Thread thread = null;
for (int i = 0; i < 5; i++) {
thread = new Thread(connectionThread, "thread-con-"+i);
thread.start();
}
cyclicBarrier.await(); // (2)
}
CyclicBarrier默認的構造方法是 CyclicBarrier(int parties),其參數表示屏障攔截的線程數量,每個線程調用await方法告訴CyclicBarrier我已經到達了屏障,然後當前線程被阻塞。
這個時候沒有類似閉鎖的 countDown 方法來計數,只能靠線程到達同步點來確認是否都到達,而其他線程不會走main方法的同步點,所以還需要一個其他五個線程匯合的同步點。那麽可以在每個線程 run 方法末尾 await 一下:
private class ConnectionThread implements Runnable{
@Override
public void run() {
Connection connection = null;
try {
connection = ds.getConnection();
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("select id from tb_user");
String firstValue;
System.out.println("<=============");
System.out.println("==============>"+Thread.currentThread().getName() + ":");
while (resultSet.next()) {
firstValue = resultSet.getString(1);
System.out.print(firstValue);
}
cyclicBarrier.await(); // (3)
} catch (SQLException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
} finally {
try {
if (connection != null) {
connection.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
這樣就感覺兩者有一個潛在的通信機制,都到了就一起放開。只不過現在是六個線程參與計數了,CyclicBarrier 構造器傳參應該是6(小於6也可能成功,大於6一定會一直阻塞)。
綜合看了一下,我覺得最合適的還是 CountDownLatch。
這裏主要是借單元測試多線程來加深下對並發相關知識點的理解,將其用於實踐,來解決一些問題。關於這個單元測試多線程的問題很多人應該都知道,當初離職前面試過幾個人,也問了這個問題,有幾個說遇到過,我問為什麽存在這個問題,你又是怎麽解決的?結果沒一個答得上來。
其實遇到問題是好事,都是成長的機會,每一個問題後面都隱藏著很多盲點,深挖下去一定收獲頗多。
單元測試多線程解決之道