熱點推薦:
您现在的位置: 電腦知識網 >> 編程 >> Java編程 >> Java高級技術 >> 正文

超線程多核心下Java多線程編程分析

2013-11-23 19:40:23  來源: Java高級技術 

  Java環境下的多線程技術

  構建線程化的應用程序往往會對程序帶來重要的性能影響例如請考慮這樣一個程序它從磁盤讀取大量數據並且在把它們寫到屏幕之前處理這些數據(例如一個DVD播放器)在一個傳統的單線程程序(今天所使用的大多數客戶端程序)上一次只有一個任務執行每一個這些活動分別作為一個序列的不同階段發生只有在一塊已定義大小的數據讀取完成時才能進行數據處理因此能處理數據的程序邏輯直到磁盤讀操作完成後才得到執行這將導致非常差的性能問題

  在一個多線程程序中可以分配一個線程來讀取數據讓另一個線程來處理數據而讓第三個線程把數據輸送到圖形卡上去這三個線程可以並行運行這樣以來在磁盤讀取數據的同時仍然可以處理數據從而提高了整體程序的性能許多大量的示例程序都可以被設計來同時做兩件事情以進一步提高性能Java虛擬機(JVM)本身就是基於此原因廣泛使用了多線程技術

  本文將討論創建多線程Java代碼以及一些進行並行程序設計的最好練習另外還介紹了對開發者極為有用的一些工具和資源篇幅所限不可能全面論述這些問題所以我想只是重點提一下極重要的地方並提供給你相應的參考信息

  線程化Java代碼

  所有的程序都至少使用一個線程在C/C++和Java中這是指用對main()的調用而啟動的那個線程另外線程的創建需要若干步驟創建一個新線程然後指定給它某種工作一旦工作做完該線程將自動被JVM所殺死

  Java提供兩個方法來創建線程並且指定給它們工作第一種方法是子類化Java的Thread類(在javalang包中)然後用該線程的工作函數重載run()方法下面是這種方法的一個示例

  

  public class SimpleThread extends Thread {
 public SimpleThread(String str) {
  super(str);
 }
 public void run() {
  for (int i = ; i < ; i++) {
   Systemoutprintln(i + + getName());
   try {
    sleep((long)(Mathrandom() * ));
   } catch (InterruptedException e) {}
  }
  Systemoutprintln(DONE! + getName());
 }
}

  這個類子類化Thread並且提供它自己的run()方法上面代碼中的函數運行一個循環來打印傳送過來的字符串到屏幕上然後等待一個隨機的時間數目在循環十次後該函數打印DONE!然後退出並由它殺死這個線程下面是創建線程的主函數

  

  public class TwoThreadsDemo {
 public static void main (String[] args) {
  new SimpleThread(Do it!)start();
  new SimpleThread(Definitely not!)start();
 }
}

  注意該代碼極為簡單函數開始給定一個名字(它是該線程將要打印輸出的字符串)並且調用start()然後start()將調用run()方法程序的結果如下所示

  

   Do it!
