轉載https://blog.csdn.net/GavinZhera/article/details/86471828
知識點
線程安全,線程封閉,線程調度,同步容器,並發容器,AQS,J.U.C,等等
高並發解決思路與手段
擴容:水平擴容、垂直擴容
緩存:Redis、Memcache、GuavaCache等
隊列:Kafka、RabitMQ、RocketMQ等
應用拆分:服務化Dubbo與微服務Spring Cloud
限流:Guava RateLimiter使用、常用限流算法、自己實現分布式限流等
服務降級與服務熔斷:服務降級的多重選擇、Hystrix
數據庫切庫,分庫分表:切庫、分表、多數據源
高可用的一些手段:任務調度分布式elastic-job、主備curator的實現、監控報警機制
基礎知識與核心知識准備
並發高並發相關概念
cpu多級緩存:緩存一致,亂序執行優化
java內存模型:JMM規定,抽象結構,同步操作與規則
並發優勢與風險
並發模擬:Postman,Jmeter,Apache Bench,代碼
並發基本概念
同時擁有兩個或多個線程,如果程序在單核處理器上運行,多個線程將交替的換入或者換出內存,這些線程是同時“存在”的,每個線程都處於執行過程中的某個狀態,如果運行在多核處理器上,此時,程序中的每個線程都將分配到一個處理器核上,因此可以同時運行。
高並發基本概念
高並發(High Concurrency)是互聯網分布式系統架構設計中必須考慮的因素之一,它通常是指,通過設計保證系統能夠同時並行處理很多請求。
並發:多個線程操作相同的資源,保證線程安全,合理使用資源
高並發:服務能同時處理很多請求,提高程序性能(更多的考慮技術手段)
知識技能
總體架構:Spring Boot、Maven、JDK8、MySQL
基礎組件:Mybatis、Guava、Lombok、Redis、Kafka
高級組件:Joda-Time、Atomic包、J.U.C、AQS、ThreadLocal、RateLimiter、Hystrix、ThreadPool、Shardbatis、curator、elastic-job等
基礎知識
cpu多級緩存
主存和cpu通過主線連接,CPU緩存在主存和CPU之間,緩存的出現可以減少CPU讀取共享主存的次數
為什么需要CPU cache:CPU的頻率太快了,快到主存跟不上,這樣在處理器時鍾周期內,CPU常常需要等待主存,浪費資源。所以cache的出現,是為了緩解CPU和內存之間速度不匹配問題(結構:cpu -> cache -> memery).
CPU cache有什么意義:
1)時間局部性:如果某個數據被訪問,name在不久的將來它很可能被再次訪問。
2)空間局部性:如果某個數據被訪問,那么與它相鄰的數據很快也可能被訪問
CPU多級緩存-緩存一致性(MESI)
MESI分別代表cache數據的四種狀態,這四種狀態可以相互轉換
緩存四種操作:local read、local write、remote read、remote write
CPU多級緩存-亂序執行優化
在多核處理器上回出現問題
java內存模型(java memory model,JMM)
java內存模型-同步八種操作
lock(鎖定):作用於主內存的變量,把一個變量標識為一條線程獨占狀態
unlock(解鎖):作用於主內存變臉個,把一個處於鎖定狀態的變量釋放出來,釋放后的變量才可以被其他線程鎖定
read(讀取):作用於主內存變量,把一個變量值從主內存傳輸到線程的工作內存中,以便隨后的load動作使用
load(載入):作用於工作內存的變量,它把read操作從主內存中得到的變量值放入工作內存的變量副本中
use(使用):作用於工作內存的變量,把工作內存中的一個變量值傳遞給執行引擎
assign(賦值):作用於工作內存的變量,它把一個從執行引擎接收到的值賦值給工作內存的變量
store(存儲):作用於工作內存的變量,把工作內存中的一個變量的值傳送到主內存中,以遍隨后的write的操作
write(寫入):作用於主內存的變量,它把store操作從工作內存中的一個變量的值傳送到主內存的變量中
java內存模型-同步規則
- 如果要把一個變量從主內存中復制到工作內存,就需要按順序的執行read和load操作,如果把變量從工作內存中同步回主內存,就需要按順序的執行store和write操作。但java內存模型只要求上述操作必須按順序執行,而沒有保證必須是連續執行
- 不允許read和load、store和write操作之一單獨出現
- 不允許一個線程丟棄它的最近assign的操作,即變量在工作內存中改變了之后必須同步到主內存中
- 不允許一個線程無原因的(沒發生過任何assign操作)把數據從工作內存同步回主內存中
- 一個新的變量只能在主內存中誕生,不允許在工作內存中直接使用一個未被初始化(load或assign)的變量。即就是對一個變量實施use和store操作之前,必須先執行過了assign和load操作
- 一個變量在同一時刻只允許一條線程對其進行lock操作,但lock操作可以被同一條線程重復執行多次,多次執行lock后,只有執行相同次數的unlock操作,變量才會被解鎖。lock和unlock必須成對出現
- 如果對一個變量執行lock操作,將會清空工作內存中此變量的值,在執行引擎使用這個變量之前需要重新執行load或assign操作初始化變量的值
- 如果一個變量實現沒有被lock操作鎖定,怎不允許對它執行unlock操作,也不允許去unlock一個被其他線程鎖定的變量
- 對一個變量執行unlock操作之前,必須先把此變量同步到主內存中(執行store和write操作)
並發的優勢與風險
優勢
速度:同時處理多個請求,響應更快;復雜的操作可以分成多個進程同時進行
設計:程序設計在某些情況下更簡單,也可以更多的選擇
資源利用:CPU能夠在等待IO的時候做一些其他的事情
風險
安全性:多個線程共享數據時可能會產生於期望不相符的結果
活躍性:某個操作無法繼續進行下去時,就會發生活躍性問題。比如死鎖、飢餓等問題
性能:線程過多時會使得CPU頻繁切換,調度時間增多;同步機制;消耗過多內存
線程安全性
定義:當多個線程訪問某個類時,不管運行時環境采用何種調度方式或者這些線程將如何交替執行,並且在主調代碼中不需要任何額外的同步或協同,這個類都能表現出正確的行為,那么就稱這個類時線程安全的。
線程安全體現在以下三個方面
原子性:提供了互斥訪問,同一時刻只能有一個線程來對他進行操作
可見性:一個線程對主內存的修改可以及時的被其他線程觀察到
有序性:一個線程觀察其他線程中的指令執行順序,由於指令重排序的存在,該觀察結果一般雜亂無序
原子性——Atomic包
AtomicXxxx:CAS、Unsafe.compareAndSwapInt
AtomicXxxx類中方法incrementAndGet(),incrementAndGet方法中調用unsafe.getAndAddInt(),getAndAddInt方法中主題是do-while語句,while語句中調用compareAndSwapInt(var1, var2, var5, var5 + var4)
compareAndSwapInt方法就是CAS核心:
在死循環內,不斷嘗試修改目標值,直到修改成功,如果競爭不激烈,修改成功率很高,否則失敗概率很高,性能會受到影響
jdk8中新增LongAdder,它和AtomicLong比較
優點:性能好,在處理高並發情況下統計優先使用LongAdder
AtomicReference、AtomicReferenceFieldUpdater原子性更新字段(字段要求volatile修飾,並且是非static)
AtomicStampReference:CAS的ABA問題
ABA問題:變量已經被修改了,但是最終的值和原來的一樣,那么如何區分是否被修改過呢,用版本號解決
AtomicBoolean可以讓某些代碼只執行一次
原子性——鎖
synchronized:依賴jvm,作用對象的作用范圍內
修飾代碼塊:同步代碼塊,大括號括起來的代碼,作用於調用的對象
修飾方法:同步方法,整個方法,作用於調用的對象
修飾靜態方法:整個靜態方法,作用於所有對象
修飾類:括號括起來的部分,作用於所有對象
Lock:依賴特殊CPU指令,代碼實現,ReentrantLock
原子性——對比
synchronized:不可中斷鎖,適合競爭不激烈,可讀性好
Lock:可中斷鎖,多樣化同步,競爭激烈時能維持常態
Atomic:競爭激烈時能維持常態,比Lock性能好,只能同步一個值
可見性
導致共享變量在線程間不可見的原因:
1 線程交叉執行
2 重排序結合線程交叉執行
3 共享變量更新后的值沒有在工作內存與主內存間及時更新
可見性——synchronized
JMM關於synchronized的兩條規定:
線程解鎖前,必須把共享變量的最新值刷新到主內存
線程加鎖時,將清空工作內存中共享變量的值,從而使用共享變量時需要從主內存中重新讀取最新的值(注意,加鎖和解鎖是同一把鎖)
可見性——volatile
通過加入內存屏障和禁止重排序優化來實現
1 對volatile變量寫操作時,會在寫操作后加入一條store屏障指令,將本地內存中的共享變量值刷新到主內存
2 隨volatile變量度操作時,會在讀操作前加入一條load屏障指令,從主內存中讀取共享變量
使用volatile修飾變量,無法保證線程安全
volatile適合修飾狀態標識量
有序性
java內存模型中,允許編譯器和處理器對指令進行重排序,但是重排序過程不會影響到單線程程序的執行,卻會影響到多線程並發執行的正確性
有序性——happens-before原則
1 程序次序規則:一個線程內,按照代碼順序,書寫在前面的操作先行發生於書寫在后面的操作
注:在單線程中,看起來是這樣的,虛擬機可能會對代碼進行指令重排序,雖然重排序了,但是運行結果在單線程中和指令書寫順序是一致的,事實上,這條規則是用來保證程序單在單線程中執行結果的正確性,無法保證程序在多線程中的正確性
2 鎖定規則:一個unlock操作先行發生於后面對同一個鎖的lock操作
3 volatile變量規則:對一個變量的寫操作先行發生於后面對這個變量的讀操作
4 傳遞規則:如果操作A先行發生於操作B,而操作B又先行發生於操作C,則可以得出操作A先行發生於操作C
前四條規則比較重要
5 線程啟動規則:Thread對象的start()方法先行發生於次線程的每一個動作
6 線程中斷規則:對線程interrupt()方法的調用先行發生於被中斷線程的代碼監測到中斷事件的發生
7 線程終結規則:線程中所有的操作都先行發生於線程的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到線程已經終止執行
8 對象終結規則:一個對象的初始化完成先行發生於他的finalize()方法的開始
線程安全性——總結
原子性:Atomic包、CAS算法、synchronized、Lock
可見性:synchronized、volatile
有序性:happens-before規則
一個線程觀察其他線程指令執行順序,由於重排序的存在,觀察結果一般是無序的,如果兩個操作執行順序無法從happens-before原則推導出來,name他們就不能保證有序性,虛擬機可以隨意的對他們重排序
發布對象
發布對象:使一個對象能夠被當前范圍之外的代碼所使用
對象逸出:一種錯誤的發布。當一個對象還沒有構造完成時,就使它被其他線程所見
安全發布對象四種方法
1 在靜態初始化函數中初始化一個對象引用
2 將對象的引用保存到volatile類型域或者AtomicReference對象中
3 將對象的引用保存到某個正確構造對象的final類型域中
4 將對象的引用保存到一個由鎖保護的域中
私有構造函數,單例對象,靜態工廠方法獲取對象
以單例模式為例
懶漢模式:單例實例在第一次使用時進行創建(線程不安全)
懶漢模式也可以實現線程安全,給getInstance方法添加synchronized關鍵字(不推薦,因為性能不好)
雙重同步鎖單例模式:雙重監測機制,在方法內部加synchronized關鍵字(不是線程安全的)
原因是,創建對象是,分為以下三個步驟:
1) memory = allocate() 分配對象的內存空間
2)ctorInstance() 初始化對象
3)instance = memory() 設置instance指向剛分配的內存
由於JVM和cpu優化,可能會發生指令重排:
1) memory = allocate() 分配對象的內存空間
3) instance = memory() 設置instance指向剛分配的內存
2) ctorInstance() 初始化對象
當以上面這種指令執行時,線程A執行到3 instance = memory() 設置instance指向剛分配的內存 這一步時,線程B執行if(instance == null)這段代碼,此時instance != null,線程B直接return instance,導致對象沒有初始化完畢就返回
解決辦法就是限制對象創建時進行指令重排,volatile+雙重監測機制->禁止指令重排引起非線程安全
餓漢模式:單例實例在類裝載時進行創建(線程安全)
枚舉模式:線程安全
不可變對象
不可變對象需要滿足的條件:
對象創建以后其狀態就不能修改
對象所有域都是final類型
對象是正確創建的(在對象創建期間,this引用沒有逸出)
參考String類型
final關鍵字定義不可變對象
修飾類、方法、變量
修飾類:不能被繼承
修飾方法:1.鎖定方法不被繼承類修改 2.效率
修飾變量:基本數據類型,數值不可變;引用類型變量,不能再指向另外一個對象,因此容易引起線程安全問題
其他實現不可變對象
Collections.unmodifiableXXX:Collection、List、Set、Map(線程安全)
Guava:ImmutableXXX:Collection、List、Set、Map
線程封閉性
線程封閉概念:把對象封裝到一個線程里,只有這個線程可以看到該對象,那么就算該對象不是線程安全的,也不會出現任何線程安全方面的問題。實現線程封閉的方法:
1 Ad-hoc線程封閉:程序控制實現,最糟糕,忽略
2 堆棧封閉:局部變量,無並發問題
3 threadLocal是線程安全的,做到了線程封閉
ThreadLocal內部維護了一個map,map的key是每個線程的名稱,map的值是要封閉的對象,每一個線程中的對象都對應者一個map中的值
線程封閉的應用場景:
數據庫連接jdbc的Connection對象
線程不安全類與寫法
字符串
StringBuilder:線程不安全
StringBuffer:線程安全
時間轉換
SimpleDateFormat:線程不安全
JodaTime:線程安全
集合
ArrayList:線程不安全
HashSet:線程不安全
HashMap:線程不安全
編程注意:
if(condition(a)){handle(a)}; 不是線程安全的,因為這條判斷語句不是原子性的,如果有線程共享這條代碼,則會出現並發問題,解決方案是想辦法這這段代碼是原子性的(加鎖)
線程安全——同步容器(在多線程環境下不推薦使用)
ArrayList -> Vector, Stack
Vector中的方法使用synchronized修飾過
Stack繼承Vector
HashMap -> HashTable(key、value不能為null)
HashTable使用synchronized修飾方法
Collections.synchronizedXXX(List、Set、Map)
同步容器不完全是線程安全的
編程注意:如果使用foreach或者iterator遍歷集合時,盡量不要對集合進行修改操作
線程安全——並發容器J.U.C(java.util.concurrent)(在多線程環境下推薦使用)
ArrayList -> CopyOnWriteArrayList:相比ArrayList,CopyOnWriteArrayList是線程安全的,寫操作時復制,即當有新元素添加到CopyOnWriteArrayList時,先從原有的數組里拷貝一份出來,然后在新的數組上寫操作,寫完之后再將原來的數組指向新的數組,CopyOnWriteArrayList整個操作都是在鎖(ReentrantLock鎖)的保護下進行的,這么做主要是避免在多線程並發做add操作時復制出多個副本出來,把數據搞亂了。第一個缺點是做寫操作時,需要拷貝數組,就會消耗內存,如果元素內容比較多會導致youngGC或者是fullGc;第二個缺點是不能用於實時讀的場景,比如拷貝數組、新增元素都需要時間,所以調用一個set操作后,讀取到的數據可能還是舊的,雖然CopyOnWriteArrayList能夠做到最終的一致性,但是沒法滿足實時性要求,因此CopyOnWriteArrayList更適合讀多寫少的場景
CopyOnWriteArrayList設計思想:1讀寫分離 2最終一致性 3使用時另外開辟空間解決並發沖突
HashSet -> CopyOnWriteArraySet
TreeSet -> ConcurrentSkipListSet
CopyOnWriteArraySet:底層實現是CopyOnWriteArrayList
ConcurrentSkipListSet:和TreeSet 一樣支持自然排序,基於map集合,但是批量操作不是線程安全的
HashMap -> ConcurrentHashMap :不允許空值,針對讀操作做了大量的優化,具有特別高的並發性
TreeMap -> ConcurrentSkipListMap :內部使用SkipList跳表結構實現的,key是有序的,支持更高的並發
安全共享對象策略——總結
1 線程限制:一個唄線程限制的對象,由線程獨占,並且只能被占有它的線程修改
2 共享只讀:一個共享只讀的對象,在沒有額外的同步情況下,可以被多個線程並發訪問,但是任何線程都不能修改它
3 線程安全對象:一個線程安全的對象或容器,在內部通過同步機制來保證線程安全,所以其他線程無序額外的同步就可以通過公共接口隨意訪問它
4 被守護對象:被守護對象只能通過獲取特定的鎖來訪問
不可變對象、線程封閉、同步容器、並發容器
J.U.C之AQS
AQS:AbstractQueuedSynchronizer
1 使用Node實現FIFO隊列,可以用於構建鎖或者其他同步裝置的基礎框架
2 利用了int類型表示狀態
3 使用方法是繼承
4 子類通過繼承並通過實現它的方法管理器狀態{acquire和release}的方法操縱狀態
5 可以同時實現排它鎖和共享鎖模式(獨占、共享)
AQS同步組件
1 CountDownLatch:閉鎖,通過計數來保證線程是否需要一直阻塞
2 Semaphore:控制同一時間並發線程的數目
3 CyclicBarrier:和CountDownLatch相似,都能阻阻塞線程
4 ReentrantLock
5 Condition
6 FutureTask
CountDownLatch
CountDownLatch是一個同步輔助類,應用場景:並行運算,所有線程執行完畢才可執行
代碼示例1:
代碼示例2:
await方法可以設定指定等待時間,超過這個時間久不再等待
Semaphore
Semaphore可以很容易控制某個資源可悲同時訪問的線程個數,和CountDownLatch使用有些類似,提供acquire和release兩個方法,acquire是獲取一個許可,如果沒有就等待,release是在操作完成后釋放許可出來。Semaphore維護了當前訪問的線程的個數,提供同步機制來控制同時訪問的個數,Semaphore可以實現有限大小的鏈表,重入鎖(如ReentrantLock)也可以實現這個功能,但是實現上比較復雜。
Semaphore使用場景:適用於僅能提供有限資源,如數據庫連接數
代碼示例1:
代碼示例2:
CyclicBarrier
與CountDownLatch相似,都是通過計數器實現,當某個線程調用await方法,該線程就進入等待狀態,且計數器進行加1操作,當計數器的值達到設置的初始值,進入await等待的線程會被喚醒,繼續執行他們后續的操作。由於CyclicBarrier在釋放等待線程后可以重用,所以又稱循環屏障。使用場景和CountDownLatch相似,可用於並發運算。
CyclicBarrier和CountDownLatch區別:
1 CountDownLatch計數器只能使用一次,CyclicBarrier的計數器可以使用reset方法重置循環使用
2 CountDownLatch主要是視線1個或n個線程需要等待其他線程完成某項操作才能繼續往下執行,CyclicBarrier主要是實現多個線程之間相互等待知道所有線程都滿足了條件之后才能繼續執行后續的操作,CyclicBarrier能處理更復雜的場景
代碼示例:
ReentrantLock
reentrantLock(可重入鎖)和synchronized區別
1 可重入性:同一線程可以重入獲得相同的鎖,計數器加1,釋放鎖計數器減1
synchronized也是可重入鎖
2 鎖的實現:synchronized依賴jvm實現(操作系統級別的實現),reentrantLock是jdk實現的(用戶自己編程實現)
3 性能區別:synchronized在優化前性能比reentrantLock差,優化后性能有了恨到提升,相同條件下優先使用synchronized
4 功能區別:1)便利性方面,synchronized使用簡單,reentrantLock需要手工加鎖和釋放鎖2)鎖的細粒度和靈活度方面,reentrantLock優於synchronized
5 reentrantlock獨有的功能:1)可指定是公平鎖還是非公平鎖,synchronized只能是非公平鎖 2)提供了一個Condition類,可以分組喚醒需要喚醒的線程 3)能夠提供中斷等待鎖的線程機制,lock.lockInterruptibly()
代碼示例:
ReentrantReadWriteLock
悲觀寫鎖,即當所有讀鎖釋放之后,才能加寫鎖,對於讀多寫少的程序,會引起堵塞或者死鎖
代碼示例:
Condition
多線程建協調通信的工具類
代碼示例:
FutureTask
Callable與Runnable接口對比
Feature接口,可以得到任務的返回值
FeatureTask父類是RunnableFeature,RunnableFeature繼承了Runnable和Feature兩個接口
代碼示例1:
示例代碼2:
Fork/Join框架
將大人物切分成多個小任務並行執行,最后將結果匯總,思想和mapreduce類似。采用工作竊取算法,充分利用線程並行計算
BlockingQueue——阻塞隊列
當隊列滿進行入隊操作,線程阻塞,當隊列空時進行出隊操作,將會阻塞
線程安全,應用場景:生產者、消費者
線程池
new Thread弊端:
1 每次new Thread新建對象,性能差
2 線程缺乏統一的管理,肯無限制的新建線程,相互競爭,有可能占用過多系統資源導致死機或者OOM
3 缺少更多功能,如更多執行、定期執行、線程中斷
線程池的好處:
1 重用存在的線程,減少對象創建、消亡的開銷,性能佳
2 可有效控制最大並發的線程數,提高系統資源利用率,同時可以避免過多資源競爭,避免阻塞
3 提供定時執行、定期執行、單線程、並發數控制等功能
線程池——ThreadPoolExecutor
ThreadPoolExecutor參數:
1 corePoolSize:核心線程數
2 maximumPoolSize:最大線程數
3 workQueue:阻塞隊列,存儲等待執行的任務,很重要,會對線程池運行過程產生重大影響
如果當前系統運行的線程數量小於corePoolSize,直接新建線程執行處理任務,即使線程池中的其他線程是空閑的。如果當前系統運行的線程數量大於或等於corePoolSize,且小於maximumPoolSize,只有當workQueue滿的時候才創建新的線程去處理任務,如果設置corePoolSize和maximumPoolSize相同的話,那么創建的線程池大小是固定的,這時如果有新任務提交,當workQueue沒滿時,把請求放進workQueue中,等待有空閑的線程從workQueue中取出任務去處理。如果運行的線程數量大於maximumPoolSize,這時如果workQueue滿,根據拒絕策略去處理。
4 keepAliveTime:線程沒有任務執行時最多保持多久的時間終止
5 unit:keepAliveTime的時間單位
6 threadFactory:線程工廠,用來創建線程
7 rejectHandler:當拒絕處理任務時的策略
線程池狀態:
線程池方法:
死鎖
線程(進程)互相等待對方釋放資源產生死鎖
HashMap與ConcurrentHashMap
HashMap在多線程環境中做rehash時容易產生死循環
Guava Cache是谷歌開源的java工具庫,借鑒ConcurrentHashMap的設計思想
消息隊列
異步解耦
Kafka:持久化、高吞吐、分布式
應用拆分
拆分原則:
服務降級和熔斷
另外安利一個學習教程:(完整)Java並發編程與高並發解決方案
分享一個it資源平台:點擊進入