概述
并行:同一個時間點,多個線程同時執行
并發:同一個時間段,多個線程交替執行,微觀上是一個一個的執行,宏觀上感覺是同時執行
核心問題:
多線程訪問共享數據存在資源競用問題
不可見性
java內存模型(jmm)
變量數據都存在于主內存里,每個線程還有自己的工作內存(本地內存),規定線程不能直接對主內存中的數據進行操作,只能把主內存的數據加載到自己的工作內存中操作,操作完成后,在寫會主內存
這樣的設計就引發了不可見性,應該線程在自己的工作內存中操作了數據后,另一個線程也在操作,但是不知道數據已經被另一個線程修改了
亂序性
為了優化指令執行,在執行一些等待時間比較長的執行時,可以把其他的一些執行指令提前執行,提高速度
但是在多線程場景下,就會出現問題
非原子性
線程切換執行帶來非原子性
cpu保證原子性執行cpu指令級別的,但是對于高級語言的一條代碼,有時是要拆分為多條指令的,線程在執行到某條執行時操作系統會切換到其他線程去執行,這樣這條高級語言指令執行就是非原子性的
總結:工作內存的緩存導致了不可見性,指令的優化導致了無序性,線程的切換執行導致了非原子性
volatile關鍵字
所修飾的變量在一個工作內存中操作后,底層會將工作內存中的數據同步到其他工作內存中,使其立即可見
- 確保多線程操作變量時的可見性,當一個線程修改了變量值,新值會立即對其他線程可見
- 防止指令重排序的發生
- volatile 無法保證變量操作的原子性
如何保證原子性?
解決非原子性問題,都可以通過加鎖的方式實現,synchronized和ReentrantLock都可以實現
Java還提供了一種方案,在不加鎖的情況下,實現++操作的原子性就是原子類
在java.util.concurrent包下,定義了許多與并發編程相關的處理類,此包一般大家簡稱為JUC
實現方式:采用CAS(比較并交換)思想,當多個線程對同一個內存數據操作時,假設A線程把主內存數據加載到自己的工作內存中,這個工作內存中的值就是預期值,然后在自己的工作內存中操作后,當寫回主內存時,先判斷預期值和主內存的值是否一致,如果一致說明還沒有其他線程修改,直接寫回主內存,一旦預期值和主內存的值不一樣,說明有其他線程已經修改過了,線程A需要重新獲取主內存的值,重新操作,判斷,直到預期值和主內存中的值相同才結束,否則一直判斷
由于采用自旋的方式,使得線程都不會阻塞,一直自旋,適合并發量低的場景,如果并發量過大,線程會一直自旋,會導致CPU開銷大
還會有一個ABA問題,線程A拿到主內存值后,期間有其他線程已經多次修改內存數據,最終又修改到和線程A拿到的值相同,可以通過帶版本號解決
鎖分類
樂觀鎖/悲觀鎖
樂觀鎖是一種不加鎖的實現,例如原子類,認為不加鎖,采用自旋的方式嘗試修改共享數據是不會有問題的,悲觀鎖是一種加鎖的實現,例如synchronized和ReentrantLock認為不加鎖修改共享數據會出問題
可重入鎖
又名遞歸鎖,當同一個線程,獲取鎖進入到外層方法中,可以在內存中可以進入另一個方法中(內層與外層使用同一把鎖)
synchronized和ReentrantLock也是可重入鎖
讀寫鎖
ReentrantReadWriteLock讀寫鎖實現
ReentrantReadWriteLockWriteLock()
ReentrantReadWriteLock.ReadLock()
讀讀不互斥
讀寫互斥
寫寫互斥
分段鎖
將鎖的粒度進一步細化,提高并發效率
hashtable是線程安全的,方法上都加了鎖,假如有兩個線程同時讀,也只能一個一個的讀,并發效率低
concurrentHashMap沒有給方法上加鎖,使用hashtable表中的每個位置上的第一個對象作為鎖對象,這樣可以多個線程對不同位置進行操作,相互不影響,只有對同一個位置操作時,才互斥
有多吧鎖,提高并發操作的效率
自旋鎖
線程嘗試不斷的獲取鎖,當第一次獲取不到時,線程不阻塞,嘗試繼續獲取鎖,有可能后面幾次嘗試后,有其他線程釋放了鎖,此時就可以獲取鎖,當嘗試獲取到一定次數后(默認10次),仍然獲取不到任何鎖,那么可以進入阻塞狀態
synchronized就是自旋鎖
并發量低適合自旋鎖
共享鎖/獨占鎖
共享鎖:可以被多個線程共享,讀寫鎖中的讀鎖就是共享鎖
獨占鎖:一把鎖只能有一個線程使用,讀寫鎖的寫鎖,synronized和reentrantLock都是獨占鎖
公平鎖/非公平鎖
公平鎖:可以按照請求獲得鎖的順序來得到鎖
非公平鎖:不按照請求獲取鎖的順序來得到鎖
synchronized是非公平的
ReentrantLock默認是非公平的,可以通過構造方法去改變成公平鎖
鎖狀態
無鎖
偏向鎖
一段同步代碼塊一直由一個線程執行,那么會在鎖對象中記錄下了線程信息,可以直接會的鎖
輕量級鎖
當鎖狀態為偏向鎖時,此時又有其他線程訪問,鎖狀態升級為輕量級鎖,線程不阻塞,采用自旋方式獲取鎖
重量級鎖
當鎖狀態為輕量級鎖時,如果有大量的線程到來,鎖狀態升級為重量級鎖,自旋的線程會進入阻塞狀態,由操作系統去調度管理
Synchronized
關鍵字,由jvm提供,實現同步,隱式的加鎖和釋放鎖修飾代碼塊和方法,修飾代碼塊時需要我們提供一個同步鎖對象,任意類的對象都可以,但只能是唯一一個,記錄線程有沒有進入到同步代碼塊,修飾方法時,對象是自己提供的,非靜態方法鎖對象默認為this,靜態方法鎖對象為當前的class對象,控制同步是依靠進入和退出監視器對象實現的,如果是同步方法,在指令中會為方法添ACC_SYNCHRONIZED標志,如果是同步代碼塊,在進入到同步代碼塊時,會執行monitorenter,離開同步代碼塊時或出異常時,執行monitorexit,為了提高鎖的獲取與釋放效率在對象頭中記錄鎖狀態,
AQS
抽象同步隊列,是一個實現線程同步的框架,并發包中有很多底層都用到了AQS,通過?FIFO 隊列?和?原子狀態變量(state)?管理線程的阻塞與喚醒,提供了統一的底層實現機制
核心思想:線程競爭資源時,通過 CAS 操作嘗試修改 state 獲取資源,成功則直接執行;失敗則封裝為節點進入隊列等待,通過 LockSupport 實現阻塞 / 喚醒。AQS 支持獨占(如 ReentrantLock)和共享(如 Semaphore)兩種模式,子類只需重寫 tryAcquire/tryRelease 等方法即可快速實現自定義同步器,是構建鎖、信號量等并發工具的高效底層機制
JUC常用類
ConcurrentHashMap
HashMap適合單線程,不允許多個線程同時訪問操作,如果有多線程訪問會報異常
Hashtable是線程安全的 直接給方法加鎖,效率低
ConcurrentHashMap是線程安全的 沒有直接給方法加鎖,用哈希表中每一個位置上的第一個元素作為鎖對象,哈希表的長度是16,那么就有16把鎖對象,鎖住自己的位置即可,這樣如果多個線程操作不同的位置,那么相互不影響,只有多個線程操作同一個位置,才會等待
如果位置上沒有何元素,那么會采用cas機制插入數據到對應位置
hashtable concurrentHashMap 鍵值都不能為空
CopyOnWriteArrlist
ArrayList是單線程場景下使用的,在多線程場景下會報異常
Vector 是線程安全的,在方法上加了鎖,效率低,讀寫用的同一把鎖
CopyOnWriteArrayList? 寫方法上加了鎖(ReentrantLock實現的),寫入數據時,先把圓數組做了一個備份,把要添加的數據寫入到備份數組中,當寫入完成后,在把修改后的數組賦值到原數組中去
給寫加鎖,讀沒有加鎖,讀的效率就變高了,適合寫操作少,讀操作多的場景
CopyOnWriteArraySet
CountDownLatch
線程池
為減少頻繁的創建和銷毀線程
JDk5引入了線程池,建議使用ThreadPoolExecutor類來創建線程池,提高了效率
ThreadPoolExecutor? 構造器的各個參數:
corePoolSize? 核心線程池的線程數量(初始化5)
maximumPoolSize? 線程池中最大線程數量(初始化10)
keepAliveTime? 空閑線程存活時間當核心線程池中的線程足以應付任務時,非核心線程池中的線程在指定空閑時間到期后,會銷毀
unit? 時間單位
workQueue??等待隊列,當核心線程池中的線程都在使用時,會先將等待的線程放到隊列中,如果隊列滿了,才會創建新的線程(非核心線程池中的線程)
threadFactory??線程工廠,用來創建線程池中的數量
handler??拒絕處理任務的策略
四大策略:AbortPolicy? 拋異常
CallRunsPolicy??由提交任務的線程執行,例如在main線程提交,則由main線程執行拒絕任務
DiscardOldestPolicy??丟棄等待時間最長的任務
DiscardPolicy??丟棄最后不能執行的任務
工作流程:
有大量工作任務到來時,先判斷核心線程池中的線程是否都忙著,有空閑的就讓核心線程池中的線程執行任務,沒有空閑的,就判斷等待的隊列是否已滿,如果沒滿,就把任務添加到隊列等待,如果隊列已經滿了,在判斷非核心線程池中的線程是否都忙著,如果有空閑的,,沒滿,就就由非核心的線程池中的線程執行,如果非核心線程池也滿了,就使用對應的拒絕策略處理
提交任務
void ececute 沒有返回值
submit? 有返回值
關閉線程池
shutdown??執行后不再接收任務,會把線程池中和等待隊列中已有的任務執行完后再停止
shutdownNow??立即執行,等待的任務不再執行
ThreadLocal
為每一個線程提供一個變量副本,只在當前線程中使用,相互隔離
實現:為每個Thread創建一個ThreadLocal.ThreadLocalMap threadLocals把變量副本放在ThreadLocal.ThreadLocalMap?中,用ThreadLocal對象統一作為鍵,每一個獲取變量時,先拿到當前線程,拿到線程中的ThreadLocal.ThreadLocalMap?
內存泄漏:對象已經不用了,但是垃圾回收不能回收該對象(例如數據庫連接對象,流對象,socket)
不再使用時調用remove方法,刪除鍵值對,可以避免內存泄漏問題
對象引用:
強引用
Object obj=new Object();強引用
對象如果有強引用關聯,那么肯定是不能被回收的
軟引用
被SoftReference類包裹的對象,當內存充足時,不會被回收,當內存不足時,即使有引用指向,也會被回收
弱引用
被WeakReference類所包裹的對象,只要發生垃圾回收,該類對象都會被回收掉
ThreadLocal被弱引用管理
當發生垃圾回收時,被回收掉,但是value還與外界保持著引用關系,
虛引用
被PhantomReference類包裹的對象,隨時都可以被回收,通過虛引用對象跟蹤對象回收狀態