併發編程相關知識點

多線程並不一定比單線程處理的效率高,開啓過多的線程,會增加上下文切換的開銷,降低了效率。

一、如何降低上下文切換的開銷:無鎖併發編程、CAS算法、使用最少線程、使用協程

       無鎖併發編程:多線程競爭鎖會產生額外的上下文切換開銷,因此多線程處理數據時儘量減少鎖的使用。例如對數據id進行hash算法取模分段,不同的線程處理不同的數據段。

       CAS算法:java.concurrent.atomic包下的原子類使用的就是CAS算法保證數據的原子性。

       使用最少線程:避免創建過多的線程,不需要那麼多線程,卻創建了許多。

       使用協程:單線程裏實現多任調度,並在單線程裏持多個任務間的切

可以通過jstack查看某進程id下線程的具體情況,也可使用jstack生成線程的dump文件,對線程的具體情況進行分析。

二、Volatile的理解

Volatile保證變量的可見性,即當一個線程修改了某變量的值,其他線程看到該變量的值就是修改以後的。

1、爲何能保證變量的可見性???

       操作系統跟數據相關的大致分爲處理器,內存,高速緩存區。爲了提高速度,處理器一般不直接跟內存進行通信,而是先將系統內存中的數據讀取到高速緩存區,然後再做操作。當對變量進行了寫操作時,JVM會向處理器發送一條帶lock前綴的指令,將這個變量在高速緩存區的數據回寫到內存中。由於各處理器之間爲了保證緩存一致性,都實現了緩存一致性協議,每個處理器通過嗅探在總線上的數據來檢查自己緩存的數據是否失效,當一個處理器將高速緩存區的數據回寫到內存中時,其它處理器會發現其緩存區緩存的該數據在內存中的地址發生了改變,於是將該數據在緩存中的地址設置爲無效。當對該數據進行再次修改時,會重新從內存中讀取數據到緩存區中。

2、多個處理器操作緩存區,最後寫入到內存中的是哪個緩存區的數據?

       緩存一致性機制會阻止同時修改由兩個以上處理器緩存的內存區域數據。

三、synchronized原理

鎖的從低到高依次爲:無鎖--》偏向鎖--》輕量級鎖--》重量級鎖

1、java對象頭

synchronized使用的鎖就是在java對象頭中。

java對象頭包括Mark Word(存儲對象的hashCode或者鎖的信息)、Class Metadata Address(存儲到對象類型數據的指針)、Array Length(存儲數組的長度)

若對象是數組,則對象頭由以上三部分組成,若對象是非數組,則對象頭只由Mark Word、Class Metadata Address組成。

若虛擬機是32位的,則Mark Word、Class Metadata Address、Array Length的長度就是32位的;

若虛擬機是64位的,則Mark Word、Class Metadata Address的長度就是64位的,但是Array Length的長度還是32位。

2、Mark Word

32位的虛擬機中,默認情況下Mark Word中包含2位lock,1位biased lock,4位gc age,25位對象的hashCode

64位的虛擬機中,默認情況下Mark Word中包含2位lock,1位biased lock,4位gc age,31位對象的hashCode,1位cms_free,25位未使用

lock:鎖標誌位,表示鎖的類型,跟biased結合使用,請看下圖

biased:偏向鎖標識,是否使用了偏向鎖,使用了就是1,未使用就是0

gc age:java對象年齡,java對象沒經過一次gc,年齡加一,由於gc age佔4位二進制,所以gc age最大值爲15

在運行期間,Mark Word會隨着鎖標誌位的變化而變化,可能變化存儲爲以下狀態

3、偏向鎖

3.1、偏向鎖的設計初衷是:大多數情況下鎖不存在多線程競爭關係,某同步代碼塊總是由同一個線程獲得並執行。

3.2、偏向鎖的競爭升級

      當線程1訪問代碼塊並獲取鎖對象時,會在對象頭以及棧幀中記錄偏向的鎖的線程ID,以後該線程進入該代碼塊就不需要使用CAS進行加鎖和解鎖了。因爲偏向鎖不會主動釋放對象頭中的線程的ID,因此當其他線程想要競爭時,會根據對象頭中的線程ID查看該線程釋放存活,若死亡,則將對象頭中該線程ID清空設置成無鎖狀態,其他線程可通過競爭獲取偏向鎖。若線程還存活,則會查找該線程的棧幀信息,看是否需要繼續持有該鎖,若需要,則暫停該線程,撤銷偏向鎖,將其升級爲輕量級鎖,然後喚醒該線程。

3.3、關閉偏向鎖

關閉偏向鎖,偏向鎖一般是在應用程序啓動以後幾秒鐘纔會激活,可通過參數關閉延遲:-XX:BiasedLockingStartupDelay=0

若確定應用程序中的所有所通常情況下都是競爭的,則可關閉偏向鎖::-XX:-UseBiasedLocking=false,程序默認進入輕量級鎖狀態。

4、輕量級鎖

4.1線程競爭,鎖升級

線程1訪問代碼,先在棧幀中分配一塊空間用於存儲鎖記錄,然後將Mark Word的內容拷貝到該空間中,這一塊空間官方稱爲Displaced Mark Word,然後開始通過CAS將對象頭中的Mark World替換爲指向鎖記錄的指針,若替換成功,則代表獲取到了該輕量級鎖。這時線程2也開始訪問該代碼,先在棧幀中分配一塊空間用於存儲鎖記錄,然後將Mark Word的內容拷貝到該空間中,然後通過CAS嘗試將對象頭中的Mark World替換爲指向鎖記錄的指針,但是由於線程1已獲得了,線程2失敗,於是線程2通過自旋不停的嘗試獲取。線程1執行完代碼塊中的代碼,準備釋放鎖,通過CAS將Displaced Mark Word替換回對象頭,但是由於線程2還在自旋競爭,於是替換失敗,膨脹位重量級鎖。線程2由於競爭的輕量級鎖升級爲重量級鎖,線程2阻塞。輕量級鎖升級爲重量級鎖以後線程1釋放鎖,並喚醒處於阻塞狀態的線程2。線程2開始進行新一輪的競爭。

5、三種鎖的優缺點

6、處理器如何實現原子操作

通過總線鎖保證原子性:總線鎖就是使用處理器提供的一個LOCK#信號,當一個處理器在總線上輸出此信號時,其他處理器的請求將被阻塞住,那麼該處理器可以獨佔共享內存

通過緩存鎖定來保證原子性:是指內存區域如果被緩存在處理器的緩存行中,並且在Lock操作期間被鎖定,那麼當它執行鎖操作回寫到內存時,處理器不在總線上聲言LOCK#信號,而是修改內部的內存地址,並允許它的緩存一致性機制來保證操作的原子性,因爲緩存一致性機制會阻止同時修改由兩個以上處理器緩存的內存區域數據,當其他處理器回寫已被鎖定的緩存行的數據時,會使緩存行無效

