併發和多線程(三)--併發容器J.U.C和lock簡介

AQS:

  是AbstractQueuedSynchronizer的簡稱,JUC的核心node

  底層是sync queue雙向鏈表,還可能有condition queue單向鏈表,使用Node實現FIFO隊列,能夠用於構建同步隊列或者其餘同步裝置的基礎程序員

框架數組

  利用了int類型表示狀態,在AQS中有個state的成員變量,基於AQS的ReentrantLock,state表示獲取鎖的線程數,等於0,沒有,1有,緩存

大於1表示重入鎖的數量。服務器

protected final int getState() {    //獲取當前同步狀態
   return this.state;
}

protected final void setState(int var1) {    //設置當前同步狀態
   this.state = var1;
}

//使用CAS設置當前狀態,該方法可以保證狀態設置的原子性
protected final boolean compareAndSetState(int var1, int var2) {
    return unsafe.compareAndSwapInt(this, stateOffset, var1, var2);
}

 基於模板方法,須要繼承AQS,重寫某些方法多線程

能夠實現排它鎖和共享鎖的模式(獨佔Reentrantlock、共享countdownlatch,同時實現一種)併發

一、實現思路:框架

  首先AQS中維護了一個queue來管理鎖,線程會嘗試獲取鎖,若是失敗,就將當前線程以及等待狀態等信息封裝成一個node節點,加入到syncjvm

queue的tail,head node的線程釋放鎖的時候,會喚醒隊列中的後繼線程,然後續節點在獲取鎖成功的時候把本身設置爲首節點工具

就是由於這些設計,jdk有不少基於AQS的設計,一些經常使用的組件:

  countdownlatch、semaphore、CyclicBarrier、Reentrantlock、Condition、Futuretask等


二、同步組件:

2.1).CountDownLatch(閉鎖):

  能夠實現阻塞當前的線程,經過一個計數器進行初始化,這個計數器都是進行原子操做,只能同時有個線程操做這個計數器,調用CountDown

Latch的await()會處於阻塞狀態,其餘線程調用Countdown(),每次減一,直到計數器變成零

  這時候全部由於調用await()阻塞的線程才能繼續往下執行,CountDownLatch只能執行一次,不能重置,想要使用重置的計數器,可使用


2.2).CyclicBarrier

  await()須要等到countdown()將計數器減到0,纔會執行後續的代碼。await()能夠有時間參數,選擇等待多長時間事後就會執行await後續的

代碼。

countDown()儘可能卸載finally內部
countDownLatch.await();
countDownLatch.await(10, TimeUnit.MILLISECONDS);

使用場景:

2.3).Semaphore:信號量

  控制併發訪問的個數,用於只能提供有限訪問的資源

semaphore.acquire(3); // 獲取多個許可
test(threadNum);
semaphore.release(3); // 釋放多個許可
Semaphore semaphore = new Semaphore(2);容許線程數必定要大於等於acquire和release的個數
semaphore.tryAcquire()嘗試獲取許可,沒有獲取許可的線程都會丟棄
semaphore.tryAcquire(5000, TimeUnit.MILLISECONDS)在5000ms中嘗試獲取許可

 2.4).CyclicBarrier

  用於多線程計算數據,最後合併計算結果,例如Excel不少頁流水,經過多線程計算每一頁流水,最後計算總的

經過調用await()方法,線程進入等待狀態,計數器進行加一操做,當值等於設置的初始值時,全部阻塞的線程繼續執行

private static CyclicBarrier barrier = new CyclicBarrier(5, () -> {
    log.info("callback is running");
});

經過使用lambda,當計數器知足條件優先執行lambda表達式裏面的代碼

2.5).CyclicBarrier和CountDownLatch區別:

  一、CountDownLatch只能使用一次,而CyclicBarrier能夠循環利用,使用reset進行重置

  二、CountDownLatch描述:1或N個線程須要等待其餘線程完成某個操做,才能繼續往下執行

  三、CyclicBarrier:多個線程之間相互等待,知道全部線程都知足某個條件才能繼續執行後續操做,是各個線程直接相互等待的操做

countdown表現:    CountDownLatch表現:

1 is ready        1 is ready
2 is ready        2 is ready
3 is ready        3 is ready
1 continue        1 continue
2 continue        2 continue
3 continue        3 continue
4 is ready        4 is ready
4 continue        5 is ready
5 is ready        6 is ready
5 continue        4 continue
6 is ready        5 continue
6 continue        6 continue

LOCK

一、ReadWriteLock:

