轉自:http://blog.sina.com.cn/s/blog_4bed7e340101atnf.html
兩年前寫的程序,出了一個當時覺得莫名其妙的bug,就是線程偶爾會死掉,當時也看不出有什么
問題所以當時的對策是起了一個監控線程,發現線程死掉就重啟一個
今天回頭再去看這段代碼,發現確實有漏洞
代碼如下
public void startOneBusiness(final String businessID) {
final IBusiness business = getOneBusiness(businessID);
business.strat();
Thread thread = new Thread(new Runnable() {
public void run() {
try {
business.setBusinessThread(Thread.currentThread());
while (business.getThreadStat() == IBusiness.START) {
try {
business.setLastRuntime(System.currentTimeMillis());
business.doTask();
Thread.sleep(business.getThreadSleepTime());
} catch (Throwable e) {
LogDebug.debug(business.getBusinessName()
+ " doTask 致命錯誤", e);
// 發郵件
AppException appException = new AppException(e);
MailUtil.sendMail(business.getBusinessName()
+ " doTask 致命錯誤",
appException.getMessage(), business
.getAddresses());
}
}
business.end();
} catch (Throwable e) {
LogDebug.debug(business.getBusinessName() + " doTask 致命錯誤",
e);
// 發郵件
AppException appException = new AppException(e);
MailUtil.sendMail(business.getBusinessName()
+ " doTask 致命錯誤", appException.getMessage(),
business.getAddresses());
} finally {
business.end();
}
}
});
IBusiness.threadPool.submit(thread);
}
public void doTask() {
System.out
.println("***************************************************");
System.out.println(getBusinessName() + " 開始");
long b = System.currentTimeMillis();
try {
T taskData = getTaskData();
if (taskData != null) {
final CountDownLatch countDownLatch = new CountDownLatch(
getRealThreadCount(taskData));
System.out.println(getBusinessName() + " 線程數:"
+ countDownLatch.getCount() + " 總任務數:"
+ getTaskCount(taskData));
final CountDownLatch startLatch = new CountDownLatch(1);// 確保在所有線程開始執行任務前,所有准備工作都已經完成,一旦准備工作完成了就調用startLatch.countDown()
for (int i = 0; i < countDownLatch.getCount(); i++) {
final T curTaskData = getCurTaskData(i, taskData);
final int m = i;
Runnable thread = new Runnable() {
public void run() {
long b1 = System.currentTimeMillis();
try {
startLatch.await();
dealTaskData(curTaskData);
} catch (Throwable e) {//注意不要跑飛了~~~
LogDebug.debug(
getBusinessName() + " doTask 異常", e);
// 發郵件
AppException appException = new AppException(e);
sendMail(getBusinessName() + " doTask 異常",
appException.getMessage(),
getAddresses());
} finally {
System.out
.println(getBusinessName()
+ " Thread-"
+ (m + 1)
+ " end "
+ " 耗時:"
+ (System.currentTimeMillis() - b1)
+ " 處理任務數:"
+ getTaskCount(curTaskData));
countDownLatch.countDown();
}
}
};
IBusiness.threadPool.submit(thread);// 這里一定要用線程池,否則當線程數超過tomcat限制,會導致線程數小於countDownLatch
}
startLatch.countDown();// 所有線程開始執行任務
countDownLatch.await();
}
} catch (Exception e) {
LogDebug.debug(getBusinessName() + " doTask 異常", e);
// 發郵件
AppException appException = new AppException(e);
sendMail(getBusinessName() + " doTask 異常", appException
.getMessage(), getAddresses());
} catch (Error e) {
LogDebug.debug(getBusinessName() + " doTask 致命錯誤", e);
// 發郵件
AppException appException = new AppException(e);
sendMail(getBusinessName() + " doTask 致命錯誤", appException
.getMessage(), getAddresses());
}
finally {
if(isMetrics()){
try{
THREAD_MONITOR = ThreadMonitor.getThreadMonitor();
Calendar calendar=Calendar.getInstance();//必須 Calendar.getInstance(),否則時間都一樣
CellInfo cellInfo =new CellInfo();
cellInfo.setApplication(getSystemName());
cellInfo.setKey(getBusinessName());
cellInfo.setServer(getLocal());
cellInfo.setExecuteCount(1);
cellInfo.setCostTime(new Long(System.currentTimeMillis() - b).intValue());
cellInfo.setCreateDate(sdFormat.format(calendar.getTime()));
THREAD_MONITOR.addInfo(cellInfo);
}catch (Throwable e) {
System.out.println("監控異常!!!!!!!!!");
e.printStackTrace();
}
}
System.out.println(getBusinessName() + " 結束 " + " 耗時:"
+ (System.currentTimeMillis() - b));
System.out
.println("***************************************************\n\n");
}
}
startOneBusiness中的任務是主任務
doTask中的任務是子任務
里面用了兩個CountDownLatch實例做主任務和子任務同步,還用到了線程池,
用的這個Executors.newFixedThreadPool固定線程池,
主任務和子任務都放到了線程池中
漏洞:
Executors.newFixedThreadPool這是個有固定活動線程數。當提
交到池中的任務數大於固定活動線程數時,任務就會放到阻塞隊列中等待。
主任務和子任務都放到了線程池中,就有下面的情況發生,
主任務調用一次CountDownLatch實例的await()方法時,當前線程就會一直占用一個活動線程,如果多次調用,
那么就會一直占用多個活動線程,如果調用次數大於固定活動線程數,那么就可能造成阻塞隊列
中某些子任務一直不被執行,CountDownLatch實例的countDown()的方法一直不被調用,那么對應的
主任務所在線程就會無限等待,與死鎖現像一樣
解決辦法是避免主任務不要和子任務放到同一線程池
另外還有另外一種情況,就是子任務有可以因為其它原因,不去執行CountDownLatch實例的
countDown()方法,造成主任務所在線程無限等待
解決辦法是最好不要用CountDownLatch實例的await(),歸避長時間阻塞線程的風險,任何多線程應用程序都有死鎖風險,改用CountDownLatch實例的await(long timeout, TimeUnit unit),設定超時時間,如果超時,
將返回false,這樣我們得知超時后,可以做異常處理,而await()是void類型,沒有返回值,
我們無法得知超時信息
另轉載如下文字,轉自http://www.duote.com/tech/5/12004.html
雖然線程池能大大提高服務器的並發性能,但使用它也會存在一定風險。與所有多線程應用程序一樣,用線程池構建的應用程序容易產生各種並發問題,如對共享資源的競爭和死鎖。此外,如果線程池本身的實現不健壯,或者沒有合理地使用線程池,還容易導致與線程池有關的死鎖、系統資源不足和線程泄漏等問題。
1.死鎖
任何多線程應用程序都有死鎖風險。造成死鎖的最簡單的情形是,線程A持有對象X的鎖,並且在等待對象Y的鎖,而線程B持有對象Y的鎖,並且在等待對象X的鎖。線程A與線程B都不釋放自己持有的鎖,並且等待對方的鎖,這就導致兩個線程永遠等待下去,死鎖就這樣產生了。
雖然任何多線程程序都有死鎖的風險,但線程池還會導致另外一種死鎖。在這種情形下,假定線程池中的所有工作線程都在執行各自任務時被阻塞,它們都在等待某個任務A的執行結果。而任務A依然在工作隊列中,由於沒有空閑線程,使得任務A一直不能被執行。這使得線程池中的所有工作線程都永遠阻塞下去,死鎖就這樣產生了。
2.系統資源不足
如果線程池中的線程數目非常多,這些線程會消耗包括內存和其他系統資源在內的大量資源,從而嚴重影響系統性能。
3.並發錯誤
線程池的工作隊列依靠wait()和notify()方法來使工作線程及時取得任務,但這兩個方法都難於使用。如果編碼不正確,可能會丟失通知,導致工作線程一直保持空閑狀態,無視工作隊列中需要處理的任務。因此使用這些方法時,必須格外小心,即便是專家也可能在這方面出錯。最好使用現有的、比較成熟的線程池。例如,直接使用java.util.concurrent包中的線程池類。
4.線程泄漏
使用線程池的一個嚴重風險是線程泄漏。對於工作線程數目固定的線程池,如果工作線程在執行任務時拋出 RuntimeException 或Error,並且這些異常或錯誤沒有被捕獲,那么這個工作線程就會異常終止,使得線程池永久失去了一個工作線程。如果所有的工作線程都異常終止,線程池就最終變為空,沒有任何可用的工作線程來處理任務。
導致線程泄漏的另一種情形是,工作線程在執行一個任務時被阻塞,如等待用戶的輸入數據,但是由於用戶一直不輸入數據(可能是因為用戶走開了),導致這個工作線程一直被阻塞。這樣的工作線程名存實亡,它實際上不執行任何任務了。假如線程池中所有的工作線程都處於這樣的阻塞狀態,那么線程池就無法處理新加入的任務了。[nextpage]
5.任務過載
當工作隊列中有大量排隊等候執行的任務時,這些任務本身可能會消耗太多的系統資源而引起系統資源缺乏。
綜上所述,線程池可能會帶來種種風險,為了盡可能避免它們,使用線程池時需要遵循以下原則。
(1)如果任務A在執行過程中需要同步等待任務B的執行結果,那么任務A不適合加入到線程池的工作隊列中。如果把像任務A一樣的需要等待其他任務執行結果的任務加入到工作隊列中,可能會導致線程池的死鎖。
(2)如果執行某個任務時可能會阻塞,並且是長時間的阻塞,則應該設定超時時間,避免工作線程永久的阻塞下去而導致線程泄漏。在服務器程序中,當線程等待客戶連接,或者等待客戶發送的數據時,都可能會阻塞。可以通過以下方式設定超時時間:
調用ServerSocket的setSoTimeout(int timeout)方法,設定等待客戶連接的超時時間;
對於每個與客戶連接的Socket,調用該Socket的setSoTimeout(int timeout)方法,設定等待客戶發送數據的超時時間。
(3)了解任務的特點,分析任務是執行經常會阻塞的I/O操作,還是執行一直不會阻塞的運算操作。前者時斷時續地占用CPU,而后者對CPU具有更高的利用率。預計完成任務大概需要多長時間?是短時間任務還是長時間任務?
根據任務的特點,對任務進行分類,然后把不同類型的任務分別加入到不同線程池的工作隊列中,這樣可以根據任務的特點,分別調整每個線程池。
(4)調整線程池的大小。線程池的最佳大小主要取決於系統的可用CPU的數目,以及工作隊列中任務的特點。假如在一個具有 N 個CPU的系統上只有一個工作隊列,並且其中全部是運算性質(不會阻塞)的任務,那么當線程池具有 N 或 N+1 個工作線程時,一般會獲得最大的 CPU 利用率。
如果工作隊列中包含會執行I/O操作並常常阻塞的任務,則要讓線程池的大小超過可用CPU的數目,因為並不是所有工作線程都一直在工作。選擇一個典型的任務,然后估計在執行這個任務的過程中,等待時間(WT)與實際占用CPU進行運算的時間(ST)之間的比例WT/ST。對於一個具有N個CPU的系統,需要設置大約N×(1+WT/ST)個線程來保證CPU得到充分利用。
當然,CPU利用率不是調整線程池大小過程中唯一要考慮的事項。隨着線程池中工作線程數目的增長,還會碰到內存或者其他系統資源的限制,如套接字、打開的文件句柄或數據庫連接數目等。要保證多線程消耗的系統資源在系統的承載范圍之內。
(5)避免任務過載。服務器應根據系統的承載能力,限制客戶並發連接的數目。當客戶並發連接的數目超過了限制值,服務器可以拒絕連接請求,並友好地告知客戶:服務器正忙,請稍后再試。