4、Java 并发编程基础
4、Java 并發編程基礎
- 4.1 線程簡介
- 什么是線程
- 為什么要使用多線程
- 線程優先級
- 線程的狀態
- Daemon線程
- 4.2 啟動和終止線程
- 構造線程
- 啟動線程
- 理解中斷
- 過期的suspend()、resume()和stop()
- 安全地終止線程
- 4.3 線程間通信
- volatile和synchronized關鍵字
- 等待/通知機制
- 等待/通知的經典范式
- 管道輸入/輸出流
- Thread.join()的使用
- ThreadLocal的使用
- 4.4 線程應用實例
- 等待超時模式
- 連接池
- 線程池技術及其示例
線程作為操作系統調度的最小單元,多個線程能夠同時執行,這將 顯著提升程序性能,在多核環境中表現得更加明顯。但是,過多地創建線程和對線程的不當管 理也容易造成問題
4.1 線程簡介
什么是線程
- 線程都擁有各自的計數器、堆棧和局 部變量等屬性,并且能夠訪問共享的內存變量,,一個Java程序的運行不僅僅是main()方法的運行,而是main線程和多個其他線 程的同時運行。
為什么要使用多線程
(1)更多的處理器核心
- 線程是大多數操作系統調度的基本單元,一個程序作為一個進程來運行,程序運行過程 中能夠創建多個線程,而一個線程在一個時刻只能運行在一個處理器核心上
- 如果該程序使用多線程技術,將計算邏輯分配到多個處理器核心 上,就會顯著減少程序的處理時間,并且隨著更多處理器核心的加入而變得更有效率
(2)更快的響應時間
- 將數據一致性不強的操作派發給其他線程處 理(也可以使用消息隊列),如生成訂單快照、發送郵件等。這樣做的好處是響應用戶請求的線 程能夠盡可能快地處理完成,縮短了響應時間,提升了用戶體驗
(3)更好的編程模型
線程優先級
- 現代操作系統基本采用時分的形式調度運行的線程,操作系統會分出一個個時間片,線 程會分配到若干時間片,當線程的時間片用完了就會發生線程調度,并等待著下次分配。線程 分配到的時間片多少也就決定了線程使用處理器資源的多少,而線程優先級就是決定線程需 要多或者少分配一些處理器資源的線程屬性
- 在Java線程中,通過一個整型成員變量priority來控制優先級,優先級的范圍從1~10,在線 程構建的時候可以通過setPriority(int)方法來修改優先級,默認優先級是5,優先級高的線程分 配時間片的數量要多于優先級低的線程。設置線程優先級時,針對頻繁阻塞(休眠或者I/O操 作)的線程需要設置較高優先級,而偏重計算(需要較多CPU時間或者偏運算)的線程則設置較 低的優先級,確保處理器不會被獨占。在不同的JVM以及操作系統上,線程規劃會存在差異, 有些操作系統甚至會忽略對線程優先級的設定
注意
線程優先級不能作為程序正確性的依賴,因為操作系統可以完全不用理會Java 線程對于優先級的設定。筆者的環境為:Mac OS X 10.10,Java版本為1.7.0_71,經過筆者驗證 該環境下所有Java線程優先級均為5(通過jstack查看),對線程優先級的設置會被忽略。另外, 嘗試在Ubuntu 14.04環境下運行該示例,輸出結果也表示該環境忽略了線程優先級的設置。
線程的狀態
-
線程在自身的生命周期中, 并不是固定地處于某個狀態,而是隨著代碼的執行在不同的狀態之間進行切換
-
Java線程狀態變遷
-
線程創建之后,調用start()方法開始運行。當線程執行wait()方法之 后,線程進入等待狀態。進入等待狀態的線程需要依靠其他線程的通知才能夠返回到運行狀 態,而超時等待狀態相當于在等待狀態的基礎上增加了超時限制,也就是超時時間到達時將 會返回到運行狀態。當線程調用同步方法時,在沒有獲取到鎖的情況下,線程將會進入到阻塞 狀態。線程在執行Runnable的run()方法之后將會進入到終止狀態。
注意
Java將操作系統中的運行和就緒兩個狀態合并稱為運行狀態。阻塞狀態是線程 阻塞在進入synchronized關鍵字修飾的方法或代碼塊(獲取鎖)時的狀態,但是阻塞在 java.concurrent包中Lock接口的線程狀態卻是等待狀態,因為java.concurrent包中Lock接口對于 阻塞的實現均使用了LockSupport類中的相關方法。
Daemon線程
- Daemon線程是一種支持型線程,因為它主要被用作程序中后臺調度以及支持性工作。當一個Java虛擬機中不存在非Daemon線程的時候,Java虛擬機將會退出。可以通過調 用Thread.setDaemon(true)將線程設置為Daemon線程。
- Daemon屬性需要在啟動線程之前設置,不能在啟動線程之后設置
- Daemon線程被用作完成支持性工作,但是在Java虛擬機退出時Daemon線程中的finally塊 并不一定會執行
- 在構建Daemon線程時,不能依靠finally塊中的內容來確保執行關閉或清理資源
的邏輯。
4.2 啟動和終止線程
構造線程
- 在運行線程之前首先要構造一個線程對象,線程對象在構造的時候需要提供線程所需要 的屬性,如線程所屬的線程組、線程優先級、是否是Daemon線程等信息
- 一個新構造的線程對象是由其parent線程來進行空間分配的,而child線程 繼承了parent是否為Daemon、優先級和加載資源的contextClassLoader以及可繼承的 ThreadLocal,同時還會分配一個唯一的ID來標識這個child線程。至此,一個能夠運行的線程對 象就初始化好了,在堆內存中等待著運行
啟動線程
- 線程對象在初始化完成之后,調用start()方法就可以啟動這個線程。線程start()方法的含義 是:當前線程(即parent線程)同步告知Java虛擬機,只要線程規劃器空閑,應立即啟動調用 start()方法的線程
注意
啟動一個線程前,最好為這個線程設置線程名稱,因為這樣在使用jstack分析程 序或者進行問題排查時,就會給開發人員提供一些提示,自定義的線程最好能夠起個名字。
理解中斷
- 線程的一個標識位屬性,它表示一個運行中的線程是否被其他線程進行 了中斷操作。中斷好比其他線程對該線程打了個招呼,其他線程通過調用該線程的interrupt() 方法對其進行中斷操作
- ,線程通過方法isInterrupted()來進行判斷是否 被中斷,也可以調用靜態方法Thread.interrupted()對當前線程的中斷標識位進行復位。如果該 線程已經處于終結狀態,即使該線程被中斷過,在調用該線程對象的isInterrupted()時依舊會返 回false
- 拋出InterruptedException,此時調用isInterrupted()方法將會返回false
過期的suspend()、resume()和stop()
暫停、恢復和停止操作
- 不建議使用的原因主要有:
以suspend()方法為例,在調用后,線程不會釋放已經占有的資 源(比如鎖),而是占有著資源進入睡眠狀態,這樣容易引發死鎖問題。同樣,stop()方法在終結 一個線程時不會保證線程的資源正常釋放,通常是沒有給予線程完成資源釋放工作的機會, 因此會導致程序可能工作在不確定狀態下。
注意
正因為suspend()、resume()和stop()方法帶來的副作用,這些方法才被標注為不建 議使用的過期方法,而暫停和恢復操作可以用后面提到的等待/通知機制來替代。
安全地終止線程
- 中斷狀態是線程的一個標識位,而中斷操作是一種簡便的線程間交互 方式,而這種交互方式最適合用來取消或停止任務。
- 利用一個boolean 變量來控制是否需要停止任務并終止該線程。
4.3 線程間通信
volatile和synchronized關鍵字
- 關鍵字volatile可以用來修飾字段(成員變量),就是告知程序任何對該變量的訪問均需要 從共享內存中獲取,而對它的改變必須同步刷新回共享內存,它能保證所有線程對變量訪問 的可見性
過多地使用volatile是不必要的,因為 它會降低程序執行的效率 - 關鍵字synchronized可以修飾方法或者以同步塊的形式來進行使用,它主要確保多個線程 在同一個時刻,只能有一個線程處于方法或者同步塊中,它保證了線程對變量訪問的可見性 和排他性
- 同步塊的實現使用了monitorenter和monitorexit指令,同步方法則 是依靠方法修飾符上的ACC_SYNCHRONIZED來完成。本質是對一個對象的監視器(monitor)進行獲取,而這個獲取過程是排他的,也就是同一時刻只能有一個 線程獲取到由synchronized所保護對象的監視器
- 任意一個對象都擁有自己的監視器,當這個對象由同步塊或者這個對象的同步方法調用 時,執行方法的線程必須先獲取到該對象的監視器才能進入同步塊或者同步方法,而沒有獲 取到監視器(執行該方法)的線程將會被阻塞在同步塊和同步方法的入口處,進入BLOCKED 狀態
等待/通知機制
等待/通知的相關方法是任意Java對象都具備的,因為這些方法被定義在所有對象的超類 java.lang.Object上
- 等待/通知機制,是指一個線程A調用了對象O的wait()方法進入等待狀態,而另一個線程B 調用了對象O的notify()或者notifyAll()方法,線程A收到通知后從對象O的wait()方法返回,進而 執行后續操作。上述兩個線程通過對象O來完成交互,而對象上的wait()和notify/notifyAll()的 關系就如同開關信號一樣,用來完成等待方和通知方之間的交互工作
- 調用wait()、notify()以 及notifyAll()時需要注意的細節
1)使用wait()、notify()和notifyAll()時需要先對調用對象加鎖。
2)調用wait()方法后,線程狀態由RUNNING變為WAITING,并將當前線程放置到對象的 等待隊列。
3)notify()或notifyAll()方法調用后,等待線程依舊不會從wait()返回,需要調用notify()或 notifAll()的線程釋放鎖之后,等待線程才有機會從wait()返回。
4)notify()方法將等待隊列中的一個等待線程從等待隊列中移到同步隊列中,而notifyAll() 方法則是將等待隊列中所有的線程全部移到同步隊列,被移動的線程狀態由WAITING變為 BLOCKED。
5)從wait()方法返回的前提是獲得了調用對象的鎖。 - 等待/通知機制依托于同步機制,其目的就是確保等待線程從 wait()方法返回時能夠感知到通知線程對變量做出的修改
等待/通知的經典范式
等待方
- 獲取對象的鎖。
- 如果條件不滿足,那么調用對象的wait()方法,被通知后仍要檢查條件。
- 條件滿足則執行對應的邏輯。
通知方 - 獲得對象的鎖。
- 改變條件。
- 通知所有等待在對象上的線程。
管道輸入/輸出流
主要用于線程之間的數據傳輸,而傳輸的媒介為內存。
- PipedOutputStream、PipedInputStream、 PipedReader和PipedWriter,前兩種面向字節,而后兩種面向字符
Thread.join()的使用
- 如果一個線程A 執行了thread.join() 語句,其含義是:當前線程A 等待thread 線程終止之后才 從thread.join() 返回。
- 線程Thread除了提供join()方法之外,還提供了join(long millis) 和join(long millis,int nanos) 兩個具備超時特性的方法。這兩個超時方法表示,如果線程thread 在給定的超時時間里沒有終止,那么將會從該超時方法中返回
- 當線程終止時,會調用線程自身的notifyAll()方法,會通知所有等待在該線程對象上的線 程。
ThreadLocal的使用
- ThreadLocal,即線程變量,是一個以ThreadLocal對象為鍵、任意對象為值的存儲結構。這 個結構被附帶在線程上,也就是說一個線程可以根據一個ThreadLocal對象查詢到綁定在這個 線程上的一個值。
- 可以通過set(T)方法來設置一個值,在當前線程下再通過get()方法獲取到原先設置的值
4.4 線程應用實例
等待超時模式
- 等待/通知的經典范式,即加鎖、條件循環和處理邏輯3個步驟,而這種 范式無法做到超時等待
假設超時時間段是T,那么可以推斷出在當前時間now+T之后就會超時。
定義如下變量。
···等待持續時間:REMAINING=T。
···超時時間:FUTURE=now+T。
··· 這時僅需要wait(REMAINING)即可,在wait(REMAINING)返回之后會將執行: REMAINING=FUTURE–now。如果REMAINING小于等于0,表示已經超時,直接退出,否則將 繼續執行wait(REMAINING)
連接池
連接池的定義
它通過構造函數初始化連接的最大上限,通過一個雙向隊列 來維護連接,調用方需要先調用fetchConnection(long)方法來指定在多少毫秒內超時獲取連接, 當連接使用完成后,需要調用releaseConnection(Connection)方法將連接放回線程池
線程池技術及其示例
- 對于服務端的程序,經常面對的是客戶端傳入的短小(執行時間短、工作內容較為單一) 任務,需要服務端快速處理并返回結果。如果服務端每次接受到一個任務,創建一個線程,然 后進行執行,這在原型階段是個不錯的選擇,但是面對成千上萬的任務遞交進服務器時,如果 還是采用一個任務一個線程的方式,那么將會創建數以萬記的線程,這不是一個好的選擇。因 為這會使操作系統頻繁的進行線程上下文切換,無故增加系統的負載,而線程的創建和消亡 都是需要耗費系統資源的,也無疑浪費了系統資源。
- 線程池技術能夠很好地解決這個問題,它預先創建了若干數量的線程,并且不能由用戶 直接對線程的創建進行控制,在這個前提下重復使用固定或較為固定數目的線程來完成任務 的執行。這樣做的好處是,一方面,消除了頻繁創建和消亡線程的系統資源開銷,另一方面, 面對過量任務的提交能夠平緩的劣化。
- 線程池的本質就是使用了一個線程安全的工作隊列連接工作者線程和客戶端 線程,客戶端線程將任務放入工作隊列后便返回,而工作者線程則不斷地從工作隊列上取出 工作并執行。當工作隊列為空時,所有的工作者線程均等待在工作隊列上,當有客戶端提交了 一個任務之后會通知任意一個工作者線程,隨著大量的任務被提交,更多的工作者線程會被 喚醒
- 線程池中線程數量并不是越多越好,具體的數量需要評估每個任務的處理時間,以 及當前計算機的處理器能力和數量。使用的線程過少,無法發揮處理器的性能;使用的線程過 多,將會增加系統的無故開銷,起到相反的作用。
總結
以上是生活随笔為你收集整理的4、Java 并发编程基础的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 服务器主机硬件介绍,服务器硬件维护常识
- 下一篇: 【钢铁侠3】【高清1280版HD-RMV