二、ReentrantReadWriteLock:

  支持多線程讀,若是有一個線程已經佔用了讀鎖,則此時其餘線程若是要申請寫鎖,則申請寫鎖的線程會一直等待釋放讀鎖。

若是有一個線程已經佔用了寫鎖,則此時其餘線程若是申請寫鎖或者讀鎖,則申請的線程會一直等待釋放寫鎖。

 

一個是讀操做相關的鎖,稱爲共享鎖;一個是寫相關的鎖,稱爲排他鎖

一個線程要想同時持有寫鎖和讀鎖,必須先獲取寫鎖再獲取讀鎖;寫鎖能夠「降級」爲讀鎖;讀鎖不能「升級」爲寫鎖。

讀讀共享、其餘都是互斥

三、ReentrantLock:

  注意不要把lock的實例化作成局部變量,每一個線程執行該方法時都會保存一個副本,那麼理所固然每一個線程執行到lock.lock()處獲取的是不一樣

的鎖,這樣lock就不能起做用了。

 

一、synchronized:

  可重入性,jvm實現,在以前和ReentrantLock性能差異很大,可是引入了偏向鎖、輕量級鎖,效率已經相差不大,只能使用非公平鎖,

能夠經過一些工具進行監控,jvm自動作加鎖、解鎖操做

二、ReentrantLock:

  可重入性,jdk實現,粒度更小,能夠指定公平鎖(先等待的線程先得到鎖)或非公平鎖,提供一個condition類,

能夠實現分組喚醒須要喚醒的線程,而synchronized關鍵字要麼喚醒一個線程,要麼所有線程,能夠經過lockInterruptibly()中斷等待鎖的線程

機制,必定要記得在finally釋放鎖

 

四、StampLock:

  對吞吐量有很大的改進,性能上有很大的提高,特別是適合讀操做比較多的狀況

ReentrantLock、ReentrantReadWriteLock、StampLock等lock都是對象層面的鎖定

 

五、鎖使用原則:

  一、當只有少許線程競爭的時候,可使用synchronized,並且不會引起死鎖

  二、線程競爭很多,線程增加可以預估,能夠選擇ReentrantLock

可重入鎖:

  synchronized和ReentrantLock都是可重入鎖

  鎖的分配機制是基於線程的分配,而不是基於方法的分配,在method1中已經獲取了對象鎖,在方法內部調用method2不用從新獲取鎖。

可中斷鎖:

  synchronized就不是可中斷鎖,而Lock是可中斷鎖。

  lockInterruptibly()的用法時已經體現了Lock的可中斷性。

公平鎖:

  公平鎖是指當一個鎖被釋放的時候,等待時間最長的線程會獲取該鎖,非公平鎖可能致使某些線程永遠不會獲取到鎖

  synchronized不是公平鎖,ReentrantLock和ReentrantReadWriteLock,它默認狀況下是非公平鎖,可是能夠設置爲公平鎖。

  ReentrantLock在實例化的時候參數true表示公平鎖,false表示非公平鎖,並且有不少判斷鎖狀態的方法。

讀寫鎖:

  多線程讀操做不會發生衝突

condition:await()、signal()能夠實現多路通知功能,可是通知部分線程要使用多個condition類,不然會所有喚醒

J.U.C組件拓展:

Callable與Runnable、Thread接口對比:

Future接口:

  能夠獲得線程任務方法的返回值

FutureTask類:

  實現了Runnable、Future,使用場景:線程A作一件事,線程B作別的事,在須要的時候能夠的到線程A的返回值

Fork/Join(jdk1.7):

  就是把大任務拆分紅若干小任務,放到雙端隊列,每一個隊列分配一個線程,先作完的線程幫助其餘線程,一個從下面,一個從上面,並行執行

,最終彙總結果,可是某些狀況下仍是有線程競爭的狀況

 

侷限性:

  一、只能經過fork、join進行操做 二、不能有io操做 三、任務不能拋出檢查異常

Queue:

  除了優先級隊列和LIFO隊列外,隊列都是以FIFO(先進先出)的方式對各個元素進行排序的

add(E e):

  將元素e插入到隊列末尾,若是插入成功,則返回true;若是插入失敗(即隊列已滿),則會拋出異常

remove():

  移除隊首元素,若移除成功,則返回true;若是移除失敗(隊列爲空),則會拋出異常

offer(E e):

  將元素e插入到隊列末尾,若是插入成功,則返回true;若是插入失敗(即隊列已滿),則返回false

poll():

  移除並獲取隊首元素,若成功,則返回隊首元素;不然返回null

peek():

  獲取隊首元素,若成功,則返回隊首元素;不然返回null


