Java高併發分析介紹

一、線程和線程池


1.線程池介紹

  • 如果每個任務都新開一個線程,並且還需要銷燬開銷太大,不需要給每個任務都開一個線程,可以線程複用避免反覆開啓和銷燬線程;
  • 在任務少時可以維護固定數量線程,任務多會有最大線程數以及任務隊列,動態擴容和縮容應對突發流量以及資源利用率。

2.線程池創建和停止線程池

2.1參數介紹

  • corePoolSize:一直會保持的線程數,對每一個到來的任務只要沒有空閒線程就會創建;
  • maxPoolSize:超過核心線程數的部分會有存活時間,不會一直保存的線程數,當任務隊列和corePoolSIze都滿了之後會進行創建;
  • keepAliveTime:超過核心線程池的線程存活時間;
  • workQueue:阻塞隊列SynchronousQueue無容量,LinkedBlockingQueue容量很大,ArrayBlockingQueue設置固定容量,放入等待執行的任務;
  • threadFactory:創建線程,通常情況默認的即可;
  • Handler:當workQueue,corePoolSize,maxPoolSize都滿之後的拒絕策略;

2.2創建線程

  • newFixedThreadPool:corePoolSize=maxPoolSize=n,keepAliveTime=0,workQueue=LinkedBlockingQueue無界隊列;
  • newSingleThreadExecutor:corePoolSize=maxPoolSize=1,workQueue=LinkedBlockingQueue無界隊列;
  • cacheThreadPool:corePoolSize=0,maxPoolSize=2的32方,keepAliveTime=60,workQueue=SynchronousQueue無容量;
  • newSheduledThreadPool:定時/延時/週期執行任務corePoolSize=n,maxPoolSize=2的32方,keepAliveTime=0,workQueue=DelayedWorkQueue延時隊列;
  • workStealingPool:任務可以切分成子任務的場景,線程之間竊取任務來平衡任務量,合理利用線程資源
  • threadPoolExecutor:自定義各個參數來創建線程池;

2.3停止線程

  • shutdown:將當前存在的任務執行完畢,新的任務不會再增加;
  • isShutDown:判斷是否進入shutdown狀態;
  • isTeriminated:判斷線程池是否完全停止;
  • awaitTeriminated:等待一段時間是否線程池完全停止;
  • shutdownNow:立刻終止,清除所有任務和當前線程;

4.線程池任務太多,如何進行拒絕

  • 當線程池關閉,或者達到最大線程數並且工作隊列飽和都會進行拒絕策略;
  • AbortPolicy:拋出異常策略;
  • DiscardPolicy:丟棄策略;
  • DiscardOldestPolicy:丟棄最老的策略;
  • CallerRunsPolicy:提交任務的線程去執行的策略;

5.線程池鉤子方法

  • 在每個任務執行前後增加功能----日.4志、統計等;
  • 重寫ThreadPoolExecutor這個類的beforeExecute以及afterExecute方法;

6.線程池實現原理

  • Executor接口只有execute方法----->ExecutorService接口增加了shutdown,submit等方法---->AbstractExecutorService抽象類----->ThreadPoolExecutor類;
  • Executors類通過ThreadPoolExecutor類創建對應線程池,屬於線程池工廠;

6.1線程池的狀態

  • RUNNING接收新任務並處理排隊任務;
  • SHUTDOWN不接受新任務但處理排隊任務;
  • STOP不接收新任務,也不處理排隊任務,並中斷正在進行的任務;
  • TIDYING所有任務都以處理完畢,並運行鉤子方法;
  • TERMINATED鉤子方法也運行完畢;

6.2線程池的組成

  • 線程池管理器、工作隊列、任務隊列、任務接口

7.線程介紹

  • Runnable:實現run方法,不能有返回值,不能拋出異常;
  • Callable:類似於Runnable,實現call方法,有返回值,可以拋出異常;

