Java 多執行緒程式設計學習總結(一)
定義篇
程序(Process)和執行緒(Thread)
怎樣實現多工處理(Multitasking)? 多工處理是同時執行多個任務的過程。我們使用多工處理來利用 CPU。可通過兩種方式實現多工處理: · 基於程序的多工 (多重處理) · 基於執行緒的多工處理 (多執行緒處理)
程序
在一個特定的程序狀態中,程序是一個執行流,我們按順序執行指令順序,一次只發生一件事。在這個狀態中可能受到影響的內容包括程式碼、資料、呼叫堆疊、開啟檔案、網路連線等。
程序 vs 程式 程式本身不是一個過程。它是由程式語句組成的靜態實體, 而程序是動態實體。程式包含由處理器執行的指令。 程式在主記憶體中的單個位置佔用空間, 並繼續留在那裡。程式本身不執行任何操作。
程序執行環境:程序通常有一組完整的、私有的基本執行資源,例如程序有自己的記憶體空間。過程記憶體分為四節, 如下圖所示:
- Text Section(可執行檔案): 包含程式啟動時載入的已編譯程式程式碼。
- Data Section(已初始化的全域性變數區): 包含全域性和靜態變數, 在執行 main 之前分配和初始化
- Stack(棧區:區域性變數): 包含區域性變數、函式呼叫幀。它從較高的記憶體地址開始, 並在新增元素時縮小。
- Heap(堆區:動態分配的記憶體空間): 用於動態記憶體分配, 例如, 新建、刪除、malloc、空閒等。
棧和堆從程序的可用空間的兩端開始, 並相互增長。要是他們遇到, 要麼將發生堆疊溢位錯誤,要麼由於可用記憶體不足呼叫 new 或 malloc 將失敗。
執行緒
幾乎每個作業系統都支援程序的概念——獨立執行的程式在某種程度上相互隔離。
執行緒處理是一種允許多個活動在單個程序中共存的工具。大多數現代作業系統支援執行緒, 並且執行緒的概念已經在不同的形式中存在多年。 Java 是第一個明確將執行緒包含在語言本身中的主流程式語言, 而不是將執行緒處理作為基礎作業系統的一個工具。
執行緒有時稱為輕量級程序。與程序一樣, 執行緒是獨立的、通過程式執行的併發路徑, 並且每個執行緒都有自己的堆疊、自己的程式計數器和自己的區域性變數。但是, 程序中的執行緒比單獨的程序更少的隔離。它們共享記憶體、檔案控制代碼和其他每個程序狀態。
程序可以支援多個執行緒, 它們似乎同時執行, 並以非同步方式進行。程序中的多個執行緒共享相同的記憶體地址空間, 這意味著它們可以訪問相同的變數和物件, 並從同一堆中分配物件。雖然這使得執行緒可以很容易地彼此共享資訊, 但必須注意確保它們不會干擾同一程序中的其他執行緒。
一個 exe執行一次就會產生一個程序,一個程序裡至少有一個執行緒:主執行緒。我們平時寫的控制檯程式預設就是單執行緒的,程式碼從上往下執行,一行執行完了再執行下一行。
如上圖所示, 執行緒在程序內執行。執行緒之間有上下文切換(Context Switch)。作業系統中可以有多個程序, 一個程序可以有多個執行緒。執行緒是輕量級子程序, 最小的處理單元。 執行緒是獨立的。如果一個執行緒中出現異常, 它不會影響其他執行緒。它使用共享記憶體區域。
區別
Process程序 | thread執行緒 |
---|---|
程序具有單獨的虛擬地址空間。同一系統上同時執行的兩個程序不會相互重疊。 | 執行緒是程序中的實體。程序的所有執行緒共享其虛擬地址空間和系統資源, 但它們有自己的堆疊建立。 |
每個程序都有自己的資料段 | 由程序建立的所有執行緒共享相同的資料段。 |
需要跨程序通訊與其他程序進行互動。 | 執行緒不需要跨程序通訊技術, 因為它們不是完全獨立的地址空間。他們共享相同的地址空間。因此, 它們可以直接與程序的其他執行緒進行通訊。 |
程序沒有執行緒那樣的同步開銷 | 同個程序的執行緒共享相同的地址空間。因此, 在程序的地址空間中同步對共享資料的訪問變得非常重要。 |
從父程序內部建立子程序需要複製父程序的資源 | 執行緒可以很容易地建立, 並且不需要重複的資源。 |
為什麼使用執行緒(Thread)
If you use Swing, servlets, RMI, or Enterprise JavaBeans (EJB) technology, you may already be using threads without realizing it. 如果使用 Swing、servlet、RMI 或Enterprise JavaBeans (EJB) 技術, 則可能已經在未實現的情況下使用了執行緒。
Some of the reasons for using threads are that they can help to: · Make the UI more responsive · Take advantage of multiprocessor systems · Simplify modeling · Perform asynchronous or background processing 使用執行緒的原因是它們可以在如下方面起到幫助: · 使 UI 響應更靈敏 · 充分利用多處理器系統 · 簡化建模 · 執行非同步或後臺處理
1、響應更快的UI
事件驅動(Event-driven)的 UI 工具包 (如 AWT 和 Swing) 具有處理 UI 事件 (如擊鍵和滑鼠單擊) 的事件執行緒。
AWT 和 Swing 程式將事件監聽器附加到 ui 物件。當發生特定事件 (如單擊按鈕) 時, 會通知這些監聽器。事件監聽器被來自於AWT 的事件執行緒呼叫。
如果事件監聽器要執行冗長的任務 (如在大型文件中檢查拼寫), 則事件執行緒將忙於執行拼寫檢查器, 因此在事件監聽器完成之前無法處理其他 UI 事件。這將使程式看起來被凍結了, 會讓使用者感到不安。
為了避免延遲 UI, 事件監聽器應將長任務交給另一個執行緒, 以便 AWT 執行緒可以在任務正在進行時繼續處理 UI 事件 (包括取消正在執行的長時間執行的任務的請求)。
2、利用多處理器系統(Multiprocessor )
現代作業系統可以利用多個處理器, 並安排執行緒在任何可用的處理器上執行。
排程的基本單位通常是執行緒。 如果一個程式只有一個活動執行緒, 則一次只能在一個處理器上執行。 如果程式有多個活動執行緒, 則可以同時計劃多個執行緒。
在設計良好的程式中, 使用多個執行緒可以提高程式吞吐量和效能。
3、建模的簡單性
在某些情況下, 使用執行緒可以使程式更易於編寫和維護。考慮一個模擬應用程式, 在該應用程式中模擬多個實體之間的互動。為每個實體提供自己的執行緒可以大大簡化許多模擬和建模應用程式。
再舉一個例子:當應用程式具有多個獨立的事件驅動元件時,可以方便地使用單獨的執行緒來簡化程式。例如, 應用程式可能有一個元件, 該元件會記下某些事件後的秒數, 並更新螢幕上的顯示。與其讓主迴圈定期檢查時間並更新顯示, 不如讓一個執行緒只執行睡眠, 直到經過一定的時間, 然後更新螢幕上的計數器, 就簡單得多, 而且更不容易出錯。這樣, 主執行緒根本不需要擔心計時器。
4、非同步或後臺處理
伺服器應用程式從遠端源 (如套接字) 獲取其輸入。從套接字讀取時, 如果當前沒有可用的資料, 則SocketInputStream.read()呼叫將被阻止, 直到資料可用為止。
如果單執行緒程式從套接字中讀取, 並且套接字另一端的實體永遠不會發送任何資料, 則該程式將永遠等待, 並且不會完成其他處理。另一方面, 程式可以輪詢套接字, 以檢視資料是否可用, 但出於效能原因, 這通常是不可取的。
相反, 如果建立了要從套接字讀取的執行緒, 則主執行緒可以執行其他任務, 而另一個執行緒正在等待套接字中的輸入。甚至可以建立多個執行緒, 以便一次從多個套接字中讀取。通過這種方式, 當資料可用時 (因為等待執行緒被喚醒), 將很快收到通知, 而無需頻繁輪詢以檢查資料是否可用。使用執行緒在套接字上等待的程式碼也比輪詢更簡單, 也更不容易出錯。
5、簡單, 但有時有風險
雖然 java 執行緒工具很容易使用, 但在建立多執行緒程式時, 應該儘量避免一些風險。
當多個執行緒訪問相同的資料項 (如靜態欄位、全域性可訪問物件的例項欄位或共享集合) 時, 您需要確保它們協調對資料的訪問, 以便兩者都能看到一致的資料檢視, 兩者都看不到其他的更改。java 語言為此提供了兩個關鍵字:synchronized 和 volatile。
從多個執行緒訪問變數時, 必須確保正確同步訪問。對於簡單變數, 宣告變數volatile可能就足夠了, 但在大多數情況下, 需要使用同步。
如果要使用同步來保護對共享變數的訪問, 則必須確保在訪問該變數的程式中的任何位置都使用同步。
6、不要過度使用
雖然執行緒可以大大簡化多種型別的應用程式, 但過度使用執行緒可能會危及程式的效能及其可維護性。執行緒會消耗資源。因此, 在不降低效能的情況下, 可以建立的執行緒數量是有限制的。特別是, 使用多個執行緒不會使 cpu 繫結的程式在單處理器系統上執行得更快。