併發編程

本文部份內容摘自:php

  1. 大白話AQS
  2. 深刻分析AQS實現原理



1、threadlocal

 threadlocal,在實際項目中應用:好比:保存本次請求用戶的信息、logId或者一些須要在一次request時須要都用的數據
html


2、線程的生命週期與狀態流轉

參考:java

Java併發編程系列:線程的五大狀態,以及線程之間的通訊與協做node

Java 線程的 5 種狀態android


3、線程的通訊與協做:

sleep、wait、notify、yield、join關係與區別:參考面試


4、線程同步與鎖

一、隊列同步器AQS

AQS的內部實現

  • AQS的實現依賴內部的同步隊列,也就是FIFO的雙向隊列,若是當前線程競爭鎖失敗,那麼AQS會把當前線程以及等待狀態信息構形成一個Node加入到同步隊列中,同時再阻塞該線程。當獲取鎖的線程釋放鎖之後,會從隊列中喚醒一個阻塞的節點(線程)。
  • AQS隊列內部維護的是一個FIFO的雙向鏈表,這種結構的特色是每一個數據結構都有兩個指針,分別指向直接的後繼節點和直接前驅節點。因此雙向鏈表能夠從任意一個節點開始很方便的訪問前驅和後繼。每一個Node實際上是由線程封裝,當線程爭搶鎖失敗後會封裝成Node加入到AQS隊列中去

AQS的兩種功能編程

從使用層面來講,AQS的功能分爲兩種:獨佔和共享數組

  • 獨佔鎖,每次只能有一個線程持有鎖,好比前面給你們演示的ReentrantLock就是以獨佔方式實現的互斥鎖安全

  • 共享鎖,容許多個線程同時獲取鎖,併發訪問共享資源,好比ReentrantReadWriteLockbash

相關方法屬性

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer{
    //指向同步隊列隊頭
    private transient volatile Node head;

    //指向同步的隊尾
    private transient volatile Node tail;

   //同步狀態,0表明鎖未被佔用,1表明鎖已被佔用
    private volatile int state;
}複製代碼


總結:如上圖所示爲AQS的同步隊列模型;

AQS內部有一個同步隊列,它是由Node組成的雙向鏈表結構
AQS內部經過state來控制同步狀態,當執行lock時,若是state=0時,說明沒有任何線程佔有共享資源的鎖,此時線程會獲取到鎖並把state設置爲1;當state=1時,則說明有線程目前正在使用共享變量,其餘線程必須加入同步隊列進行等待.

AQS內部分爲共享模式(如Semaphore)和獨佔模式(如Reentrantlock),不管是共享模式仍是獨佔模式的實現類,都維持着一個虛擬的同步隊列,當請求鎖的線程超過現有模式的限制時,會將線程包裝成Node結點並將線程當前必要的信息存儲到node結點中,而後加入同步隊列等會獲取鎖,而這系列操做都有AQS協助咱們完成,這也是做爲基礎組件的緣由,不管是Semaphore仍是Reentrantlock,其內部絕大多數方法都是間接調用AQS完成的。


二、釋放鎖以及添加線程對於隊列的變化

添加節點


當出現鎖競爭以及釋放鎖的時候,AQS同步隊列中的節點會發生變化,首先看一下添加節點的場景。裏會涉及到兩個變化

  • 新的線程封裝成Node節點追加到同步隊列中,設置prev節點以及修改當前節點的前置節點的next節點指向本身

  • 經過CAS講tail從新指向新的尾部節點

釋放鎖移除節點

head節點表示獲取鎖成功的節點,當頭結點在釋放同步狀態時,會喚醒後繼節點,若是後繼節點得到鎖成功,會把本身設置爲頭結點,節點的變化過程以下


這個過程也是涉及到兩個變化

  • 修改head節點指向下一個得到鎖的節點

  • 新的得到鎖的節點,將prev的指針指向null