8.Future

  • 任務異步執行,結果阻塞獲取,判斷任務是否執行完畢;
  • 線程池的submit方法會返回Future對象的,execute方法不會返回,cancle取消任務;
  • FutureTask獲取Future和任務結果,將Runnable,Future,Callable包裝起來(實現Runnable,Future,組裝Callable),通過適配器模式,可以執行Runnable可以作爲Future得到Callable的返回值

二、ThreadLocal


1.ThreadLocal的用途

  • 每個線程需要獨享對象---線程不安全,可以ThreadLocal實現initialValue方法;
  • 單個線程需要保存全局變量---防止傳參麻煩,可以ThreadLocal直接set值,當前線程的所有調用方法都可獲取到值;

2.主要原理

  • 一個Thread包含n個ThreadLocal;
  • 一個ThreadLocalMap包含一個Thread;
  • 一個ThreadLocalMap包含n個Entry(key和value);
  • 實際上每個線程都含有一個不同的ThreadLocalMap,而map裏又含有n個ThreadLocal以及對應的value值(key和value組合爲Entry);
  • 我們定義的ThreadLocal一般爲全局靜態變量每個線程的ThreadLocalMap不同,經過相同的ThreadLocal計算得到的index是相同的,所以實際是ThreadLocalMap進行存儲;

2.1方法介紹

  • initialValue:初始化值,懶加載,get的時候纔會初始化;
  • set:爲這個線程設置一個ThreadLocal值;
  • get:得到線程所對應的值,首次調用會觸發initialValue方法------獲取當前線程的ThreadLocalMap,根據ThreadLocal在Map中獲取到對應的value值;
  • remove:刪除這個對應線程的值;

3.注意點

  • ThreadLocal--key是弱引用(每次垃圾回收時會被回收,除非有其他引用指向來改變生命週期),value值對應強引用;
  • 只要ThreadLocal沒有外部強引用,那麼進行gc後key就會被回收,而value在Entry中還有強引用是不會被gc回收的,通過key是獲取不到value將他置爲null,至此value會永遠也沒法回收除非線程退出,否則就會造成內存泄露;
  • 在set、remove、rehash方法中會掃描key爲null的Entry,會把value設置爲null;
  • 在task任務執行的時候ThreadLocal是擁有外部強引用的,但當任務執行之後ThreadLocal的強引用也就結束了,如果不在任務執行完畢之前手動remove就可能會造成內存泄露;

ps:堆上的ThreadLocal對象,如果方法運行完畢棧上的強引用就會失效,下一次gc弱引用也會失效,那麼key對象就會被回收,value也就內存泄露了;


三、各種鎖


1.Lock接口

1.1介紹

  • Lock可以提供synchronized沒有的高級功能,支持wait/signal

1.2方法介紹

  • lock:如果無法加鎖進行等待;
  • tryLock:可立即返回,可設置超時時間(並可響應中斷);
  • lockInterruptibly:超時時間無限,等待鎖的過程中可以被中斷;

3.樂觀鎖和悲觀鎖

  • 要不要鎖住同步資源;
  • 悲觀鎖:每次獲取資源並修改的時候都要把數據鎖住,確保內容正確,比如synchronized,Lock的相關類;
  • 樂觀鎖:先獲取到當前值,更新值的時候先和內存中的數據比對是否相同,如果沒有改變就更新值成功,如果改變了就重試,比如CAS算法,原子類,併發容器中就大量使用;
  • 悲觀鎖是直接加鎖(適合併發寫很多/持有鎖時間長/大量IO操作的場景),樂觀鎖不會直接加鎖而是在操作系統底層加鎖更加輕量(適合併發寫少讀多的場景,否則重試次數太多失敗率大)

4.可重入鎖(ReentrantLock)和非可重入鎖

  • 同一個線程是否可以重複獲取同一把鎖;
  • 可重入鎖可以避免死鎖,一般來說第二次獲取相同的鎖需要先釋放掉這就會導致死鎖,通過累加計數來控制鎖的控制次數;

5.公平鎖和非公平鎖

  • 多線程競爭時是否排隊;
  • 非公平鎖效率更高,誰最先獲取到鎖誰就可以得到,如果要保證公平就可能需要喚醒等待線程帶來消耗;
  • ReentrantLock鎖可以支持公平和非公平鎖;

