網際網路架構多執行緒併發程式設計高階教程(上)
阿新 • • 發佈:2018-11-09
#基礎篇幅:
執行緒基礎知識、併發安全性、JDK鎖相關知識、執行緒間的通訊機制、
JDK提供的原子類、併發容器、執行緒池相關知識點
#高階篇幅:
ReentrantLock原始碼分析、對比兩者原始碼,更加深入理解讀寫鎖,JAVA記憶體模型、先行發生原則、指令重排序
#環境說明:
idea、java8、maven
#第一章 併發簡介
##01 課程簡介 為什麼要學習併發程式設計? 方便實際開發 面試 課程特點 適合群體 ##02什麼是併發程式設計 併發歷史: 早期計算機--從頭到尾執行一個程式,資源浪費 作業系統出現--計算機能執行多個程式,不同的程式在不同的單獨的程序中執行 一個程序,有多個執行緒 提高資源的利用率,公平 序列與並行的區別: 序列:洗茶具、打水、燒水、等水開、沖茶 並行:打水、燒水同時洗茶具、水開、沖茶 好處:可以縮短整個流程的時間 併發程式設計目的: 摩爾定律:當價格不變時,積體電路上可容納的元器件的數目,約每隔18-24個月便會增加一倍,效能也將提升一倍。這一定律揭示了資訊科技進步的速度。 讓程式充分利用計算機資源 加快程式響應速度(耗時任務、web伺服器) 簡化非同步事件的處理 什麼時候適合使用併發程式設計: 任務會阻塞執行緒,導致之後的程式碼不能執行:比如一邊從檔案中讀取,一邊進行大量計算的情況 任務執行時間過長,可以劃分為分工明確的子任務:比如分段下載 任務間斷性執行:日誌列印 任務本身需要協作執行:比如生產者消費者問題 ##03併發程式設計的挑戰之頻繁的上下文切換 cpu為執行緒分配時間片,時間片非常短(毫秒級別),cpu不停的切換執行緒執行,在切換前會儲存上一個任務的狀態,以便下次切換回這個任務時,可以再載入這 個任務的狀態,讓我們感覺是多個程式同時執行的 上下文的頻繁切換,會帶來一定的效能開銷 如何減少上下文切換的開銷? 無鎖併發程式設計 無鎖併發程式設計。多執行緒競爭鎖時,會引起上下文切換,所以多執行緒處理資料時,可以用一 些辦法來避免使用鎖,如將資料的ID按照Hash演算法取模分段,不同的執行緒處理不同段的資料 CAS Java的Atomic包使用CAS演算法來更新資料,而不需要加鎖。 使用最少執行緒。 避免建立不需要的執行緒,比如任務很少,但是建立了很多執行緒來處理,這 樣會造成大量執行緒都處於等待狀態。 協程 在單執行緒裡實現多工的排程,並在單執行緒裡維持多個任務間的切換。--GO ##04 併發程式設計的挑戰之死鎖 package com.xdclass.synopsis; /** * 死鎖Demo */ public class DeadLockDemo { private static final Object HAIR_A = new Object(); private static final Object HAIR_B = new Object(); public static void main(String[] args) { new Thread(()->{ synchronized (HAIR_A) { try { Thread.sleep(50L); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (HAIR_B) { System.out.println("A成功的抓住B的頭髮"); } } }).start(); new Thread(()->{ synchronized (HAIR_B) { synchronized (HAIR_A) { System.out.println("B成功抓到A的頭髮"); } } }).start(); } } ##05 併發程式設計的挑戰之執行緒安全 package com.xdclass.synopsis; import java.util.concurrent.CountDownLatch; /** * 執行緒不安全操作程式碼例項 */ public class UnSafeThread { private static int num = 0; private static CountDownLatch countDownLatch = new CountDownLatch(10); /** * 每次呼叫對num進行++操作 */ public static void inCreate() { num++; } public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(()->{ for (int j = 0; j < 100; j++) { inCreate(); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } //每個執行緒執行完成之後,呼叫countdownLatch countDownLatch.countDown(); }).start(); } while (true) { if (countDownLatch.getCount() == 0) { System.out.println(num); break; } } } } ##06 併發程式設計的挑戰之資源限制 硬體資源 伺服器: 1m 本機:2m 頻寬的上傳/下載速度、硬碟讀寫速度和CPU的處理速度。 軟體資源 資料庫連線 500個連線 1000個執行緒查詢 並不會因此而加快 socket
#第二章----執行緒基礎
##01 程序與執行緒的區別 程序:是系統進行分配和管理資源的基本單位 執行緒:程序的一個執行單元,是程序內排程的實體、是CPU排程和分派的基本單位,是比程序更小的獨立執行的基本單位。執行緒也被稱為輕量級程序,執行緒是程式執行的最小單位。 一個程式至少一個程序,一個程序至少一個執行緒。 程序有自己的獨立地址空間,每啟動一個程序,系統就會為它分配地址空間,建立資料表來維護程式碼段、堆疊段和資料段,這種操作非常昂貴。 而執行緒是共享程序中的資料的,使用相同的地址空間,因此CPU切換一個執行緒的花費遠比程序要小很多,同時建立一個執行緒的開銷也比程序要小很多。 執行緒之間的通訊更方便,同一程序下的執行緒共享全域性變數、靜態變數等資料,而程序之間的通訊需要以通訊的方式進行。 如何處理好同步與互斥是編寫多執行緒程式的難點。 多程序程式更健壯,程序有獨立的地址空間,一個程序崩潰後,在保護模式下不會對其它程序產生影響, 而執行緒只是一個程序中的不同執行路徑。執行緒有自己的堆疊和區域性變數,但執行緒之間沒有單獨的地址空間,所以可能一個執行緒 出現問題,進而導致整個程式出現問題 ##02 執行緒的狀態及其相互轉換 初始(NEW):新建立了一個執行緒物件,但還沒有呼叫start()方法。 執行(RUNNABLE):處於可執行狀態的執行緒正在JVM中執行,但它可能正在等待來自作業系統的其他資源,例如處理器。 阻塞(BLOCKED):執行緒阻塞於synchronized鎖,等待獲取synchronized鎖的狀態。 等待(WAITING):Object.wait()、join()、 LockSupport.park(),進入該狀態的執行緒需要等待其他執行緒做出一些特定動作(通知或中斷)。 超時等待(TIME_WAITING):Object.wait(long)、Thread.join()、LockSupport.parkNanos()、LockSupport.parkUntil,該狀態不同於WAITING, 它可以在指定的時間內自行返回。 終止(TERMINATED):表示該執行緒已經執行完畢。 ##03 建立執行緒的方式(上) 1.繼承Thread,並重寫父類的run方法 2.實現Runable介面,並實現run方法 實際開發中,選第2種:java只允許單繼承 增加程式的健壯性,程式碼可以共享,程式碼跟資料獨立 ##04 建立執行緒的方式(下) 1.使用匿名內部類 2.Lambda表示式 3.執行緒池 ##05 執行緒的掛起跟恢復 什麼是掛起執行緒? 執行緒的掛起操作實質上就是使執行緒進入“非可執行”狀態下,在這個狀態下CPU不會分給執行緒時間片,進入這個狀態可以用來暫停一個執行緒的執行。 線上程掛起後,可以通過重新喚醒執行緒來使之恢復執行 為什麼要掛起執行緒? cpu分配的時間片非常短、同時也非常珍貴。避免資源的浪費。 如何掛起執行緒? 被廢棄的方法 thread.suspend() 該方法不會釋放執行緒所佔用的資源。如果使用該方法將某個執行緒掛起,則可能會使其他等待資源的執行緒死鎖 thread.resume() 方法本身並無問題,但是不能獨立於suspend()方法存在 可以使用的方法 wait() 暫停執行、放棄已經獲得的鎖、進入等待狀態 notify() 隨機喚醒一個在等待鎖的執行緒 notifyAll() 喚醒所有在等待鎖的執行緒,自行搶佔cpu資源 什麼時候適合使用掛起執行緒? 我等的船還不來(等待某些未就緒的資源),我等的人還不明白。直到notify方法被呼叫 ##06 執行緒的中斷操作 stop() 廢棄方法,開發中不要使用。因為一呼叫,執行緒就立刻停止,此時有可能引發相應的執行緒安全性問題 Thread.interrupt方法 自行定義一個標誌,用來判斷是否繼續執行 ##07 執行緒的優先順序 執行緒的優先順序告訴程式該執行緒的重要程度有多大。如果有大量執行緒都被堵塞,都在等候執行,程式會盡可能地先執行優先順序的那個執行緒。 但是,這並不表示優先順序較低的執行緒不會執行。若執行緒的優先順序較低,只不過表示它被准許執行的機會小一些而已。 執行緒的優先順序設定可以為1-10的任一數值,Thread類中定義了三個執行緒優先順序,分別是: MIN_PRIORITY(1)、NORM_PRIORITY(5)、MAX_PRIORITY(10),一般情況下推薦使用這幾個常量,不要自行設定數值。 不同平臺,對執行緒的優先順序的支援不同。 程式設計的時候,不要過度依賴執行緒優先順序,如果你的程式執行是否正確取決於你設定的優先順序是否按所設定的優先順序執行,那這樣的程式不正確 任務: 快速處理:設定高的優先順序 慢慢處理:設定低的優先順序 ##08 守護執行緒 執行緒分類 使用者執行緒、守護執行緒 守護執行緒:任何一個守護執行緒都是整個程式中所有使用者執行緒的守護者,只要有活著的使用者執行緒,守護執行緒就活著。當JVM例項中最後一個非守護執行緒結束時,也隨JVM一起退出 守護執行緒的用處:jvm垃圾清理執行緒 建議: 儘量少使用守護執行緒,因其不可控 不要在守護執行緒裡去進行讀寫操作、執行計算邏輯
#第三章----執行緒安全性問題
##01 什麼是執行緒安全性? 當多個執行緒訪問某個類,不管執行時環境採用何種排程方式或者這些執行緒如何交替執行,並且在主調程式碼中不需要任何額外的同步或協同,這個類都能表現出正確的行為,那麼就稱這個類為執行緒安全的。----《併發程式設計實戰》 什麼是執行緒不安全? 多執行緒併發訪問時,得不到正確的結果。 ##02 從位元組碼角度剖析執行緒不安全操作 javac -encoding UTF-8 UnsafeThread.java 編譯成.class javap -c UnsafeThread.class 進行反編譯,得到相應的位元組碼指令 0: getstatic #2 獲取指定類的靜態域,並將其押入棧頂 3: iconst_1 將int型1押入棧頂 4: iadd 將棧頂兩個int型相加,將結果押入棧頂 5: putstatic #2 為指定類靜態域賦值 8: return 例子中,產生執行緒不安全問題的原因: num++ 不是原子性操作,被拆分成好幾個步驟,在多執行緒併發執行的情況下,因為cpu排程,多執行緒快遞切換,有可能兩個同一時刻都讀取了同一個num值,之後對它進行+1操作,導致執行緒安全性。 ##03 原子性操作 什麼是原子性操作 一個操作或者多個操作 要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行。 A想要從自己的帳戶中轉1000塊錢到B的帳戶裡。那個從A開始轉帳,到轉帳結束的這一個過程,稱之為一個事務。在這個事務裡,要做如下操作: 1. 從A的帳戶中減去1000塊錢。如果A的帳戶原來有3000塊錢,現在就變成2000塊錢了。 2. 在B的帳戶里加1000塊錢。如果B的帳戶如果原來有2000塊錢,現在則變成3000塊錢了。 如果在A的帳戶已經減去了1000塊錢的時候,忽然發生了意外,比如停電什麼的,導致轉帳事務意外終止了,而此時B的帳戶裡還沒有增加1000塊錢。 那麼,我們稱這個操作失敗了,要進行回滾。回滾就是回到事務開始之前的狀態,也就是回到A的帳戶還沒減1000塊的狀態,B的帳戶的原來的狀態。 此時A的帳戶仍然有3000塊,B的帳戶仍然有2000塊。 通俗點講:操作要成功一起成功、要失敗大家一起失敗 如何把非原子性操作變成原子性 volatile關鍵字僅僅保證可見性,並不保證原子性 synchronize關機字,使得操作具有原子性 ##04 深入理解synchronized 內建鎖 每個java物件都可以用做一個實現同步的鎖,這些鎖稱為內建鎖。執行緒進入同步程式碼塊或方法的時候會自動獲得該鎖,在退出同步程式碼塊或方法時會釋放該鎖。獲得內建鎖的唯一途徑就是進入這個鎖的保護的同步程式碼塊或方法。 互斥鎖 內建鎖是一個互斥鎖,這就是意味著最多隻有一個執行緒能夠獲得該鎖,當執行緒A嘗試去獲得執行緒B持有的內建鎖時,執行緒A必須等待或者阻塞,直到執行緒B釋放這個鎖,如果B執行緒不釋放這個鎖,那麼A執行緒將永遠等待下去。 修飾普通方法:鎖住物件的例項 修飾靜態方法:鎖住整個類 修飾程式碼塊: 鎖住一個物件 synchronized (lock) 即synchronized後面括號裡的內容 ##05 volatile關鍵字及其使用場景 能且僅能修飾變數 保證該變數的可見性,volatile關鍵字僅僅保證可見性,並不保證原子性 禁止指令重排序 A、B兩個執行緒同時讀取volatile關鍵字修飾的物件 A讀取之後,修改了變數的值 修改後的值,對B執行緒來說,是可見 使用場景 1:作為執行緒開關 2:單例,修飾物件例項,禁止指令重排序 ##06 單例與執行緒安全 餓漢式--本身執行緒安全 在類載入的時候,就已經進行例項化,無論之後用不用到。如果該類比較佔記憶體,之後又沒用到,就白白浪費了資源。 懶漢式 -- 最簡單的寫法是非執行緒安全的 在需要的時候再例項化 ##07 如何避免執行緒安全性問題 執行緒安全性問題成因 1:多執行緒環境 2:多個執行緒操作同一共享資源 3:對該共享資源進行了非原子性操作 如何避免 打破成因中三點任意一點 1:多執行緒環境--將多執行緒改單執行緒(必要的程式碼,加鎖訪問) 2:多個執行緒操作同一共享資源--不共享資源(ThreadLocal、不共享、操作無狀態化、不可變) 3:對該共享資源進行了非原子性操作-- 將非原子性操作改成原子性操作(加鎖、使用JDK自帶的原子性操作的類、JUC提供的相應的併發工具類)