這裏有一個小的變化,就是設置head節點不須要用CAS,緣由是設置head節點是由得到鎖的線程來完成的,而同步鎖只能由一個線程得到,因此不須要CAS保證,只須要把head節點設置爲原首節點的後繼節點,而且斷開原head節點的next引用便可

三、公平鎖與非公平鎖

ReentrantLock.lock()public void lock() {    
   sync.lock();
}複製代碼

這個是獲取鎖的入口,調用sync這個類裏面的方法,sync是什麼呢?

sync是一個靜態內部類,它繼承了AQS這個抽象類,前面說過AQS是一個同步工具,主要用來實現同步控制。咱們在利用這個工具的時候,會繼承它來實現同步控制功能。 經過進一步分析,發現Sync這個類有兩個具體的實現,分別是 NofairSync(非公平鎖), FailSync(公平鎖).

  • 公平鎖 表示全部線程嚴格按照FIFO來獲取鎖

  • 非公平鎖 表示能夠存在搶佔鎖的功能,也就是說無論當前隊列上是否存在其餘線程等待,新線程都有機會搶佔鎖

面試題:公平鎖與非公平鎖是經過什麼實現的?CAS


四、重入鎖加鎖釋放鎖的步驟

ReentrantLock內部包含了一個AQS對象,也就是AbstractQueuedSynchronizer類型的對象。這個AQS對象就是ReentrantLock能夠實現加鎖和釋放鎖的關鍵性的核心組件。

AQS內部有個變量 state ,是int類型的,表明了 加鎖的狀態 。初始狀態下,state值是0。
AQS內部還有個 變量 ,用來記錄 當前加鎖的是哪一個線程 ,初始化狀態下,變量是null。
AQS內部還有一個等待隊列,專門放那些加鎖 敗的線程!

  • 一、線程1跑過來調用ReentrantLock的lock()方法嘗試進行加鎖,這個加鎖的過程,直接就是用CAS操做將state值從0變爲1。一旦線程1加鎖成功了以後,就能夠設置當前加鎖線程是本身。

如何進行可重入加鎖! 其實每次線程1可重入加鎖一次,會判斷一下當前加鎖線程就是本身,那麼他本身就能夠可重入屢次加鎖,每次加鎖就是把state的值給累加1,別的沒變化

  • 二、線程2跑過來一下看到,state的值不是0啊?因此CAS操做將state從0變爲1的過程會失敗,由於state的值當前爲1,說明已經有人加鎖了!接着線程2會看一下,是否是本身以前加的鎖啊?固然不是了,「加鎖線程」這個變量明確記錄了是線程1佔用了這個鎖,因此線程2此時就是加鎖失敗。

  • 三、接着,線程2會將本身放入AQS中的一個等待隊列,由於本身嘗試加鎖失敗了,此時就要將本身放入隊列中來等待,等待線程1釋放鎖以後,本身就能夠從新嘗試加鎖了


AQS是如此的核心!AQS內部還有一個等待隊列,專門放那些加鎖失敗的線程!

  • 四、接着,線程1在執行完本身的業務邏輯代碼以後,就會釋放鎖!他釋放鎖的過程很是的簡單,就是將AQS內的state變量的值遞減1,若是state值爲0,則完全釋放鎖,會將「加鎖線程」變量也設置爲null!


  • 五、接下來,會從等待隊列的隊頭喚醒線程2從新嘗試加鎖。線程2如今就從新嘗試加鎖,這時仍是用CAS操做將state從0變爲1,此時會成功,成功以後表明加鎖成功,就會將state設置爲1。此外,還要把「加鎖線程」設置爲線程2本身,同時線程2本身就從等待隊列中出隊了。



五、Synchronized 和 ReentrantLock 區別

本題轉自 :公衆號來源:孤獨煙

