1. 程式人生 > >單元測試多線程解決之道

單元測試多線程解決之道

tac multipl 還需要 rst 測試 方法 成功 time 發的

遇到問題

曾今在開發的過程遇到一個問題,當時有一個服務是群發郵件的,由於一次發送幾十個上百個,所以就使用了多線程來操作。

在單元測試的時候,我調了這個方法測試下郵件發送,結果總是出現莫名其妙的問題,每次都沒有全部發送成功。

後來我感覺到啟動的子線程都被殺掉了,好像測試方法一走完就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。

這裏主要是借單元測試多線程來加深下對並發相關知識點的理解,將其用於實踐,來解決一些問題。關於這個單元測試多線程的問題很多人應該都知道,當初離職前面試過幾個人,也問了這個問題,有幾個說遇到過,我問為什麽存在這個問題,你又是怎麽解決的?結果沒一個答得上來。

其實遇到問題是好事,都是成長的機會,每一個問題後面都隱藏著很多盲點,深挖下去一定收獲頗多。

單元測試多線程解決之道