注:總線鎖定把CPU和內存之間的通信鎖住了,這使得鎖定期間,其他處理器不能操作其他內存地址的數據,所以總線鎖定的開銷比較大,目前處理器在某些場合下使用緩存鎖定代替總線鎖定來進行優化

四、線程的基礎

1、線程的優先級值越大,線程越優先,大的優先級高於低的優先級,但是線程的優先級不能作爲程序正確性的依賴,因爲操作系統可能不理會java程序對於優先級的設置。

2、一個進程中若無非守護線程,則jvm就自動退出了。但是jvm在退出的時候,守護線程的finally塊並不一定會執行。因此在構建守護線程的時候,不能依靠finally塊的內容來確保執行關閉或清理資源的邏輯。

3、線程的中斷可通過interrupt(),線程本身可通過isInterrupted()來判斷是否被中斷,也可調用靜態方法Thread.interrupted()對當前線程的中斷標識進行復位。如果該線程已經處於終結狀態,即使該線程被中斷過,在調用該線程對象的isInterrupted()時依舊會返回false。

從java的API可以看到,許多聲明拋出InterruptedException的方法(例如Thread.sleep(long millis)方法),這些方法在拋出InterruptedException之前,java虛擬機會先將該線程的中斷標識位清除,然後拋出InterruptedException,此時調用isInterrupted()方法將會返回false。

4、線程的suspend、resume、stop都是不建議使用的,因爲使用這幾個方法可能會使資源不被釋放,依舊處於佔用狀態,最後導致死鎖。例如suspend是在佔用資源的情況下進入睡眠的,資源未被釋放,而stop則是直接停止線程,沒有給資源釋放的機會,導致程序可能處於不正確的狀態。

5、使用線程的wait(),notify(),notifyAll()方法,需要先對調用對象加鎖。調用notify()或notifyAll()方法以後,處於WAITING狀態的線程不會獲得對象的鎖,因爲調用notify()或notifyAll()並不會釋放對象的鎖,需要調用該方法的線程釋放鎖。

6、notify()方法將等待列中的一個等待程從等待列中移到同步列中,而notifyAll() 方法則是將等待列中所有的程全部移到同步列,被移程狀WAITING變爲BLOCKED。

7、管道輸/出流和普通的文件/出流或者網絡輸/出流不同之在於,它主要 用於線程之的數據傳輸,而傳輸的媒介內存。

管道/出流主要包括瞭如下4種具體實現PipedOutputStreamPipedInputStream、 PipedReader和PipedWriter,前兩種面向字,而後兩種面向字符。

五、同步器(AbstractQueuedSychronizer)AQS

1、隊列同步器:是用來構建鎖和其它同步組件的基礎框架,它使用了一個int成員變量表示同步狀態,通過內置的FIFO隊列完成資源獲取線程的排隊工作。

同步器只要提供三個方法,getSatate(),setState(int newState),compareAndSetState(int expect,int update),子類通過繼承同步器並實現其其抽象方法來實現對同步狀態的管理。

2、隊列同步器的實現:同步隊列、獨佔式同步狀態獲取和釋放、共享式同步狀態獲取與釋放、超時獲取同步狀態

2.1當一個線程獲取同步狀態失敗時,同步器會將當前線程以及等待狀態等信息構造成一個節點(Node)並將其加入同步隊列,同時會阻塞當前線程,當同步狀態釋放時,會把隊列中首節點中的的線程喚醒,使其再次嘗試獲取同步狀態。

2.2同步隊列中節點(Node)用來保存獲取同步狀態失敗線程的引用、等待狀態以及前驅和後繼節點。

2.3線程獲取同步器狀態失敗,轉而被構造成節點並加入同步隊列隊尾,這個加入隊列的過程必須是安全的,因此同步器提供了一個基於CAS的設置尾結點的方法:compareAndSetTail(Node expect,Node update).

設置首節點不需要CAS方法保證而設置尾結點需要CAS保證的原因:設置首節點是根據獲取同步狀態成功的線程來完成的,只有一個線程能成功獲取到同步狀態,因此不用CAS保證,而獲取狀態失敗的線程可能有很多,因此需要CAS保證加入尾結點的安全性。

獨佔式同步狀態的獲取和釋放:在獲取同步狀態時,同步器會維護一個同步隊列,獲取狀態失敗的線程都會被加入到同步隊列中並在隊列中進行自旋;移出隊列或者停止自旋的條件是前驅節點爲頭結點且成功獲取了同步狀態。在釋放同步狀態時,同步器調用tryRealease(int arg)方法釋放同步狀態,然後喚醒頭節點的後繼節點。

共享式同步狀態的獲取和釋放:在共享式獲取自旋的過程中,成功獲取到同步狀態並推出自旋的條件就是當前節點的前驅節點爲頭節點,當前節點嘗試獲取同步狀態,使用tryAcquireShared(int arg)方法返回值大於等於0。跟獨佔式不同之處在於,共享式要確保同步狀態的釋放是安全的,一般通過循環和CAS,因爲同步狀態的釋放操作可能來自多個線程。

獨佔式超時獲取同步狀態:獨佔式超時獲取同步狀doAcquireNanos(int arg,long nanosTimeout)和獨佔式獲取同步狀acquire(int args)在流程上非常相似,其主要區在於未取到同步狀態時的邏輯acquire(int args)在未取到同步狀態時,將會使當前程一直於等待狀態,而doAcquireNanos(int arg,long nanosTimeout)會使當前程等待nanosTimeout秒,如果當前線程在nanosTimeout秒內沒有取到同步狀,將會從等待邏輯中自返回。

六、Java中的鎖

1、lock接口,其實現類可以手動加鎖,然後手動釋放鎖。但是注意在finally模塊釋放鎖,保證鎖能被釋放。但是獲取鎖的過程不能放在try中,要放在try外,不然在獲取鎖的過程中如果發生了異常,異常拋出的同時,也會導致鎖無故釋放。

2、重入鎖(ReentrantLock、Sychronized)

支持重進入的鎖,它表示該鎖能過支持一個線程對資源的重複加鎖。除此之外,該鎖還支持獲取鎖時的公平性和非公平性的選擇。

重進入和釋放:ReentrantLock是通過組合自定義同步器來實現鎖的獲取和釋放。ReentrantLock內部獲取同步狀態的判斷沒變,但是多了一個步驟,當同一個線程再次獲取同步狀態時,判斷該線程是否跟已獲取同步狀態的線程是一個線程,若是同一個線程,則同步狀態加一,並返回true,代表獲取同步狀態成功。當該線程釋放時,如果該鎖被獲取了n次,那麼前n-1次釋放方法必須返回false,而只有同步狀態都釋放了才返回true。當同步狀態返回爲0時,將佔有線程設置爲null,並返回爲true,代表釋放成功。