API方面: synchronized既能夠修飾方法,也能夠修飾代碼塊。ReentrantLock只能在方法體中使用。
公平鎖: synchronized的鎖是非公平鎖,ReentrantLock默認狀況下也是非公平鎖,但能夠經過帶布爾值的構造函數要求使用公平鎖。
等待可中斷: 假如業務代碼中有兩個線程,Thread1 Thread2。假設 Thread1 獲取了對象object的鎖,Thread2將等待Thread1釋放object的鎖。

  • 使用synchronized。若是Thread1不釋放,Thread2將一直等待,不能被中斷。synchronized也能夠說是Java提供的原子性內置鎖機制。內部鎖扮演了互斥鎖(mutual exclusion lock ,mutex)的角色,一個線程引用鎖的時候,別的線程阻塞等待。
  • 使用ReentrantLock。若是Thread1不釋放,Thread2等待了很長時間之後,能夠中斷等待,轉而去作別的事情。

至於判斷重入鎖, ReenTrantLock的字面意思就是再進入的鎖,其實synchronized關鍵字所使用的鎖也是可重入的,二者關於這個的區別不大。二者都是同一個線程沒進入一次,鎖的計數器都自增1,因此要等到鎖的計數器降低爲0時才能釋放鎖。


5、併發包

擴展:hashset,hashtable

0. hashset:無序、不重複

HashSet底層使用了哈希表來支持的,特色:存儲快

哈希表的出現是爲了解決鏈表訪問不快速的弱點,哈希表也稱散列表。

HashSet是經過HasMap來實現的,HashMap的輸入參數有Key、Value兩個組成,在實現HashSet時,保持HashMap的Value爲常量,至關於在HashMap中只對Key對象進行處理。

HashSet存儲對象的過程

往HashSet添加元素的時候,HashSet會先調用元素的hashCode方法獲得元素的哈希值 ,

而後經過元素的哈希值通過移位等運算,就能夠算出該元素在哈希表中的存儲位置。

狀況1: 若是算出元素存儲的位置目前沒有任何元素存儲,那該元素能夠直接存儲到該位置上

狀況2: 若是算出該元素的存儲位置目前已經存在有其餘的元素了,那麼會調用該元素的equals方法與該位置的元素再比較一次,若是equals返回的是true,那麼該元素與這個位置上的元素就視爲重複元素,不容許添加,若是equals方法返回的是false,那麼該元素運行添加。

1. hashmap

因此說,當數組長度爲2的n次冪的時候,不一樣的key算得得index相同的概率較小,那麼數據在數組上分佈就比較均勻,也就是說碰撞的概率小,相對的,查詢的時候就不用遍歷某個位置上的鏈表,這樣查詢效率也就較高了。
說到這裏,咱們再回頭看一下hashmap中默認的數組大小是多少,查看源代碼能夠得知是16,爲何是16,而不是15,也不是20呢,看到上面的解釋以後咱們就清楚了吧,顯然是由於16是2的整數次冪的緣由,在小數據量的狀況下16比15和20更能減小key之間的碰撞,而加快查詢的效率。 

2. concurrentMap

摘自:JDK1.8中的實現

ConcurrentHashMap取消了segment分段鎖,而採用CAS和synchronized來保證併發安全。數據結構跟HashMap1.8的結構同樣,數組+鏈表/紅黑二叉樹

synchronized只鎖定當前鏈表或紅黑二叉樹的首節點,這樣只要hash不衝突,就不會產生併發,效率又提高N倍。TreeBin: 紅黑二叉樹節點,Node: 鏈表節點。


  1. 判斷Node[]數組是否初始化,沒有則進行初始化操做
  2. 經過hash定位Node[]數組的索引座標,是否有Node節點,若是沒有則使用CAS進行添加(鏈表的頭結點),添加失敗則進入下次循環。
  3. 檢查到內部正在擴容,若是正在擴容,就幫助它一塊擴容。
  4.  若是f!=null,則使用synchronized鎖住f元素(鏈表/紅黑二叉樹的頭元素):         f:鏈表或紅黑二叉樹頭結點,向鏈表中添加元素時,須要synchronized獲取f的鎖。
    4.1 若是是Node(鏈表結構)則執行鏈表的添加操做。
    4.2 若是是TreeNode(樹型結果)則執行樹添加操做。
  5. 判斷鏈表長度已經達到臨界值8 就須要把鏈表轉換爲樹結構。