Definitely not!
Definitely not!
Definitely not!
Do it!
Do it!
Do it!
Definitely not!
Do it!
Definitely not!
Do it!
Definitely not!
Do it!
Do it!
Definitely not!
Do it!
Definitely not!
Definitely not!
Do it!
DONE! Do it!
Definitely not!
DONE! Definitely not!

  正如你所看到的這兩個線程的輸出結果糾合到一起在一個單線程程序中所有的Do it!命令將一起打印後面跟著輸出Definitely not!

  這個程序的不同運行將產生不同的結果這種不確定性來源於兩個方面在循環中有一個隨機的暫停更為重要的是因為線程執行時間沒法保證這是一個關鍵的原則JVM將根據它自己的時間表運行這些進程(虛擬機一般支持盡可能快地運行這些線程但是沒法保證何時運行一個給定線程)對於每個線程可以使一個優先級與之相關聯以確保關鍵線程被JVM處理在次要的線程之前

  啟動一個線程的第二種方法是使用一個實現Runnable接口的類這個接口也定義在javalang中這個Runnable接口指定一個run()方法然後該方法成為線程的主函數類似於前面的代碼

  現在Java程序的一般風格是支持繼承的接口通過使用接口一個類在後面仍然能夠繼承(子類化)如果必要的話(例如如果該類要在後面作為一個applet使用的話就會發生這種情況)

  線程的含義

  在采用多線程技術增強性能的同時它也增加了程序內部運行的復雜性這種復雜性主要是由線程之間的交互引起的熟悉這些問題是很重要的因為隨著越來越多的核心芯片加入到Intel處理器中要使用的線程數目也將相應地增長如果在創建多線程程序時不能很好地理解這些問題那麼是調試時將很難發現錯誤因此讓我們先看一下這些問題及其解決辦法

  等待另一個線程完成假定我們有一個整型數組要進行處理我們可以遍歷這個數組每次一個整數並執行相應的操作更高效地我們可以建立多個線程這樣以來讓每個線程處理數組的一部分假定我們在開始下一步之前必須等待所有的線程結束為了暫時同步線程之間的活動這些線程使用了join()方法它使得一個線程等待另一個線程的完成加入的線程(線程B)等待被加入的線程(線程A)的完成在join()中的一個可選的超時值使得線程B可以繼續處理其它工作如果線程A在給定的時間幀內還沒有終止的話這個問題將觸及到線程的核心復雜性等待線程的問題下面我們將討論這個問題

  在鎖定對象上等待假定我們編寫一個航空公司座位分配系統在開發這種大型的程序時為每個連接到該軟件的用戶分配一個線程是很經常的如一個線程對應一個機票銷售員(在很大的系統中情況並非總是如此)如果有兩個用戶同時想分配同一個座位就會出現問題除非采取特殊的措施否則一個線程將分配該座位而另一個線程將會在做相同的事情兩個用戶都會認為他們在這趟航班上擁有一個分配的位子

  為了避免兩個線程同時修改一樣的數據項我們讓一個線程在修改數據前鎖定數據項用這種方法當第二個線程開始作修改時它將等待到第一個線程釋放鎖為止當這種發生時線程將會看到座位已被分配而對於座位分配的請求就會失敗兩個線程競爭分配座位的問題也就是著名的競爭條件問題而當競爭發生時有可能導致系統的洩漏為此最好的辦法就是鎖定任何代碼該代碼存取一個可由多個線程共同存取的變量

  在Java中存在好幾種鎖選擇其中最為常用的是使用同步機制當一個方法的簽名包含同步時在任何給定時間只有一個線程能夠執行這個方法然後當該方法完成執行時對該方法的鎖定即被解除例如

  

  protected synchronized int reserveSeat ( Seat seat_number ){
 if ( seat_numbergetReserved() == false ){
  seat_numbersetReserved();
  return ( );
 }
 else return ( );
}

  就是一個方法在這種方法中每次只運行一個線程這種鎖機制就打破了上面所描述的競爭條件

  使用同步是處理線程間交互的幾種方法中的一種JSE 中添加了若干方便的方法來鎖定對象大多數這些方法可以在包ncurrentlocks中找到一旦你熟悉了Java線程就應該對它進行詳細的研究

  在鎖機制解決了競爭條件的同時它們也帶來了新的復雜性在這種情況下最困難的問題就是死鎖假定線程A在等待線程B並且線程B在等待線程A那麼這兩個線程將永遠被鎖定這正是術語死鎖的意義死鎖問題可能很難判定並且必須相當小心以確保在線程之間沒有這種依賴性

  使用線程池

  如前所提及在線程完成執行時它們將被JVM殺死而分配給它們的內存將被垃圾回收機制所回收不斷地創建和毀滅線程所帶來的麻煩是它浪費了時鐘周期因為創建線程確實耗費額外的時間一個通用的且最好的實現是在程序運行的早期就分配一組線程(稱為一個線程池)然後在這些線程可用時再使用它們通過使用這種方案在創建時分配給一個線程指定的功能就是呆在線程池中並且等待分配一項工作然後當分配的工作完成時該線程被返回到線程池

  JSE 引入了ncurrent包它包括了一個預先構建的線程池框架這大大便利了上述方法的實現有關Java線程池的更多信息及一部教程請參見

  在設計線程程序和線程池時自然出現關於應該創建多少線程的問題答案看你怎樣計劃使用這些線程如果你基於分離的任務來用線程劃分工作那麼線程的數目等於任務的數目例如一個字處理器可能使用一個線程用於顯示(在幾乎所有系統中的主程序線程負責更新用戶接口)一個用於標記文檔第三個用於拼寫檢查而第四個用於其它後台操作在這種情況中創建四個線程是理想的並且它們提供了編寫該類軟件的一個很自然的方法

  然而如果程序象早些時候所討論的那個一樣使用多個線程來做類似的工作那麼線程的最佳數目將是系統資源的反映特別是處理器上可執行管道的數目和處理器的數目的反映在采用英特爾處理器超線程技術(HT技術)的系統上當前在每個處理器核心上有兩個執行管道最新的多核心處理器在每個芯片上有兩個處理器核心英特爾指出將來的芯片有可能具有多個核心大部分是因為額外的核心會帶來更高的性能而不會從根本上增加熱量或電量的消耗因此管道數將會越來越多

  照上面這些體系結構所作的算術建議在一個雙核心Pentium 處理器系統上可以使用四條執行管道並因此可以使用四個線程將會提供理想的性能在一個雙處理器英特爾Xeon?處理器的工作站上理想的線程數目是因為目前Xeon芯片提供HT技術但是沒提供多核心模型你可以參考下面文檔來了解這些新型處理器上的執行管道的數目(na/eng/)

  小結

  你當在平台上運行線程化的Java程序時你將可能想要監控在處理器上的加載過程與線程的執行最好的獲得這些數據與管理JVM怎樣處理並行處理的JVM之一是BEA的WebLogic JRockitJRockit還有其它一些由來自於BEA和Intel公司的工程師專門為Intel平台設計和優化的優點

  不考慮你使用哪一種JVMIntel的VTune Performance Analyzer將會給你一個關於JVM怎樣執行你的代碼的很深入的視圖這包括每個線程的性能瓶頸等另外Intel還提供了關於如何在Java環境下使用VTune Performance Analyzer的白皮書[PDF MB]

  總之本文提供了線程在Java平台工作機理的分析由於Intel還將繼續生產HT技術的處理器並且發行更多的多核心芯片所以想從這些多管道中得到性能效益的壓力也會增加並且由於核心芯片數目的增加管道的數目也將相應地增加唯一的利用它們的優點的辦法就是使用多線程技術如在本文中所討論的並且Java多線程程序的優勢也越來越明顯


From:http://tw.wingwit.com/Article/program/Java/gj/201311/27272.html
    推薦文章
    Copyright © 2005-2013 電腦知識網 Computer Knowledge   All rights reserved.