公平和非公平獲取鎖:公平性與否是針對鎖而言的,如果一個鎖是公平的,那麼鎖的獲取順序就應該符合請求的絕對時間順序,也就是FIFO。公平性獲取鎖和非公平性獲取鎖的區別是:獲取鎖的時候多了一個判斷方法,判斷加入同步隊列中該節點是否有前驅節點,如果返回true,則代表有前驅節點,只能等前驅節點獲取同步狀態並釋放以後,當前節點才能繼續獲取鎖。

公平性獲取鎖保證了鎖的獲取按照FIFO原則,而代價是進行大量的線程切換。非公平性鎖雖然可能造成線程"飢餓",但極少的線程切換,保證了更大的吞吐量。

3、讀寫鎖

3.1、一般情況下讀寫鎖的性能都比排它鎖的性能好,因爲大多數場景都是讀多於寫。java併發包提供的讀寫鎖的實現是ReentrantReadWriteLock。該鎖支持:公平性選擇、重進入、鎖的降級(寫鎖降級爲讀鎖)。

3.2、讀寫鎖的實現設計

定義一個整型的變量來維護多個讀線程和一個寫線程的狀態。32位的整型變量,高16位表示讀,低16表示寫。

寫鎖是一個支持重入的排它鎖。讀鎖是一個支持重入的共享鎖。

3.3、鎖的降級

若當前線程已獲取了寫鎖,然後再獲取到讀鎖,然後釋放寫鎖,則成爲鎖的降級。

3.4、鎖的升級

若當前線程已經獲得了讀鎖,然後再獲取寫鎖,然後釋放讀鎖,則成爲鎖的升級。

ReentrantReadWriteLock支持鎖的降級,但不支持鎖的升級。目的是保證數據可見性,多個線程同時獲取了讀鎖,若其中一個線程又獲得了寫鎖並更新了數據,這些數據的更新對那些獲得讀鎖的線程不可見。

4、LockSupport工具

當需要阻塞或者喚醒線程的時候都需要LockSupport工具,其park()和unpark()方法,jdk1.6以後在parkNanos(long nanos)中增加了一個blocker參數,用來標識當前線程正在等待的對象。jdk1.6以及以後變成parkNanos(Object blocker,long nanos)。

5、condition

任何一個java對象,都有一組監視器方法(定義在Object上),主要包括wait(),wait(long timeout),notify(),notifyAll(),這些方法跟sychronized關鍵字配合,可以實現等待/通知模式。而Condition接口提供了跟Object類似的監視器方法,與Lock配合實現等待/通知模式。Object監視器擁有一個同步隊列一個等待隊列,而lock則擁有一個同步隊列和多個等待隊列。

Lock lock = new ReentrantLock

Conditon conditon = lock.newCondition()

ConditionObject是同步器AbstractQueuedSychronizer的內部類,因爲Condition的操作需要獲取相關的鎖。每個Condition對象都包含一個隊列,該隊列是Condition對象實現等待/通知功能的關鍵。如果調用condition.await()方法,則必須先獲取鎖。

進入等待狀態:同步隊列的首節點獲取到鎖以後,如果調用了condition.await()方法,則該首節點的線程被構建成一個新的節點進入等待隊列中,然後釋放同步狀態,喚醒同步隊列中的後繼節點,同時當前線程會進入等待狀態。

通知:另一個線程獲取到了鎖,然後調用conditon.signal()方法,然後獲取等待隊列中的首節點,然後將其移動到同步隊列的尾結點,然後使用LockSupport喚醒該節點中的線程,被喚醒的線程將會從await()方法中的while循環中退出,進而調用同步器的acquireQueued()方法加入到獲取同步狀態的競爭中去。若成功獲取同步之後,被喚醒的線程將會從之前調用的await()方法返回,此時該線程已經成功獲取到了鎖。

condition.signalAll()方法,相當於同步隊列中的每個節點均執行一次signal方法,效果就是等待隊列中的所有節點全部移動到同步隊列中,並喚醒每個節點的線程。

七、Java併發容器和框架

1、ConcurrentHashMap

1.1、多線程情況下,使用hashMap的put會導致死循環,導致CPU利用率接近100%,因爲多線程會導致hashMap的Entry鏈表形成環形數據結構,一旦形成環形數據結構,Entry的next節點永遠不爲空,就會產生死循環獲取Entry。

產生的原因:例如有一個只有2個容量的數組,在1位置存放了3,7,5,這個時候新加入數據發現達到了閾值,就得擴容,擴容以後是4,這裏位置的算法按照取模來算,則3和7都對應下標爲3的地方,5對應下標爲1的地方,但是如果這個時候多線程擴容將數據重新排放,則線程一先把3放進去,然後再把7放到3的前面,現在就是7-3,然後線程二拿到了CPU,開始執行,它會開始重排,結果3放在了首位,也就是3-7-3,然後再放7,就變成了7-3-7-3,其實就變成了7《--》3,產生了死循環。

1.2、hashMap和ConcurrentHashMap擴容

hashMap是在數據插入以後判斷是否達到了閾值,如果達到了就數據擴容兩倍,如果擴容後沒有數據插入,則就進行了一次無效的插入。但是ConcurrentHashMap就不一樣,它是在插入之前判斷Segment裏的HashEntry數組是否達到閾值,如果達到了就進行擴容,但爲了高效,ConcurrentHashMap不會對整個容器擴容,而只是對某個segment進行擴容。

1.3、統計ConcurrentHashMap裏元素的大小

統計每個Segment裏元素的大小,然後求和。但是如果統計每個Segment的元素大小count時,某個Segment對元素進行了操作,那麼就不準確了,但是對每個Segment加鎖然後統計,會導致低效。ConcurrentHashMap默認認爲對元素個數統計相加的時候,之前累加的count變化的概率非常小的。ConcurrentHashMap先嚐試2次通過不加鎖的方式統計大小,如果統計過程中count發生了變化,則再使用加鎖統計。至於如何判斷髮生了變化,其內部有一個modCount屬性,每次put,remove,clean操作,modCount都會加一,統計count前後對比modCount是否發生了變化即可。

2、阻塞隊列(BlockingQueue)

2.1、當隊列滿的時候,隊列會阻塞插入元素的線程,直到隊列不滿。當隊列爲空時,獲取元素的線程會被阻塞,直到隊列非空。

一直阻塞的方法:put,take     超時阻塞的方法:offer,poll

2.2、java中的阻塞隊列

2.2.1、ArrayBlockingQueue