6.共享鎖和排他鎖(ReentrantReadWriteLock)

  • 多線程是否共享一把鎖;
  • 排他鎖:獨佔,獨享鎖,比如寫鎖,synchronized等;
  • 共享鎖:又稱爲讀鎖,可以多個線程共同獲取鎖;
  • ReentrantReadWriteLock:多個線程可以共同申請讀鎖,申請寫鎖會等待讀鎖全部釋放,申請讀鎖會等待寫鎖釋放,讀和寫之間會互相阻塞要麼多讀要麼一寫
  • 非公平策略:如果當前持有讀鎖,寫鎖會等待,讀鎖插隊寫鎖,這樣可能會造成飢餓,所以讀鎖無法插隊寫鎖,可以避免飢餓,寫鎖隨時可以插隊讀鎖在隊列頭節點不是寫鎖的情況下可以插隊
  • 公平策略:全部都無法進行插隊,嚴格的按照順序;
  • 鎖降級:從寫鎖直接獲取讀鎖進行降級,然後釋放寫鎖,不支持鎖升級,因爲多讀就可能會有多個線程同時想要升級,只能一個升級成功如果其他讀鎖不釋放就會存在有讀又有寫,如果每個線程都在等待其他線程釋放讀鎖就會陷入死鎖
  • StampedLock:支持樂觀讀鎖(不加鎖) 、悲觀讀鎖(使用CAS)、寫鎖(使用CAS),來更好的提升性能;

7.自旋鎖和阻塞鎖

  • 阻塞等待還是自旋獲取鎖;
  • 阻塞或喚醒線程需要切換操作系統的狀態,自旋不需要狀態的切換隻需要cpu不斷重試執行代碼,當切換狀態耗時比自旋重試還要耗時那麼自旋鎖性能會更好,否則相反;
  • 自旋鎖的實現是CAS--樂觀鎖,阻塞鎖的實現是synchronized--悲觀鎖;
    private AtomicReference<Thread> sign = new AtomicReference<>();
    public void lock() {
        Thread current = Thread.currentThread();
        //初始值爲null,修改爲current
        while (!sign.compareAndSet(null, current)) {
            System.out.println("自旋獲取失敗,再次嘗試");
        }
    }
    public void unlock() {
        Thread current = Thread.currentThread();
        //會將值重新設置爲null,讓其他阻塞的線程重新獲取鎖
        sign.compareAndSet(current, null);
    }

8.可中斷鎖

  • 鎖是否可以中斷,synchronized不可中斷,Lock是可中斷鎖;

9.鎖優化

  • JVM的優化自旋鎖和自適應(嘗試一定次數),鎖消除(對一定不會出現併發的情況下消除掉),鎖粗化(將小的加鎖代碼塊合併爲大的);

四、Atomic包


1.介紹

  • 保證併發安全;
  • 基於樂觀鎖CAS來實現,高度競爭下導致大量失敗,失敗率高可能效率不如直接加的悲觀鎖;

2.基本類型原子類

  • AtomicInteger、AtomicLong、AtomicBoolean
  • get:獲取當前值;
  • getAndSet:獲取當前值,並設置新值;
  • getAndIncrement:獲取當前值,並自增;
  • getAndDecrement:獲取當前值,並自減;
  • getAndAdd:獲取當前值,並加上預期值;
  • compareAndSet:如果預期值expect等於當前值,則以原子方式將該值設置爲輸入值update;

3.數組類型原子類

  • AtomicIntegerArray、AtomicL ongArray、AtomicReferenceArray,還是以基本原子類爲基礎的;

4.引用類型原子類

  • AtomicReference、AtomicStampedReference、AtomicMarkableReference,指定泛型;
  • private AtomicReference<Thread> sign = new AtomicReference<>();

5.普通變量升級爲原子類

  • AtomicIntegerFieldupdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater;