注意點:
  一、對於非阻塞隊列,通常狀況下建議使用offer、poll和peek三個方法,不建議使用add和remove方法。由於使用offer、poll和peek三個方法

以經過返回值

  二、判斷操做成功與否,而使用add和remove方法卻不能達到這樣的效果。注意,非阻塞隊列中的方法都沒有進行同步措施。

  三、阻塞隊列對於上面五個方法有作同步處理,而非阻塞隊列沒有同步

put(E e)
take()
offer(E e,long timeout, TimeUnit unit)
poll(long timeout, TimeUnit unit)

put方法用來向隊尾存入元素,若是隊列滿,則等待;
take方法用來從隊首取元素,若是隊列爲空,則等待;
offer方法用來向隊尾存入元素,若是隊列滿,則等待必定的時間,當時間期限達到時,若是尚未插入成功,則返回false;不然返回true;
poll方法用來從隊首取元素,若是隊列空,則等待必定的時間,當時間期限達到時,若是取到,則返回null;不然返回取得的元素;

 

一、BlockingQueue:

  主要用在生產者消費者場景,不須要關注何時阻塞和喚醒

二、ArrayBlockingQueue:

  有界的阻塞隊列,就是容量是有限的,初始化指定容量大小,FIFO,內部是由數組實現

三、DelayQueue:

  必須實現Delay接口,它的元素要進行排序,應用場景:定時關閉鏈接、緩存對象,超時處理等

四、LinkedBlockingQueue:

  內部是鏈表,和ArrayBlockingQueue類似,FIFO

五、priorityBlockingQueue:

  容許插入null

六、SynchronousQueue:

  只能插入一個值,插入一個元素就會阻塞,也叫同步隊列

線程池:

優勢:

  一、減小了建立和銷燬線程的次數,每一個工做線程均可以被重複利用,可執行多個任務

  二、能夠根據系統的承受能力,調整線程池中工做線程的數據,防止由於消耗過多的內存致使服務器崩潰

 

ThreadPoolExecutor:

  注意corePoolSize、maximumPoolSize、workQueue等參數之間的關係,詳情見9-1

參數:

一、corePoolSize

  核心池的大小。在建立了線程池以後,默認狀況下,線程池中沒有任何線程,而是等待有任務到來才建立線程去執行任務。默認狀況下,在建立了線程池以後,

  線程池中的線程數爲0,當有任務到來後就會建立一個線程去執行任務

二、maximumPoolSize

  池中容許的最大線程數,這個參數表示了線程池中最多能建立的線程數量,當任務數量比corePoolSize大時,任務添加到workQueue,當workQueue滿了,

將繼續建立線程以處理任務,maximumPoolSize表示的就是wordQueue滿了,線程池中最多能夠建立的線程數量


三、keepAliveTime

  只有當線程池中的線程數大於corePoolSize時,這個參數纔會起做用。當線程數大於corePoolSize時,終止前多餘的空閒線程等待新任務的最長時間

四、unit

  keepAliveTime時間單位

五、workQueue

  存儲還沒來得及執行的任務

六、threadFactory

  執行程序建立新線程時使用的工廠

七、handler

  因爲超出線程範圍和隊列容量而使執行被阻塞時所使用的處理程序

解釋:

  一、池中線程數小於corePoolSize,新任務都不排隊而是直接添加新線程

  二、池中線程數大於等於corePoolSize,workQueue未滿,首選將新任務加入workQueue而不是添加新線程

  三、池中線程數大於等於corePoolSize,workQueue已滿,可是線程數小於maximumPoolSize,添加新的線程來處理被添加的任務

  四、池中線程數大於大於corePoolSize,workQueue已滿,而且線程數大於等於maximumPoolSize,新任務被拒絕,使用handler處理被拒絕的任務

強烈建議程序員使用較爲方便的Executors工廠方法Executors.newCachedThreadPool()(無界線程池,能夠進行線程自動回收)、

Executors.newFixedThreadPool(int)(固定大小線程池)和Executors.newSingleThreadExecutor()(單個後臺線程),它們均爲大多數使用場景預

定義了設置。因此重點關注一下JDK推薦的Executors


併發最佳實踐:

  一、使用本地變量

  二、使用不可變類

  三、最小化鎖的做用範圍:S=1/(1-a+a/n)

  四、寧肯使用同步也不使用線程的wait和notify

  五、使用BlockingQueue實現生產-消費模式

  六、使用併發集合而不是加了鎖的同步集合

  七、使用Semaphore建立有界的訪問

  八、在使用synchronized時,寧肯使用同步代碼塊,也不使用同步方法

  九、避免使用靜態變量

相關文章
相關標籤/搜索