總結:
    JDK8中的實現也是鎖分離的思想,它把鎖分的比segment更細一些,只要hash不衝突,就不會出現併發得到鎖的狀況。它首先使用無鎖操做CAS插入頭結點,若是插入失敗,說明已經有別的線程插入頭結點了,再次循環進行操做。若是頭結點已經存在,則經過synchronized得到頭結點鎖,進行後續的操做。性能比segment分段鎖又再次提高。

3. ConcurrentLinkedQueue

參見:Java併發包--ConcurrentLinkedQueue , 

         Java併發編程之ConcurrentLinkedQueue詳解

ConcurrentLinkedQueue是一個基於連接節點的無界線程安全隊列,它採用FIFO先進先出的規則對節點進行排序,當咱們添加一個元素的時候,它會添加到隊列的尾部,當咱們獲取一個元素時,它會返回隊列頭部的元素。

ConcurrentLinkedQueue的數據結構,以下圖所示:


說明
1. ConcurrentLinkedQueue繼承於AbstractQueue。
2. ConcurrentLinkedQueue內部是經過鏈表來實現的。它同時包含鏈表的頭節點head和尾節點tail。ConcurrentLinkedQueue按照 FIFO(先進先出)原則對元素進行排序。元素都是從尾部插入到鏈表,從頭部開始返回。
3. ConcurrentLinkedQueue的鏈表Node中的next的類型是volatile,並且鏈表數據item的類型也是volatile。關於volatile,咱們知道它的語義包含:「即對一個volatile變量的讀,老是能看到(任意線程)對這個volatile變量最後的寫入」。ConcurrentLinkedQueue就是經過volatile來實現多線程對競爭資源的互斥訪問的。


4. 阻塞隊列

摘自:阻塞隊列及實現原理

三種阻塞隊列:
BlockingQueue<Runnable> workQueue = null;
workQueue = new ArrayBlockingQueue<>(5);   //基於數組的先進先出隊列,有界
workQueue = new LinkedBlockingQueue<>();   //基於鏈表的先進先出隊列,無界
workQueue = new SynchronousQueue<>();      //無緩衝的等待隊列,無界

一、ArrayBlockingQueue是一個用數組實現的有界阻塞隊列。此隊列按照先進先出(FIFO)的原則對元素進行排序。默認狀況下不保證訪問者公平的訪問隊列,

訪問者的公平性是使用可重入鎖實現的,代碼以下:

public ArrayBlockingQueue(int capacity, boolean fair) {
        if (capacity <= 0)
            throw new IllegalArgumentException();
        this.items = new Object[capacity];
        lock = new ReentrantLock(fair);
        notEmpty = lock.newCondition();
        notFull =  lock.newCondition();
}複製代碼

二、LinkedBlockingQueue是一個用鏈表實現的有界阻塞隊列。此隊列的默認和最大長度爲Integer.MAX_VALUE。此隊列按照先進先出的原則對元素進行排序。

三、SynchronousQueue是一個不存儲元素的阻塞隊列。每個put操做必須等待一個take操做,不然不能繼續添加元素。SynchronousQueue能夠當作是一個傳球手,負責把生產者線程處理的數據直接傳遞給消費者線程。隊列自己並不存儲任何元素,很是適合於傳遞性場景,好比在一個線程中使用的數據,傳遞給另一個線程使用,SynchronousQueue的吞吐量高於LinkedBlockingQueue 和 ArrayBlockingQueue。

總結:


六. 併發工具類 

CountDownLatch、Semaphore和CyclicBarrier

參考:Java併發之CountDownLatch、Semaphore和CyclicBarrier

          CountDownLatch使用之等待超時   

CountDownLatch:
.await(long, TimeUnit); 等待超時,針對某些業務場景,若是某一個線程的操做耗時很是長或者發生了異常. 可是並不想影響主線程的繼續執行, 則可使用await(long, TimeUnit)方法. 即一個線程(或者多個線程),等待另外n個線程執行long時間後繼續執行. 