6.Adder累加器

  • LongAdder、DoubleAdder,使用了多段鎖提高了併發性;
  • 基本原子類每次累加都要同步到主內存然後刷新到線程的工作內存,而累加器是每個線程不進行同步而是自己累加最後進行彙總(只在彙總的時候進行同步值),高併發下累加器表現更好;

7.Accumulator累加器

  • LongAccumulator、DoubleAccumulator,擴展了累加器,還可以進行乘法、最大最小值等操作

五、CAS和final


1.CAS介紹

  • 傳入當前值A,在要更改成值B前會先判斷值A與內存值V是否相等,如果相等修改成功,否則失敗;

2.CAS原理介紹

  • 加載Unsafe工具,用來直接操作內存數據,來實現底層操作,會調用c語言,然後最終調用匯編和機器的原子指令保證原子性
  • 用volatile修飾value字段,保證可見性和有序性,原子性本身就具備,保證了內存模型的語義;

3.CAS缺點

  • ABA問題:值沒發生改變,但版本號發生了改變;
  • 自旋時間長,會很消耗CPU,高併發會導致大量失敗,難以成功;

4.final介紹

  • 類防止被繼承,方法防止被重寫,變量防止被修改,天生保證線程安全,存在方法區(靜態變量也會在方法區);

5.final用法

  • 如果聲明類的final變量,只能等號賦值、構造函數賦值、初始化代碼塊賦值這三種情況;
  • 如果final修飾對象,只是對象的引用不可變,而對象本身的屬性可變;

六、併發容器


1.介紹

  • HashTable:線程安全的Map性能不好,HashMap線程不安全,LinkedHashMap、TreeMap;
  • Vector:線程安全的List性能不好,ArrayList線程不安全;

2.ConcurrentHashMap

  • Map線程安全性能好;
  • HashMap的死循環造成的CPU100%:鏈地址法解決衝突,頭插法進行擴容,多線程下線程來回切換(頭插法擴容節點會反向)會造成鏈表節點循環,此問題只在1.7造成,1.8使用了尾插法避免擴容節點反向解決了此問題;
  • HashMap:JDK7時只是使用了鏈地址法,而JDK8時使用了鏈地址法+紅黑樹;
  • ConcurrentHashMap:JDK1.7使用了分段鎖(保證總體的原子性)+volatile(保證各個鎖之間的可見性與有序性)來提高併發,如果是同一把鎖根本就不需要volatile來保證可見性與有序性的,JDK1.8放棄了分段鎖使用了CAS+Synchronized來實現;
  • put操作的大致過程:判斷是否發生哈希衝突,如果沒有發生使用CAS設置節點,判斷是否是轉移節點,如果是那麼要等擴容完成纔可插入,如果發生了哈希衝突鎖住頭節點,先進行鏈表判斷如果中途有相同key判斷是否可以覆蓋,在進行紅黑樹判斷與鏈表類似(需要鎖住樹的根節點),最後判斷是否鏈表可以轉化爲紅黑樹,如果覆蓋了oldVal值會進行返回;(get操作類似put的判斷過程)
  • 紅黑樹爲什麼爲8:正常情況下不會轉化爲紅黑樹,當數據有針對性時會造成鏈表過長這時轉化爲紅黑樹可以解決極端境況,設置爲8是經過概率統計計算得來,達到8的概率非常小並且我麼也不希望鏈表長度會達到8;

3.CopyOnWriteArrayList

  • List線程安全性能好,適合讀操作多,寫操作少的場景;
  • 讀寫規則:讀寫鎖(只有讀讀不互斥,其他均互斥),mysql(樂觀併發控制保證讀讀和讀寫不互斥--因爲讀取的是快照,悲觀併發控制讀寫是互斥的),而CopyOnWriteArrayList(讀讀和讀寫不互斥,寫的時候修改快照完成之後纔會進行設置,借鑑了mysql的樂觀併發控制),且迭代器經過了重寫會拷貝當前數組(類變量)對list修改不會報錯滴但也不可見滴,重寫的迭代器不支持增刪改

