后端学习 - 并发编程
文章目錄
- 一 進程與線程
- 1 區別與聯系
- 2 Java內存區域
- 3 線程組
- 4 線程的上下文切換
- 5 并發與并行
- 6 線程的生命周期與狀態
- 二 線程間的通信和同步
- 1 線程同步:鎖
- 2 線程同步:等待-通知機制
- 3 線程同步:volatile 信號量
- 4 線程通信:管道
- 三 線程死鎖
- 1 死鎖的四個必要條件
- 2 死鎖解決:預防死鎖、避免死鎖、檢測與解除死鎖
- 四 并發編程的相關方法
- 1 sleep() 與 wait()
- 2 run() 與 start()
- 3 join()
- 4 線程創建的四種方法
- 5 獲取與設置優先級(不可靠)、守護線程
- 五 synchronized 關鍵字
- 1 簡介
- 2 使用方法
- 3 注意事項
- 六 volatile 關鍵字
- 1 作用
- 2 雙重校驗鎖實現 單例模式(線程安全)
- 3 與 synchronized 的關系
- 七 ReentrantLock 可重入鎖
- 1 可重入鎖
- 2 和 synchronized 異同
- 八 ThreadLocal
- 1 概念
- 2 get 和 set 方法的源碼
- 3 ThreadLocalMap - key 的弱引用和 GC
- 九 并發容器
- 1 ConcurrentHashMap
- 2 CopyOnWriteArrayList
- 3 ConcurrentLinkedQueue
- 4 BlockingQueue
- 5 ConcurrentSkipListMap
- 十 線程池
- 1 為什么使用線程池
- 2 ThreadPoolExecutor 構造方法
- 3 ThreadPoolExecutor 的狀態
- 4 任務處理流程
一 進程與線程
1 區別與聯系
- 進程是系統分配資源的基本單位,線程是 CPU 調度的基本單位
- 進程和線程本質的區別是是否單獨占有內存地址空間及其它系統資源(比如I/O)
- 線程是輕量級進程,進程在其執行的過程中可以產生多個線程。與進程不同的是屬于同一進程的多個線程共享進程的堆和方法區資源 (JDK1.8 之后的元空間),但每個線程有自己的程序計數器、虛擬機棧和本地方法棧,所以系統在產生一個線程,或是在各個線程之間作切換工作時,負擔要比進程小得多
- 使用多線程而非多進程實現并發的優勢:進程間的通信比較復雜,而線程間的通信比較簡單;進程是重量級的,而線程是輕量級的,故多線程方式的系統開銷更小
2 Java內存區域
- 線程私有程序計數器的目的是,線程切換后能恢復到正確的執行位置
- 線程私有虛擬機棧和本地方法棧的目的是,保證線程中的 局部變量(存放在棧幀中的局部變量表) 不被別的線程訪問到
- 堆和方法區是所有 線程共享的資源,其中堆是進程中最大的一塊內存,主要用于存放新創建的對象 (幾乎所有對象都在這里分配內存),方法區主要用于存放已被加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據
3 線程組
- 線程組是一個樹狀的結構,每個線程組下面可以有多個線程或者 線程組
- 每個 Thread 必然存在于一個 ThreadGroup 中,Thread 不能獨立于 ThreadGroup 存在
- 如果在 new Thread 時沒有顯式指定,那么默認將父線程(當前執行new Thread的線程)線程組設置為自己的線程組
- ThreadGroup 是一個標準的向下引用的樹狀結構,這樣設計的原因是防止"上級"線程被"下級"線程引用而無法有效地被GC回收
- 線程組可以起到統一控制線程的優先級和檢查線程的權限的作用
4 線程的上下文切換
- 線程在執行過程中會有自己的運行條件和狀態(也稱上下文),比如上文所說到過的程序計數器,棧信息等
- 線程切換時,需要保存當前線程的上下文,留待線程下次占用 CPU 的時候恢復現場,并加載下一個將要占用 CPU 的線程上下文
舉例說明:線程A切換到線程B
1.先掛起線程A,將其在CPU中的狀態保存在內存中
2.在內存中檢索下一個線程B的上下文,并將其在 CPU 的寄存器中恢復,開始執行B線程
3.當B執行完,根據程序計數器中指向的位置恢復線程A
5 并發與并行
- 并發: 同一時間段,多個任務都在執行 (單位時間內不一定同時執行,可以來回切換);
- 并行: 單位時間內,多個任務同時執行
6 線程的生命周期與狀態
反復調用同一個線程的start()方法是否可行?假如一個線程執行完畢(此時處于TERMINATED狀態),再次調用這個線程的 start() 方法是否可行?
兩個問題的答案都是不可行,因為 threadStatus 的值會改變,調用 start() 的前提是 threadStatus==0 ,此時再次調用 start() 方法會拋 IllegalThreadStateException異常
二 線程間的通信和同步
1 線程同步:鎖
- 線程同步的根本目的是讓線程按照一定的順序執行
- 示例
2 線程同步:等待-通知機制
- 基于 Object 類的 wait() 方法和 notify()(隨機叫醒一個正在等待的線程) ,notifyAll()(叫醒所有正在等待的線程) 方法實現
- 一個鎖同一時刻只能被一個線程持有。假如線程A現在持有了一個鎖 lock 并開始執行,它可以使用 lock.wait() 讓自己進入等待狀態,lock 被釋放
- 線程B獲得了 lock 這個鎖并開始執行,它可以在某一時刻,使用 lock.notify(),通知之前持有 lock 鎖并進入等待狀態的線程A,指示線程A繼續執行(需要注意的是,這個時候線程B并沒有釋放鎖 lock,除非線程B這個時候使用 lock.wait() 釋放鎖,或者線程B執行結束自行釋放鎖,線程A才能得到 lock 鎖)
3 線程同步:volatile 信號量
- volatile 關鍵字能夠保證內存的可見性,如果用 volatile 關鍵字聲明了一個變量,在一個線程里面改變了這個變量的值,那其它線程是立馬可見更改后的值的;同時,它可以禁止指令重排
- 多個線程(超過2個)需要相互合作,用簡單的“鎖”和“等待通知機制”就不那么方便了。這個時候就可以用到信號量
4 線程通信:管道
- JDK提供了 PipedWriter、 PipedReader、 PipedOutputStream、 PipedInputStream。其中,前面兩個是基于字符的(處理單元為2字節),后面兩個是基于字節流的(處理單元為1字節)
三 線程死鎖
1 死鎖的四個必要條件
- 互斥條件:該資源任意一個時刻只由一個線程占用
- 請求與保持條件:一個進程因請求資源而阻塞時,對已獲得的資源保持不放
- 不剝奪條件:線程已獲得的資源在未使用完之前不能被其他線程強行剝奪,只有自己使用完畢后才釋放資源
- 循環等待條件:若干進程之間形成一種頭尾相接的循環等待資源關系
2 死鎖解決:預防死鎖、避免死鎖、檢測與解除死鎖
- 破壞請求與保持條件 :一次性申請所有的資源
- 破壞不剝奪條件 :占用部分資源的線程進一步申請其他資源時,如果申請不到,可以主動釋放它占有的資源
- 破壞循環等待條件 :靠按序申請資源來預防。按某一順序申請資源,釋放資源則反序釋放。破壞循環等待條件
操作系統:死鎖
四 并發編程的相關方法
1 sleep() 與 wait()
- sleep() 方法沒有釋放鎖,而 wait() 方法釋放了鎖
- 都可以暫停線程的執行
- wait() 通常被用于線程間交互/通信,sleep() 通常被用于暫停執行
- wait() 方法被調用后,線程不會自動蘇醒,需要別的線程調用同一個對象上的 notify() 或者 notifyAll() 方法。sleep() 方法執行完成后,線程會自動蘇醒。或者可以使用 wait(long timeout) 超時后線程會自動蘇醒
- wait() 可以指定時間,也可以不指定;而 sleep() 必須指定時間
- wait() 釋放CPU資源,同時釋放鎖;sleep() 釋放CPU資源,但是不釋放鎖,所以易死鎖
為什么 sleep 函數的精度很低?
- sleep函數并不能起到定時的作用,主要作用是延時。在一些多線程中可能會看到sleep(0),其主要目的是讓出時間片
- 當系統越繁忙的時候它精度也就越低,因為它的精度取決于線程自身優先級、其他線程的優先級,以及線程的數量等因素,所以說sleep 函數是不能用來精確計時的
2 run() 與 start()
- 調用 start() 方法,會啟動一個線程并使線程進入了就緒狀態,當分配到時間片后就可以開始運行了。 start() 會執行線程的相應準備工作,然后自動執行 run() 方法的內容,實現多線程工作
- 直接執行 run() 方法,會把 run() 方法當成一個 main 線程下的普通方法去執行,并不會在某個線程中執行它,所以不是多線程工作
3 join()
- 在 a 線程中調用 b 線程的 join() 方法,線程 a 進入阻塞狀態,直到線程 b 完全執行完,線程 a 從阻塞狀態中恢復
- 如果主線程想等待子線程執行完畢后,獲得子線程中的處理完的某個數據,就使用 join()
4 線程創建的四種方法
- Runnable 接口不會返回結果或拋出檢查異常,Callable 接口可以,如果任務不需要返回結果或拋出異常推薦使用 Runnable,這樣代碼看起來會更加簡潔
5 獲取與設置優先級(不可靠)、守護線程
- Java 只是給操作系統一個優先級的 參考值,線程最終在操作系統的調用順序由操作系統的線程調度算法決定的
- 優先級獲取:thread_instance.getPriority()
- 優先級設置:setPriority(int LEVEL),默認5,最高10,最低1,優先級越高,先執行的 概率 更大
- 線程組也具有優先級,如果某個線程優先級大于線程所在線程組的最大優先級,那么該線程的優先級被線程組的最大優先級取代
- 守護線程默認的優先級比較低
- 如果某線程是守護線程,那如果所有的非守護線程都結束了,這個守護線程也會自動結束,就免去了還要繼續關閉子線程的麻煩
五 synchronized 關鍵字
1 簡介
- Java 多線程的鎖都是基于對象的,Java中的每一個對象都可以作為一個鎖
- 一句話概括:synchronized 關鍵字解決的是多個線程之間訪問資源的同步性,synchronized 關鍵字可以保證被它修飾的方法或者代碼塊在任意時刻只能有一個線程執行
- 在 Java 早期版本中,synchronized 屬于 重量級鎖,效率低下。因為操作系統實現線程之間的切換時需要從用戶態轉換到內核態(通過 trap 指令),這個狀態之間的轉換需要相對比較長的時間
2 使用方法
要求是多個線程共用一把鎖
synchronized(this) {// TODO }3 注意事項
- 不要使用 synchronized(String a),因為 JVM 中,字符串常量池具有緩存功能
- 構造方法不能使用 synchronized 關鍵字修飾。因為構造方法本身就屬于線程安全的,不存在同步的構造方法一說
六 volatile 關鍵字
1 作用
因為在 Java 內存模型中,在 JDK1.2 之前,Java 的內存模型實現總是從主存(即共享內存)讀取變量,是不需要進行特別的注意的。而 在當前的 Java 內存模型下,線程可以把變量保存本地內存(比如機器的寄存器,CPU cache)中,而不是直接在主存中進行讀寫。這就可能造成一個線程在主存中修改了一個變量的值,而另外一個線程還繼續使用它在寄存器中的變量值的拷貝,造成數據的不一致
使用 volatile 關鍵字,指定變量每次使用時都從主存讀取
2 雙重校驗鎖實現 單例模式(線程安全)
public class Singleton {private volatile static Singleton uniqueInstance; // 對象實例,需要用volatile修飾private Singleton() { // 構造方法,設置為private}public static Singleton getUniqueInstance() { // 創建實例//先判斷對象是否已經實例過,沒有實例化過才進入加鎖代碼if (uniqueInstance == null) {//類對象加鎖,保證只能創建一個實例synchronized (Singleton.class) {if (uniqueInstance == null) {uniqueInstance = new Singleton();// 執行過程:// 1.為 uniqueInstance 分配內存空間// 2.初始化 uniqueInstance// 3.將 uniqueInstance 指向分配的內存地址}}}return uniqueInstance;} }必須使用 volatile 關鍵字的原因:
由于 JVM 具有指令重排的特性,uniqueInstance = new Singleton() 執行順序有可能變成注釋中的 1->3->2。指令重排在單線程環境下不會出現問題,但是在多線程環境下會導致一個線程獲得還沒有初始化的實例(僅僅是剛分配了內存空間)
線程 T1 執行了 1 和 3,此時 T2 調用 getUniqueInstance() 后發現 uniqueInstance 不為空,因此返回 uniqueInstance,但此時 uniqueInstance 還未被初始化。 使用 volatile 禁止 JVM 的指令重排,保證在多線程環境下也能正常運行
3 與 synchronized 的關系
- synchronized 關鍵字和 volatile 關鍵字是互補而非對立的關系
- volatile 關鍵字是線程同步的 輕量級 實現,性能比 synchronized 關鍵字好
- volatile 關鍵字只能用于變量(僅僅保證對單個volatile變量的讀/寫具有原子性),而 synchronized 關鍵字可以修飾方法以及代碼塊
- volatile 關鍵字主要用于解決變量在多個線程之間的 可見性 ,而 synchronized 關鍵字解決的是多個線程之間訪問資源的同步性
七 ReentrantLock 可重入鎖
該部分的參考
1 可重入鎖
- 可重入鎖,指的是一個線程能夠對一個臨界資源重復加鎖
- AQS 有一個變量 state 用于記錄同步狀態:初始情況下,state = 0,表示 ReentrantLock 目前處于解鎖狀態。如果有線程調用 lock 方法進行加鎖,state 就由0變為1,如果該線程再次調用 lock 方法加鎖,就執行 state++。線程每調用一次 unlock 方法釋放鎖,會讓 state–。通過查詢 state 的數值,即可知道 ReentrantLock 被重入的次數了
- 現在有方法 m1 和 m2,兩個方法均使用了同一把鎖對方法進行同步控制,同時方法 m1 會調用 m2。線程 t 進入方法 m1 成功獲得了鎖,此時線程 t 要在沒有釋放鎖的情況下,調用 m2 方法。由于 m1 和 m2 使用的是同一把可重入鎖,所以線程 t 可以進入方法 m2,并再次獲得鎖,而不會被阻塞住;假如 lock 是不可重入鎖,那么上面的示例代碼必然會引起死鎖情況的發生
2 和 synchronized 異同
- synchronized 使用的是對象或類進行加鎖,而 ReentrantLock 內部是通過 AQS(AbstractQueuedSynchronizer)中的同步隊列進行加鎖
- 公平與非公平指的是線程獲取鎖的方式:
公平模式下,線程在同步隊列中通過 FIFO 的方式獲取鎖,每個線程最終都能獲取鎖,缺點是效率低
在非公平模式下,線程會通過“插隊”的方式去搶占鎖,搶不到的則進入同步隊列進行排隊,缺點是可能出現線程饑餓
八 ThreadLocal
參考鏈接
1 概念
- ThreadLocal 類主要解決的就是讓每個線程綁定自己的值,這個值不能被其它線程訪問到
- 每個 Thread 中都具備一個容器 ThreadLocalMap,而 ThreadLocalMap 可以存儲以 ThreadLocal 為 key (其實是 ThreadLocal 的弱引用),Object 對象為 value 的鍵值對
- ThrealLocal 類中可以通過 Thread.currentThread() 獲取到當前線程對象后,直接通過 getMap(Thread t) 可以訪問到該線程的 ThreadLocalMap 對象
- ThreadLocalMap 不使用拉鏈法解決哈希沖突,而是向后探測:如果先遇到了空位置則直接插入;如果先遇到了 key 過期的數據則進行垃圾回收并替換
2 get 和 set 方法的源碼
public void set(T value) {Thread t = Thread.currentThread(); // 獲取當前的線程ThreadLocalMap map = getMap(t); // 每一個線程都維護各自的一個容器(ThreadLocalMap)if (map != null)map.set(this, value); // 這里的key對應的是ThreadLocalelsecreateMap(t, value); // 默認map是null,第一次往其中添加數據時,執行初始化}public T get() {Thread t = Thread.currentThread(); // 獲取當前的線程ThreadLocalMap map = getMap(t); // 當前線程的ThreadLocalMapif (map != null) {ThreadLocalMap.Entry e = map.getEntry(this); // this指的是ThreadLocal對象if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value; // entry.value就可以獲取到工具箱了return result;}}return setInitialValue();}3 ThreadLocalMap - key 的弱引用和 GC
- ThreadLocalMap 中,key 是它對應的 ThreadLocal 的弱引用
- 強引用不存在的話,那么 key 就會被回收,也就是會出現 value 沒被回收,key 被回收的情況,導致 value 永遠存在,出現內存泄漏
九 并發容器
1 ConcurrentHashMap
- 數據結構:
| ConcurrentHashMap JDK1.7 | Segment 數組 + HashEntry 數組 + 鏈表/紅黑樹 | Segment(本質是 ReentrantLock),每次鎖若干 HashEntry |
| ConcurrentHashMap JDK1.8 | Node 數組 + 鏈表/紅黑樹 | synchronized,每次鎖一個 Node |
| Hashtable | 數組+鏈表 | synchronized,每次鎖全表 |
JDK1.8 的時候已經摒棄了 Segment 的概念,synchronized 只鎖定當前鏈表或紅黑二叉樹的首節點,并發控制使用 synchronized 和 CAS 來操作,雖然在 JDK1.8 中還能看到 Segment 的數據結構,但是已經簡化了屬性,只是為了兼容舊版本
Hashtable(同一把鎖) :使用 synchronized 來保證線程安全,效率非常低下。當一個線程訪問同步方法時,其他線程也訪問同步方法,可能會進入阻塞或輪詢狀態,如使用 put 添加元素,另一個線程不能使用 put 添加元素,也不能使用 get,競爭會越來越激烈效率越低
2 CopyOnWriteArrayList
- 線程安全的 List,在讀多寫少的場合性能非常好,遠遠好于 Vector
- CopyOnWriteArrayList 讀取完全不用加鎖,寫入也不會阻塞讀取操作
- 寫時復制的思想:想要對一塊內存進行修改時,不在原有內存塊中進行寫操作,而是將內存拷貝一份,在新的內存中進行寫操作,寫完之后將指向原來內存指針指向新的內存,原來的內存就可以被回收掉了
3 ConcurrentLinkedQueue
- 高效的并發隊列,非阻塞隊列(通過 CAS 操作實現)
- 使用鏈表實現,可以看做一個線程安全的 LinkedList
4 BlockingQueue
- 接口,阻塞隊列(通過加鎖實現),適合用于作為數據共享的通道
- 被廣泛使用在 “生產者-消費者” 問題中,其原因是 BlockingQueue 提供了可阻塞的插入和移除的方法。當隊列容器已滿,生產者線程會被阻塞,直到隊列未滿;當隊列容器為空時,消費者線程會被阻塞,直至隊列非空時為止
5 ConcurrentSkipListMap
- 使用跳表實現的 Map,使用跳表的數據結構進行快速查找
- 對平衡樹的插入和刪除往往很可能導致平衡樹進行一次全局的調整,而對跳表的插入和刪除只需要對整個數據結構的局部進行操作即可。這樣帶來的好處是:在高并發的情況下,需要一個全局鎖來保證整個平衡樹的線程安全。而對于跳表,只需要部分鎖
- 跳表內所有的元素都是排序的,對跳表進行遍歷會得到有序的結果,適用于數據需要有序的環境
十 線程池
參考鏈接
1 為什么使用線程池
- 創建/銷毀線程需要消耗系統資源,線程池可以復用已創建的線程
- (主要原因)控制并發的數量:并發數量過多,可能會導致資源消耗過多,從而造成服務器崩潰
- 統一管理線程
2 ThreadPoolExecutor 構造方法
- Java中的線程池頂層接口是 Executor 接口,ThreadPoolExecutor 是這個接口的實現類
| int corePoolSize | 線程池中核心線程數最大值 | 核心線程默認情況下會一直存在于線程池中,即使這個核心線程什么都不干(鐵飯碗),而非核心線程如果長時間的閑置,就會被銷毀(臨時工) | 是 |
| int maximumPoolSize | 線程池中線程總數最大值 | 核心線程數量 + 非核心線程數量 | 是 |
| long keepAliveTime | 非核心線程閑置超時時長 | 非核心線程如果處于閑置狀態超過該值,就會被銷毀。如果設置 allowCoreThreadTimeOut(true),則會也作用于核心線程 | 是 |
| TimeUnit unit | keepAliveTime 的單位 | 枚舉類型 | 是 |
| BlockingQueue workQueue | 阻塞隊列,維護著等待執行的 Runnable 任務對象 | 下面補充說明 | 是 |
| ThreadFactory threadFactory | 線程創建工廠 | 用于批量創建線程,統一在創建線程時設置一些參數,如是否守護線程、線程的優先級等。如果不指定,會新建一個默認的線程工廠 | 否 |
| RejectedExecutionHandler handler | 拒絕處理策略,線程數量大于最大線程數就會采用拒絕處理策略 | 下面補充說明 | 否 |
- 常用的阻塞隊列
- 有關拒絕處理策略
3 ThreadPoolExecutor 的狀態
| RUNNING | 線程池創建后處于 RUNNING 狀態 |
| SHUTDOWN | 調用 shutdown() 方法后處于 SHUTDOWN 狀態,線程池不能接受新的任務,正在執行的線程不中斷,完成阻塞隊列的任務(存疑) |
| STOP | 調用 shutdownNow() 方法后處于 STOP 狀態,線程池不能接受新的任務,中斷所有線程,阻塞隊列中沒有被執行的任務全部丟棄。此時,工作線程全部停止,阻塞隊列為空 |
| TIDYING | 當所有的任務已終止,線程池會變為 TIDYING 狀態,接著會執行 terminated() 函數 |
| TERMINATED | 線程池處在 TIDYING 狀態時,并且執行完 terminated() 方法之后 , 線程池被設置為 TERMINATED 狀態 |
4 任務處理流程
為什么在步驟2中,要二次檢查線程池的狀態?
- 在多線程的環境下,線程池的狀態是時刻發生變化的。有可能剛獲取線程池狀態后線程池狀態就改變了。判斷是否將 command 加入workqueue 是線程池之前的狀態。倘若沒有二次檢查,萬一線程池處于非 RUNNING 狀態(在多線程環境下很有可能發生),那么 command 永遠不會執行
- 類似于單例模式的雙重校驗
總結
以上是生活随笔為你收集整理的后端学习 - 并发编程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 后端学习 - Java容器
- 下一篇: 后端学习 - RabbitMQ