七. 原子操做類

摘自:AtomicInteger

synchronized :重量級操做,基於悲觀鎖,可重入鎖。

AtomicInteger:樂觀 ,用CAS實現

incrementAndGet()方法在一個無限循環體內,不斷嘗試將一個比當前值大1的新值賦給本身,若是失敗則說明在執行"獲取-設置"操做的時已經被其它線程修改過了,因而便再次進入循環下一次操做,直到成功爲止。
CAS指令在Intel CPU上稱爲CMPXCHG指令, 它的做用是將指定內存地址的內容與所給的某個值相比,若是相等,則將其內容替換爲指令中提供的新值,若是不相等,則更新失敗。這一比較並交換的操做是原子的,不能夠被中斷。初一看,CAS也包含了讀取、比較 (這也是種操做)和寫入這三個操做,和以前的i++並無太大區別,是的,的確在操做上沒有區別,但 CAS是經過硬件命令保證了原子性,而i++沒有,且硬件級別的原子性比i++這樣高級語言的軟件級別的運行速度要快地多。雖然CAS也包含了多個操做,但其的運算是固定的(就是個比較),這樣的鎖定性能開銷很小。


經過查看AtomicInteger的源碼可知, 

private volatile int value;

public final boolean compareAndSet(int expect, int update) {        

       return unsafe.compareAndSwapInt(this, valueOffset, expect, update); 

經過申明一個volatile (內存鎖定,同一時刻只有一個線程能夠修改內存值)類型的變量,再加上unsafe.compareAndSwapInt的方法,來保證明現線程同步的。

優勢:AtomicInteger比直接使用傳統的java鎖機制(阻塞的)有什麼好處最大的好處就是能夠避免多線程的優先級倒置死鎖狀況的發生,固然高併發下的性能提高也是很重要的。

比較:

  • 低併發狀況下:使用AtomicInteger,由於其是基於樂觀鎖,併發低,基本都能成功。
  • 高併發狀況下:使用synchronized,若是此時使用AtomicInteger,失敗的機率很大,incrementAndGet()就須要一直不斷重複的嘗試,直到成功。既然很大狀況會失敗,就直接synchronized鎖住


8、線程池相關

摘自:Java-五種線程池,四種拒絕策略,三種阻塞隊列

一、線程池要隔離

摘自:構建更健壯的系統:不一樣的業務放在不一樣的線程/線程池裏面

二、拒絕策略

三、阻塞隊列 原理:AtomicInteger

四、如何合理設置線程池大小

對於不一樣性質的任務來講,
  • CPU密集型任務應配置儘量小的線程,如配置CPU個數+1的線程數,
  • IO密集型任務應配置儘量多的線程,由於IO操做不佔用CPU,不要讓CPU閒下來,應加大線程數量,如配置兩倍CPU個數+1,
  • 而對於混合型的任務,若是能夠拆分,拆分紅IO密集型和CPU密集型分別處理,前提是二者運行的時間是差很少的,若是處理時間相差很大,則不必拆分了。


9、線程安全的程序計數器

線程安全的計數器實現原理簡介:
在java中volatile關鍵字能夠保證共享數據的可見性,它會把更新後的數據從工做內存刷新進共享內存,並使其餘線程中工做內存中的數據失效,進而從主存中讀入最新值來保證共享數據的可見性,實現線程安全的計數器經過循環CAS操做來實現。就是先獲取一箇舊指望值值,再比較獲取的值與主存中的值是否一致,一致的話就更新,不一致的話接着循環,直到成功爲止.

程序參考:java如何實現線程安全的計數器

Java 提供了一組atomic class來幫助咱們簡化同步處理。基本工做原理是使用了同步synchronized的方法實現了對一個long, integer, 對象的增、減、賦值(更新)操做. 

程序參考:Java線程安全的計數器  ; 

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息