4.併發隊列(阻塞隊列、非阻塞隊列)

  • 阻塞隊列在原本的非阻塞隊列上加入了阻塞取數據和放數據;
  • 阻塞隊列:SynchronousQueue(容量爲0+不進行存儲元素)、ArrayBlockingQueue(有界+指定容量+一把鎖+循環隊列)、PriorityBlockingQueue(使用堆實現+無界)、LinkedBlockingQueue(無界+容量爲Int最大值+兩把鎖+鏈表)、DelayQueue(使用優先級隊列來實現),線程池一個重要的組成部分也是阻塞隊列來存放任務;
  • take和put:刪除元素並返回,會進行阻塞;插入元素,會進行阻塞;
  • add、remove和element:插入元素,會拋出異常;刪除元素並返回,會拋出異常;返回隊列頭元素,會拋出異常;
  • offer、poll和peek:添加元素,返回布爾值;刪除元素並刪除,返回元素值/null;取元素不刪除,返回元素/null;
  • 非阻塞隊列:ConcurrenLinkedQueue(鏈表+CAS實現非阻塞)

七、控制併發流程


1.介紹

  • 線程之間相互配合與合作,滿足業務邏輯;

2.CountDownLatch倒計時門閂

  • 類似CyclicBarrier線程之間互相等待,數量遞減到0,纔會觸發下一步操作,不可重複使用
  • await:掛起線程,直到count值爲0纔可繼續執行;
  • countDown:將count減一,直到爲0時,等待的線程會被喚醒;
  • 用法一:(一等多)一個線程等待多個線程都執行完畢,再繼續自己的工作;

  • 用法二:(多等一,比如壓測場景)多個線程等待一個線程執行完畢,多個線程才能繼續自己的工作;

3.Semaphore信號量

  • 通過許可證來控制線程,線程只有拿到許可證才能繼續運行,限制和管理有限資源的使用(限制併發量);
  • acquire和release:獲取許可證,會進行阻塞;釋放許可證;
  • tryAcquire:嘗試獲取,立馬返回,可以設置超時時間;

4.Condition條件對象

  • 控制線程的等待和喚醒;
  • await、signal和signalAll:掛起當前線程(必須持有鎖+會自動釋放鎖);喚醒等待的線程(等待時間最長的);喚醒所有線程;
  • 可以用於生產者和消費者模式(阻塞隊列+lock+notFull條件+notEmpty條件),生產者滿了會進行await自身且signal消費者,消費者空了會進行await自身且signal生產者;

5.CyclicBarrier循環柵欄

  • 適用於線程之間互相等待場景,直到足夠多的線程達到規定好的數目,纔可以進行下一步操作(支持重複使用),每次所有線程都到達之後還能進行一個回調方法;
  • await:將掛起線程和countDown整合成一個;
  • 應用:將線程任務分成多個部分來進行完成,多個部分之間呈依賴關係,線程的一個task任務部分爲一個循環

八、AQS


1.介紹

  • 在併發類裏面提取出的一個抽象類,對公用部分進行實現其他的交給子類實現,屬於模板設計模式;

2.實現原理

  • state:代表當前狀態,持有鎖的情況;
  • 等待隊列:雙鏈表實現,head表示已經拿到鎖的線程,其他的節點會被阻塞;
  • 子類去實現嘗試獲取和釋放鎖的方法(都要通過CAS操作實現)可以設置公平和非公平,自己實現了在等待隊列的共享鎖和排他鎖的獲取和釋放的邏輯(將當前線程放入等待隊列中);

3.應用

  • CountDownLatch中的應用:初始化時會將count值設置爲state值,await時會調用tryAcquireShared(只有在state爲0纔可成功),countDown時會調用tryReleaseShared(每次將state減一);
  • Semaphore中的應用:初始化會將許可證數量設置爲state值,tryAcquire進行嘗試獲取許可證, 
  • ReentrantLock的應用:實現tryAcquire和tryRelease方法,由於可重入所以state代表重入次數(同一個線程對同一個鎖可多次獲得)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章