一個用數組實現的有界阻塞隊列,按照FIFO對元素進行排序。默認情況下不保證線程公平的訪問隊列。但是可使用重入鎖保證線程訪問的公平性。

2.2.2、LinkedBlockingQueue

一個用鏈表實現的有界阻塞隊列,按照FIFO對元素進行排序。此隊列默認的和最大長度爲Integer.MAX_VALUE。

2.2.3、PriorityBlockingQueue

一個支持優先級的無界阻塞隊列,默認情況下按照自然順序升序排列。

2.3.4、DelayQueue 

一個支持延時獲取元素的無界阻塞隊列。隊列中的元素必須實現Delayed接口,在創建元素時可以指定多久能從隊列中獲取元素,只有在延遲期滿才能從隊列中提取元素。

2.3.5、SynchronousQueue

一個不存儲元素的阻塞隊列,每一個put操作必須等待一個take操作,否則不能繼續添加元素。默認情況下線程使用非公平的策略訪問隊列。其吞吐量高於LinkedBlockingQueue和ArrayBlockingQueue。

2.3.6、LinkedTransferQueue

一個由鏈表結構組成的無界阻塞隊列。其多了transfer和tryTransfer方法

如果有消費者等待消費,則transfer方法會將生產者生產的消息直接發送給消費者。如果沒有生產者,則transfer會將元素存放在隊列的tail節點,然後等待元素被生產者消費以後再返回。

tryTransfer方法試探生產者生產的消息能否直接發送給消費者。如果沒有消費者等待消費,則直接返回false。tryTransfer是無論是否有消費者消費均直接返回,而不像transfer等待再返回。

tryTransfer(E e,long timeout,TimeUnit unit)方法試圖把生產者傳入的元素直接給消費者,如果沒有消費者消費則等待指定時間以後再返回。如果超時元素還沒被消費,則返回false,如果超時時間內消費了元素,則返回true。

2.3.7、LinkedBlockingDeque

一個有鏈表結構組成的雙向阻塞隊列。

3、Fork/Join框架

3.1、Fork/Join框架是java 7提供的一個用於執行任務的框架,是一個把大任務分割成若干個小任務,最終彙總每個小任務的結果最後得到大任務結果的框架。

3.2、工作竊取算法:大任務分爲n多個小任務,爲了減少線程間的競爭,把不同的任務放到不同的隊列中,併爲每個隊列創建一個線程。當一個隊列中的線程完成了任務,而其他的隊列還未完成,則其去其他隊列幫忙,爲了防止衝突,因此使用的是雙端隊列,被竊取放從隊列頭拿任務,竊取方從隊列尾拿任務。

優點:充分利用線程進行計算,減少了線程間的競爭;‘缺點:某些情況下存在競爭。例如雙端隊列只有一個任務、該算法會消耗更多的系統資源,比如創建多個線程和多個雙端隊列。

3.3、Fork/Join框架的設置

創建ForkJoin任務,不需要繼承ForkJoinTask類,繼承其子類RecursiveAction:用於沒有返回結果的任務或RecursiveTask:用於有返回結果的任務,這個兩個子類都有fork(),join()方法。然後用ForkJoinPool執行ForkJoinTask。

八、Java中13個原子類操作

JDK1.5開始提供了java.util.concurrent.atomic包,該包裏的類基本都是使用Unsafe實現的包裝類。分爲原子更新基本類型、原子更新數組、原子更新引用和原子更新屬性。

1、原子更新基本類型

AtomicBoolean、AtomicInteger、AtomicLong

都是先拿到當前的值,然後使用unsafe.compareAndSet()方法,對比拿到的當前的值跟要加的值是否一樣,一樣則加一,如果不一樣則for循環進行compareAndSet()。

unsafe只提供了三種CAS方法,CompareAndSwapObject、CompareAndSwapInt、CompareAndSwapLong。通過看AtomicInBooelan的源碼,發現是先把Boolean轉換成整型,再使用CompoareAndSwapInt進行CAS,所以原子更新char,float,double也可以用類似的思路實現。

2、原子更新數組`

AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray、AtomicInteger

注意:數組通過構造方法傳遞進原子更新數組,然後原子更新數組會將傳入的數組複製一份,所以當原子更新數組對內部的數組進行操作時,不會影響傳入數組的原數據。

3、原子跟更新引用類型

AtomicReference:原子更新引用型;AtomicReferenceFieldUpdate:原子更新引用型裏的字段;AtomicMarkableReference:原子更新標記位的引用型。

可以原子更新一個布類型的標記位和引用型。構造方法是AtomicMarkableReferenceV initialRefboolean initialMark)。

4、原子更新字段類:如果需原子地更新某個裏的某個字段,就需要使用原子更新字段

AtomicIntegerFieldUpdate:原子更新整型的字段的更新器;AtomicLongFieldUpdate:原子更新整型字段的更新器;AutomicStampedReference:原子更新有版本號的引用

要想原子地更新字段類需要兩步:一、使用靜方法newUpdater()建一個更新器,並且需要置想要更新的類和屬性;二、更新的字段(屬性)必使用public volatile

九、Java中的併發工具類

 1、CountDownLatch:允許一個或者多個線程等待其他線程完成操作。

CountDownLatch可以實現join()的功能,並且比join()的功能更多。CountDownLatch的構造函數接收一個int類型的參數作爲計數器,如果你想等待N個點完成,就傳入N。當我們調用一次countDown()方法,N就減一,CountDownLatch的await方法會阻塞當前進程,直到N變成0。

2、CyclicBarrier(同步屏障):讓一組線程達到一個屏障時被阻塞,直到最後一個線程到達屏障時,屏障纔會開門,所有被屏障攔截的線程纔會繼續運行。

CyclicBarrier的默認構造方法CyclicBarrier(int parties),其參數表示屏障攔截的線程數量,每個線程調用await方法告訴CyclicBarrier我已經到達了屏障,然後阻塞當前線程。

CyclicBarrier還提供了了一個更高級的構造函數CyclicBarrier(int parties,Runnable barrierAction),用於在線程到達屏障時,優先執行barrierAction,方便處理更復雜的業務場景。

CyclicBarrier和CountDownLatch的區別:CountDownLatch的計數器只能使用一次,而CyclicBarrier的計數器可以使用reset()方法重置。

3、Semaphore:控制併發線程數

Semaphore的構造方法Semaphore(int permits)接收一個整型的數字,表示可用的許可證的數量。Semaphore(10)代表允許10個線程獲得許可證,也是就是最大併發數爲10。

用法:首先線程使用Semaphore的acquire()方法接收一個許可證,然後使用完成以後調用release()方法歸還許可證。還可以使用tryAcquire()方法嘗試獲取許可證。

4、Exchanger:線程間交換數據

Exchanger提供一個同步點,在這個同步點兩個線程可以互相交換數據。這兩個線程通過exchange方法交換數據,如果第一個線程先執行exchange()方法,它會一直等待第二個線程也執行exchange方法,當兩個線程都到達同步點,這個兩個線程就可以交換自己的數據,將本線程生產出來的數據傳遞給對方。

十、Java中的線程池

1、使用線程池的好處:1、降低資源消耗,通過重複利用已創建的線程降低線程創建和銷燬造成的消耗;2、提高響應速度,一旦有任務可以立馬調用線程池空閒的線程,不用再重新創建;3、提高線程的可管理性,線程池可以對線程進行統一化管理,降低資源的消耗,降低系統的穩定性。

2、線程池的工作流程:(1)當線程池接收到一個任務的時候,先判斷執行任務的線程數是否小於核心線程數,如果是,則新創建一個線程來執行該任務,如果不是,進入下一個流程;(2)判斷任務隊列是否滿了,如果不是,將該任務放在任務隊列中,等待覈心線程完成任務釋放,如果是,則進入下一個流程;(3)查看當前執行任務的線程是否到達最大線程數,如果沒有,則新創建一個線程來執行,如果等於最大線程數了,則根據飽和策略來針對飽和任務進行操作。

3、線程池的幾個參數:核心線程數,最大線程數(如果使用了無界隊列,該參數就無用了),線程活動保持時間(線程空閒時保持存活的時間),任務隊列,飽和策略(默認的是AbortPolicy,拋出異常)。

4、任務隊列

ArrayBlockingQueue隊列,FIFO,有界隊列;

LinkedBlockingQueue隊列,FIFO,有界隊列,吞吐量比LinkedBlockingQueue,是Executors.newFixedThreadPool()使用的隊列;

SynchronousQueue隊列,不存儲元素的隊列,一個元素的插入必須等另一個線程調用移出操作,否則插入一直處於阻塞狀態,效率高於LinkedBlockingQueue隊列,是Executors.newCachedThreadPool()使用的隊列。

PriorityBlockingQueue:一個具有優先級的無限阻塞隊列

5、飽和策略

AbortPolicy:拋出異常(默認策略);CallerRunsPolicy:只用調用者所在的線程來執行任務;DiscardOldestPolicy:丟棄隊列裏最近的一個任務,執行當前任務;DiscardPolicy:不處理,丟棄掉。

當然,也可以根據需求來實現RejectedExecutionHandler接口來自定義策略,例如記錄日誌或者持久化存儲不能處理的任務。

6、任務的提交

向線程池提交任務,有兩個方法execute()和submit()方法。

execute()方法用於提交不需要返回值的任務,因此不知道任務是否被成功執行。

submit()方法用於提交需要返回值的任務。線程池會返回一個future類型的對象,通過future對象可以判斷任務是否執行成功,並且可以通過future的get()方法來回去返回值,get()方法會阻塞當前線程直到任務完成。

7、關閉線程池

使用shutdown或者shutdownNow方法,都是遍歷工作線程,然後逐個調用線程的interrupt()中斷線程,所以無法響應中斷的任務可能永遠無法停止。

shutdown將線程池的狀態設置爲SHUTDOWN狀態,然後中斷所有沒有正在執行任務的線程。

shutdownNow將線程池的狀態設置爲STOP,然後嘗試停止所有正在執行或者暫停任務的線程,並返回等待執行任務列表。

因此,調用shutdown以後並不是所有的線程都關閉了,正在執行的會執行完才關閉,如果想無論是否執行都關閉,則用shutdownNow。

兩個方法調用任意一個,isShutdown方法都會返回true,只有當所有的任務都關閉了,isTerminated纔會返回true。

十一、Executor框架

1、Executor框架的結構

任務:被執行任務需要實現Runnable接口或者Callable接口

任務的執行:任務執行的核心接口Executor和繼承自Executor的ExecutorService接口。Executor框架有兩個實現類實現了ExecutorService,它們分別是ThreadPoolExecutor和ScheduledThreadPoolExecutor。

異步計算的結果:接口Future以及它的實現類FutureTask,有get()方法獲取任務執行的結果,有cancel()取消提交給線程池的的任務。

可以將實現Runnable的類通過工具類Executors.callable(Runnable task)或Executors.callable(Runnable task,Object resule)封裝成Callable對象

2、Executor架構的成員

ThreadPoolExecutor:SingleThrealExecutor、FixedThreadPool(適用於需要保證順序地執行各個任務,並且在多個時間點不會有多個線程)、CachedThreadPool(大小無界的線程池)

ScheduledThreadPoolExecutor(定期執行任務):ScheduledThreadPoolExecutor(包含多個線程)、SingleThreadScheduledExecutor(只包含一個線程)

注意:並不是調用了executor.sumit()就能返回FutureTask,然後通過get方法獲取結果,而是實現Callable接口的類才能返回值,實現Runnable接口的類,調用的如果不是帶有返回結果的sumbit方法,FutureTask.get()將返回null。同理,通過Executors工具類的callable方法將實現Runnable接口的類包裝成一個Callable,如果調用的方法不帶返回結果的,就算調用了submit方法,然後FutureTask.get()也是null。

3、ScheduledThreadPoolExecutor

3.1、任務的執行過程

3.1.1、當調用ScheduledThreadPoolExecutor的scheduleAtFixedRate()或者shceduleWithFixedDelay()方法時,會向ScheduledThreadPoolExecutor的DelayQueue隊列中添加一個實現了RunnableScheduledFuture接口的ScheduledFutureTask類。

3.1.2、線程池中的線程從DelayQueue中獲取ScheduledFutureTask,然後執行任務

3.1.3、任務執行完成,修改任務的time,即下次執行的時間,然後將該任務放回DelayQueu隊列中

ScheduledFutureTask有三個變量,time,sequenceNumber,period,三者都是long類型的,其中time代表任務執行的具體時間,sequenceNumber代表任務加入隊列的序號,period代表任務執行的間隔週期。

其實DelayQueue隊列封裝了PriorityQueue,PriorityQueue是一個基於優先級的隊列,其根據time大小進行排序,time小的排在前面,時間早的任務先執行,如果時間一致,則再根據sequenceNumber排序,sequenceNumber小的排在前面,也就是先提交的任務先被執行。

3.2、獲取任務

3.2.1、獲取Lock鎖;

3.2.2、獲取週期任務

如果PriorityQueue爲空,則將線程放到Condition中等待,若不爲空,則執行下一步;

如果PriorityQueue的頭元素的time大於當前時間,則將線程放到Condition中等待到time的時間,否則執行下一步;

獲取PriorityQueue的頭元素,如果不爲空,且其time不大於當前時間,則喚醒Conditon中等待的所有線程。

3.2.3、釋放Lock鎖

4、FutureTask

FutureTask即實現了Task接口,又實現了Runnable接口。因此可以直接調用FutueTask.run()方法啓動FutureTask。

當FutureTask未啓動或已處於啓動狀態,執行futureTask.get()將會阻塞調用線程,當FutureTask處於已完成狀態時,執行FutureTask.get()方法將導致調用線程立即返回結果或者直接拋出異常。

FutureTask.cancel()會使未執行的任務永遠不會執行,FutureTask.cancel(true)會中斷正在執行的任務,FutureTask.cancel(false)不會對正在執行的線程產生影響,會讓正在執行的任務執行完成。FutureTask.cancel()會會在FutureTask處於已完成時返回false。

 十二、JAVA內存模型(JMM)

1、java併發採用共享的內存模型,java線程間的通信總是隱式的進行。

2、由於堆內存和方法區是線程共享的,因此這些共享變量可能會產生內存可見性問題。局部變量、方法定義的參數、異常處理器參數都屬於線程私有的,不會有內存可見性問題,也不受內存模型的影響。

3、java內存模型控制線程之間的通信。java內存模型定義了線程和主內存之間的抽象關係:線程之間的共享變量存儲在主內存中,每個線程都有一個私有的本地內存,用來存儲該線程以讀/寫共享變量的副本。

4、指令重排

爲了提高效率,編譯器和處理器常常會對指令進行重排序。重排序分爲三種類型,編譯器優化的重排序、指令級並行的重排序、內存系統的重排序。其中第一個爲編譯器重排序,後兩個屬於處理器重排序。

對於編譯器,JMM的編譯器重排序規則會禁止特定類型的編譯器重排序,對於處理器重排序,JMM的處理器重排序規則會要求java編譯器在生成指令序列時,插入特定類型的內存屏障指令,通過內存屏障指令來禁止特定類型的處理器重排序。

JMM屬於語言級的內存模型,它確保不同的編譯器和不同的處理器平臺上,通過禁止特定類型的編譯器重排序和處理器重排序,爲程序員提供一致的內存可見性保證。

注:編譯器和處理器不會改變存在數據依賴關係的兩個操作的順序。這裏僅指單個處理器或者單個線程內的操作。

5、內存屏障分爲LoadLoad Barriers、StoreStore Barriers、LoadStore Barriers、StoreLoad Barriers。

其中StoreLoad Barriers是一個全能型的屏障,它同時具有其它三個屏障的效果。現在大多數處理器都支持該屏障,但是該屏障的開銷很昂貴,因爲當前處理器通常要把寫緩存區的全部數據都刷新到內存中。

6、happens-before

在JMM中,如果一個操作執行的結果對另一個操作可見,那麼這兩個操作之間必須存在happens-before關係。這兩個操作可以在同一個線程也可以在不同的線程中。

常見的happens-before:鎖的解鎖happens-before鎖的加鎖;一個對volatile的寫happens-before後續對這個volatile的讀。

注意:兩個操作具有happens-before並不意味着前一個操作必須要在後一個操作之前執行。happens-before是指前一個操作的結果對後一個可見,且前一個操作按順序排在第二個操作之前。

其實對於程序員來講,happens-before簡單易懂,它避免程序員爲了理解JMM提供的內存可見性保證而學習複雜的重排序規則以及規則的具體方法。其實一個happens-before規則對應於一個或多個編譯器或者處理器重排序規則,但是JMM已經幫我們把一些編譯器和處理器的重排進行了禁止。

7、as-if-serial:不管怎麼重排序,單線程的程序的執行結果不能被改變。

編譯器、runtime、處理器都必須遵守as-if-serial語義。爲了遵守該語義,編譯器和處理器不會對存在數據依賴關係的操作做重排序,因此給人的感覺是單線程的程序是按照程序順序執行的,但是有可能是發生了改變,但是結果一樣而已。

8、軟硬件開發者的共同目標:在不改變程序執行結果的前提下,儘可能提高並行度。

9、順序一致性模型

JMM對正確同步的多線程程序的內存一致性做出了保證:如果程序是正確同步的,程序的執行將具有順序一致性,即程序的執行結果與該程序在順序一致性內存模型中的執行結果一致。

順序一致性模型:只有一個單一的全局內存,這個內存通過左右擺動的開關可以連接到任意一個線程,同時每一個線程必須按照程序的順序來執行內存讀/寫操作。

10、volatile的內存語義

volatile寫:JMM將本地內存中的共享變量的值刷新到主內存。

volatile讀:JMM將本地內存中的設置爲無效,然後從主內存中讀取共享變量。

從內存語義的角度來說,volatile的讀-寫與鎖的釋放有相同的內存效果:volatile寫和鎖的釋放有相同的內存語義,volatile的讀和鎖的獲取有相同的語義。

線程A寫了一個volatile變量,隨後線程B讀取這個volatile變量,這個過程實質就是線程A通過主內存向線程B發送消息。

爲了保證volatile讀/寫內存語義的實現,JMM對於編譯器指令重排和處理器重排都做了限制,在編譯器方面制定了volatile重排序規則表,在處理器方面加入了內存屏障。

        其中對於編譯器,當第二個操作時volatile寫時,無論第一個是什麼操作,都不能重排;對於第一個操作時讀時,無論是第二個操作是什麼,都不能重排序。

        對於處理器來講,在每個volatile寫之前插入StoreStore屏障,在volatile寫之後插入StoreLoad屏障;在每個volatile讀之後插入一個LoadLoad屏障,在LoadLoad之後再插入一個LoadStore屏障。

11、鎖的內存語義

鎖時JMM會把該線對應的本地內存中的共享量刷新到主內存中。

鎖時JMM會把該線對應的本地內存置無效。從而使得被監視器保的臨界區代從主內存中取共享量。

鎖內存語義的實現:公平鎖和非公平鎖釋放時,都要寫一個volatile變量state。公平鎖獲取時,首先會去讀volatile變量。非公平鎖獲取時,首先使用CAS更新volatile,這個操作同時具有volatile讀和volatile寫的內存語義。

總結:鎖的獲取和釋放的內存語義的實現至少有以下兩種方式:利用volatile變量的讀-寫所具有的語義;利用CAS附帶的volatile讀和volatile寫的內存語義。

爲什麼說CAS同時具有volatile讀和volatile寫的內存語義?

從編譯器方面看,編譯器不會對volatile讀和讀之後、volatile寫和寫之前的任意內存操作重排序,而編譯器不能對CAS與CAS前面和後面的任意內存操作重排序,因此編譯器方面說明了。

再從處理器方面看,CAS的源碼是使用了lock前綴指令,處理器禁止該指令,與之前和之後的讀寫指令重排序;處理器把寫緩存區中的數據都刷新到內存中,具有volatile讀寫同樣的效果。因此處理器方面也說明了。

12、關於happens-before,JMM對程序員的保證是:如果A happens-before B,則A操作的結果對B可見,且A的執行順序在B之前,但這只是對程序員的保證。

對於編譯器和處理器而言:只要不改變程序執行的的結果,編譯器和處理器怎麼優化都行,A的執行順序不一定在B之前。

13、單純的雙重檢查出現的問題以及解決方案

public class DoubleCheckedLocking { 
    private static Instance instance; 
    public static Instance getInstance() { 
        if (instance == null) { 
            synchronized (DoubleCheckedLocking.class){
                if (instance == null) 
                    instance = new Instance(); 
            } 
        } 
    return instance; 
    } 
}

出現問題的地方: instance = new Instance(); 這個步驟是分配對象的內存空間,然後初始化對象,再講instance指向剛分配的內存地址。但是這個地方會發生指令重排,會先分配對象的內存空間,然後instance指向剛分配的地址,然後再初始化。因爲能保證單線程內結果不變,所以指令重排是允許的,但是對於多線程的併發,可能另一個線程訪問了第一個if(instance == null),發現instance不爲null,然後就走到renturn instance,訪問instance。但是這時A線程還未初始化該對象,則B將會訪問一個未初始化的對象。

解決方案:1、使用volatile,volatile會禁止在多線程的情況下該初始化的指令重排,這樣對象不爲null的時候一定初始化了。

2、使用靜態內部類,靜態內部類在被調用的時候會初始化,同時會初始化該靜態類的靜態資源。原因:在類的初始化期間,JVM會獲取一個鎖,保證多個線程同步對一個類的初始化。

類初始化的條件:

1、T是一個類,而且一個T類型的實例被創建。2、T是一個類,且T中聲明的一個靜態方法被調用。3、T中聲明的一個靜態字段被賦值。4、T中聲明一個靜態字段被使用,而且這個字段不是常量字段。5、T是一個頂級類,而且一個斷言語句嵌套在T內部被執行。

14、JMM總結

14.1、不同處理器的內存模型不能,爲了給程序員呈現一個一致的內存模型,JMM通過給不同的處理器插入不同的內存屏障來屏蔽不同處理器內存模型的差異。

14.2、各內存模型之間的關係(JMM、處理器內存模型、順序一致性內存模型)

JMM是語言級內存模型,處理區內存模型是硬件級的內存模型,順序一致性內存模型是一個理論參考模型。

處理器內存模型都比語言級的內存模型弱,處理器和語言級內存模型都比順序一致性內存模型弱。跟處理器模型一樣,越是追求性能的語言,內存模型就會被設計的越弱。

41.3、JMM的內存可見性保證

對於單線程程序,不會出現內存可見性問題,JMM保證其處理結果跟在順序一致性模型內的結果一致。

對於正確同步的多線程程序,也不會出現內存可見性問題。JMM通過限制編譯器和處理器的重排序來提供保證。

對於未同步或者未正確同步的多線程程序。JMM提供了最小的安全性保障:線程執行時讀取到的值,要麼是之前某個線程寫入的值,要麼是默認值。

十三、總結

1、線程並非使用的越多越好,在資源限定的情況下,使用過多的線程可能會更慢,因爲上下文切換會耗費資源。當一個線程拿到CPU時間片,然後運行一段時間以後,另一個線程獲得了CPU時間片,該線程的運行狀態就會保存下來,以便再次獲得CPU時間片的時候可以再加載這個狀態,這個線程從保存到再加載的過程就是一次上下文切換的過程。

2、爲了減少上下文切換帶來的資源浪費,可以用以下三個方法: 使用無鎖併發編程、使用CAS算法、使用最少線程、使用協程。無鎖併發編程:一堆任務,可以根據取模算法,將數據根據id分爲不同的線程,不用的線程執行自己負責的數據範圍。

3、volatile能保證可見性,無法保證原子性。何爲可見性:當一個線程修改了共享變量,另一個線程能讀到這個修改的值。

volatile怎麼保證數據的可見性的?多個處理器共享一個變量,會將變量在內存中的地址存儲到處理器自己的高速緩存區(L1L2L3),當給變量加上volatile時,一個處理器對變量進行修改時,會將修改的數據直接回寫到內存中,爲了保證多處理器的緩存一致,就會實現緩存一致性,每個處理器會通過嗅探總線上的數據來感知自己的數據是否過期了,當處理器發現自己緩存行對應的內存地址被修改了,就會將緩存行設置爲失效。當對這個數據進行再操作時,會重新從內存中把數據讀到處理器的緩存中。

4、處理器如何保證操作的原子性的?兩個方法:總線鎖、緩存鎖。但是總線鎖會將CPU與內存之間的通信鎖住,鎖定期間,其他處理器不能操作其他內存地址的數據,因此開銷比較大,一般都使用緩存鎖,不同的處理器使用的不一樣,根據情況而定。

總線鎖:處理器提供一個LOCK#信號,當一個處理器在總線上輸出次信號時,其他處理器的請求會被阻塞,那麼處理器會獨佔共享內存。

緩存鎖:指內存區域如果被緩存在理器的存行中,並且在Lock操作期定,那麼當它操作回寫到內存理器不在總線上聲言LOCK#信號,而是修改內部的內存地址,並允它的存一致性機制來保操作的原子性,因爲緩存一致性機制會阻止同修改由兩個以上理器存的內存區域數據,當其他處理器回寫已被鎖定的存行的數據,會使存行無效。

5、sychronized鎖

鎖在普通方法上,其實就是鎖了實例,如果鎖在Classs和靜態方法上,其實就是鎖對象

6、鎖的信息存儲在java對象頭中。如果對象是非數組類型的數據,那麼虛擬機就用3個字寬存儲對象頭。如果對象是數組類型的數據,那麼虛擬機就用2個字寬存儲對象頭。在32位虛擬機中,一個字寬=4字節(byte),也就是32(位)bit,在64位虛擬機中,一個字寬=8字節,也就是64位。

7、對象頭中的字寬,一個字寬用來存儲數組的長度,一個字寬用來存儲到對象類型數據的指針,另一個字寬就是常用的MarkWord,用來存儲對象的hashcode值、分代年齡、鎖的信息(鎖狀態,是否偏向鎖,鎖的標誌位)。

8、鎖的升級

鎖可以升級,但不能降級,一旦升級爲上一層級的鎖,無法再降級爲下一層級的鎖。

無鎖--》偏向鎖--》輕量級鎖--》重量級鎖

偏向鎖:當一個線程獲取偏向鎖的時候,會嘗試使用CAS在棧幀和mark word中記錄鎖偏向該線程的線程ID,如果記錄成功了,則代表獲取了鎖。偏向鎖不會自己釋放,只有當發生了競爭,纔會執行鎖釋放的過程。當一個線程持有了偏向鎖,另一個線程嘗試獲取該鎖的時候,它會先暫停持有偏向鎖的線程,然後查看持有偏向鎖的線程是否存活,若不存活,則將對象頭設置爲無鎖狀態。若持有線程依然存活,則要麼將該鎖重新偏向該線程,要麼將對象設置爲無鎖狀態,然後喚醒暫停的狀態。

注:偏向鎖的釋放需要等待全局安全點,也就是這個時間點上沒有正在執行的字節碼。

優:加鎖解鎖不需要額外的消耗;缺:鎖競爭的情況下,鎖的撤銷會帶來額外的消耗

輕量級鎖:當一個線程嘗試獲取輕量級鎖時,會將mark word複製一份到其棧幀的空間中,然後使用CAS嘗試將對象頭中的mark word替換成執行其棧幀的指針,若替換成功,則代表獲取了鎖,若失敗,則自旋繼續嘗試替換。當執行完同步方法時,釋放鎖的時候,其會嘗試將棧幀中的mark word替換到對象頭中,這時,如果其它的線程來競爭鎖,那麼該輕量級鎖會升級爲重量級鎖,並阻塞競爭線程。

優:競爭的線程不會阻塞,提高了程序的相應速度。缺:如果始終獲取不到鎖的線程,自旋會消耗CPU。

重量級鎖:阻塞鎖

優:線程競爭不使用自旋,不會消耗CPU。缺:線程阻塞,響應時間慢。

9、隊列同步器

鎖的實現是調用同步器提供的模板方法,然後來完成相關鎖的特性。

獨佔式的:當多個線程調用同步器的acquire方法試圖獲取同步器同步狀態,同一個時間只能有一個獲取成功個,獲取失敗的線程會被構建成一個節點加入到同步隊列中去,通過CAS(因爲失敗的可能很多)的方式加入隊列的尾部,自旋同時會阻塞該線程,然後同步器的首節點指向同步隊列中的首節點,尾結點指向同步隊列中的最後一個。當同步狀態釋放時,會把首節點中的線程喚醒,使其嘗試獲取同步狀態。當頭節點釋放同步狀態以後,會喚醒後繼節點,後繼節點的線程被喚醒以後檢查自己前驅節點是不是頭節點,如果是的話則嘗試獲取同步狀態。

        停止自旋或者移出隊列的條件是:前驅節點爲頭節點且成功獲取了同步狀態。

        線程被喚醒的情況:前驅節點爲頭節點,釋放同步狀態而喚醒後繼節點的線程;線程由於被中斷而被喚醒,這個時候會立刻返回並拋出InterruptedException。

        自旋:自我檢查,看自己是否滿足嘗試獲取同步狀態的條件,如果滿足,則嘗試獲取。滿足嘗試的條件:前驅節點爲頭節點。

共享式的:共享式跟獨佔式的區別是在同步狀態釋放的時候,因爲同步狀態的釋放操作可能來自多個線程,因此爲了保證安全一般都是通過循環和CAS保證的。

獨佔式超時獲取同步狀態:與獨佔式獲取同步狀態的區別在於當未獲取到同步狀態時,則會使當前線程等待n納秒,如果當前線程在n納秒沒有獲取到同步狀態,將會從等待邏輯中自動返回。

10、可重入鎖

Sychronized和ReentrantLock,ReentrantReadWriteLock都是可重入的,其都實現了隊列同步器,當獲取到鎖的線程再次獲取鎖的時候,同步狀態加一,返回true,當釋放鎖的時候,同步狀態減一,當什麼時候同步狀態減到0,則代表完全釋放才能返回true,否則之前都是返回false。

公平鎖與非公平鎖的獲取:不是通過CAS設置同步狀態成功即可,而是加了一個判斷條件,即同步隊列中當前節點是否有前驅節點的判斷,若有則代表有線程比當前線程更早的請求獲取鎖,因此需要等待前驅節點獲取並釋放鎖以後才能繼續獲取鎖。

11、讀寫鎖

用一個整型變量維護多個讀線程和一個寫線程的狀態,低16位表示寫,高16位表示讀。

鎖的降級:當前線程先獲取寫鎖,然後獲取讀鎖,然後再釋放寫鎖的過程。

不能有鎖的升級,即當前線程先獲取讀鎖,然後再獲取寫鎖,然後釋放讀鎖。因爲保證數據的可見性,若多個線程獲取了讀鎖,該線程進行了鎖的升級,修改了變量的內容,其它線程無法感知。

12、Condition

Condition對象是由Lock對象獲取出來的,通過Lock.newConditon()獲取。只有獲取了鎖,才能調用conditon的await等方法。

ConditionObject是同步器(AQS)的內部類。ConditionObject有一個等待隊列來實現其等待通知的功能,跟同步器的同步隊列一樣,一旦調用conditon的await方法以後,線程就會釋放鎖,將當前線程重新構建成一個新節點加入等待隊列的尾結點,調用condition的signal方法,將會將等待隊列中的首節點先移動到同步隊列的尾結點,然後調用LockSupport的方法將該節點中的線程喚醒,當喚醒後再次獲取同步狀態成功以後,纔會從先前調用的await()方法中返回true。

在Object的監視器模型上,一個對象擁有一個同步隊列和一個等待隊列。而同步器則擁有一個同步隊列和多個等待隊列,因爲它本身有一個同步隊列,它的內部類ConditinObject有等待隊列,可以創建多個,因此其有一個同步隊列,對個等待隊列。

13、CountDownLatch和CyclicBarrier(同步屏障)、Semaphore

兩者都是讓一個或者多個線程等待其它線程完成操作,都是在初始化的時候賦數字,然後每次調用一次計數減一,然後等待數字減爲0以後才能繼續走。都是使用await()阻塞當前線程。

不同之處:CountDownLatch的計數只能用一次,不可重置,而CyclicBarrier的計數可以使用以後再重置。

使用方式不同:countDownLatch.countDown();調用一定次數以後調用它的await()方法,而CyclicBarrier是直接調用await()方法一定的次數。

Semaphore:控制併發量。就算線程是100個,如果我使用Semaphore初始化的時候就初始化10個,最大併發也就是10。

14、JMM是用來描述多線程和內存之間通信的問題,而jvm解決的是對象內存自動管理的問題。

15、8個原子操作:加鎖(lock)、解鎖(unlock)、讀取(read)、載入(load)、使用(user)、賦值(assign)、存儲(store)、寫入(write)

16、緩存鎖定,若出現兩個同時鎖定,怎麼辦,總線會裁決。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章