互聯網架構多線程併發編程高級教程(下)

基礎篇幅:
線程基礎知識、併發安全性、JDK鎖相關知識、線程間的通信機制、
JDK提供的原子類、併發容器、線程池相關知識點
 
高級篇幅:
ReentrantLock源碼分析、對比二者源碼,更加深刻理解讀寫鎖,JAVA內存模型、先行發生原則、指令重排序java

 
環境說明:
    idea、java八、maven
 
 
第四章--鎖mysql

    01 鎖的分類程序員

        自旋鎖: 線程狀態及上下文切換消耗系統資源,當訪問共享資源的時間短,頻繁上下文切換不值得。jvm實現,使線程在沒得到鎖的時候,不被掛起,轉而執行空循環,循環幾回以後,若是還沒能得到鎖,則被掛起sql

        阻塞鎖:阻塞鎖改變了線程的運行狀態,讓線程進入阻塞狀態進行等待,當得到相應的信號(喚醒或者時間)時,才能夠進入線程的準備就緒狀態,轉爲就緒狀態的全部線程,經過競爭,進入運行狀態數據庫

        重入鎖:支持線程再次進入的鎖,就跟咱們有房間鑰匙,能夠屢次進入房間相似編程

        讀寫鎖: 兩把鎖,讀鎖跟寫鎖,寫寫互斥、讀寫互斥、讀讀共享api

        互斥鎖: 上廁所,進門以後就把門關了,不讓其餘人進來數組

        悲觀鎖: 老是假設最壞的狀況,每次去拿數據的時候都認爲別人會修改,因此每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖緩存

        樂觀鎖:每次去拿數據的時候都認爲別人不會修改,因此不會上鎖,可是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可使用版本號等機制。安全

        公平鎖:你們都老老實實排隊,對你們而言都很公平

        非公平鎖:一部分人排着隊,可是新來的可能插隊

        偏向鎖:偏向鎖使用了一種等到競爭出現才釋放鎖的機制,因此當其餘線程嘗試競爭偏向鎖時,持有偏向鎖的線程纔會釋放鎖

        獨佔鎖:獨佔鎖模式下,每次只能有一個線程能持有鎖

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


    02 深刻理解Lock接口

        Lock的使用
        lock與synchronized的區別
            lock
                獲取鎖與釋放鎖的過程,都須要程序員手動的控制
                Lock用的是樂觀鎖方式。所謂樂觀鎖就是,每次不加鎖而是假設沒有衝突而去完成某項操做,若是由於衝突失敗就重試,直到成功爲止。樂觀鎖實現的機制就 是CAS操做
            synchronized託管給jvm執行
                原始採用的是CPU悲觀鎖機制,即線程得到的是獨佔鎖。獨佔鎖意味着其餘線程只能依靠阻塞來等待線程釋放鎖。

        實現了lock接口的鎖
        各個方法的簡介


    03 實現屬於本身的鎖

        實現lock接口
        使用wait notify
        具體見視頻


    04 AbstractQueuedSynchronizer淺析
 

        AbstractQueuedSynchronizer -- 爲實現依賴於先進先出 (FIFO) 等待隊列的阻塞鎖和相關同步器(信號量、事件,等等)提供一個框架。
        此類的設計目標是成爲依靠單個原子 int 值來表示狀態的大多數同步器的一個有用基礎。
        子類必須定義更改此狀態的受保護方法,並定義哪一種狀態對於此對象意味着被獲取或被釋放。
        假定這些條件以後,此類中的其餘方法就能夠實現全部排隊和阻塞機制。子類能夠維護其餘狀態字段,但只是爲了得到同步而只追蹤使用 getState()、setState(int) 和 compareAndSetState(int, int) 方法來操做以原子方式更新的 int 值。
        應該將子類定義爲非公共內部幫助器類,可用它們來實現其封閉類的同步屬性。類 AbstractQueuedSynchronizer 沒有實現任何同步接口。而是定義了諸如 acquireInterruptibly(int) 之類的一些方法,在適當的時候能夠經過具體的鎖和相關同步器來調用它們,以實現其公共方法。

        此類支持默認的獨佔 模式和共享 模式之一,或者兩者都支持。處於獨佔模式下時,其餘線程試圖獲取該鎖將沒法取得成功。在共享模式下,多個線程獲取某個鎖可能(但不是必定)會得到成功。此類並不「瞭解」這些不一樣,除了機械地意識到當在共享模式下成功獲取某一鎖時,下一個等待線程(若是存在)也必須肯定本身是否能夠成功獲取該鎖。處於不一樣模式下的等待線程能夠共享相同的 FIFO 隊列。一般,實現子類只支持其中一種模式,但兩種模式均可以在(例如)ReadWriteLock 中發揮做用。只支持獨佔模式或者只支持共享模式的子類沒必要定義支持未使用模式的方法。

        此類經過支持獨佔模式的子類定義了一個嵌套的 AbstractQueuedSynchronizer.ConditionObject 類,能夠將這個類用做 Condition 實現。isHeldExclusively() 方法將報告同步對於當前線程是不是獨佔的;使用當前 getState() 值調用 release(int) 方法則能夠徹底釋放此對象;若是給定保存的狀態值,那麼 acquire(int) 方法能夠將此對象最終恢復爲它之前獲取的狀態。沒有別的 AbstractQueuedSynchronizer 方法建立這樣的條件,所以,若是沒法知足此約束,則不要使用它。AbstractQueuedSynchronizer.ConditionObject 的行爲固然取決於其同步器實現的語義。

        此類爲內部隊列提供了檢查、檢測和監視方法,還爲 condition 對象提供了相似方法。能夠根據須要使用用於其同步機制的 AbstractQueuedSynchronizer 將這些方法導出到類中。

        此類的序列化只存儲維護狀態的基礎原子整數,所以已序列化的對象擁有空的線程隊列。須要可序列化的典型子類將定義一個 readObject 方法,該方法在反序列化時將此對象恢復到某個已知初始狀態。        

tryAcquire(int)
        tryRelease(int)
        tryAcquireShared(int)
        tryReleaseShared(int)
        isHeldExclusively()
            Acquire:
             while (!tryAcquire(arg)) {
                    enqueue thread if it is not already queued;
                    possibly block current thread;
                 }

            Release:
                   if ((arg))
                        unblock the first queued thread;

    05 深刻剖析ReentrantLock源碼之非公平鎖的實現

        如何閱讀源碼?
            一段簡單的代碼
            看構造
            看類之間的關係,造成關係圖
            看使用到的方法,並逐步理解,邊看代碼邊看註釋
            debug


    06 深刻剖析ReentrantLock源碼之公平鎖的實現
    
        公平鎖與非公平鎖的區別
            公平鎖:顧名思義--公平,你們老老實實排隊
            非公平鎖:只要有機會,就先嚐試搶佔資源
            公平鎖與非公平鎖其實有點像在公廁上廁所。公平鎖遵照排隊的規則,只要前面有人在排隊,那麼剛進來的就老老實實排隊。而非公平鎖就有點流氓,只要當前茅坑沒人,它就佔了那個茅坑,無論後面的人排了多久。

        源碼解析
            詳見視頻

        非公平鎖的弊端
            可能致使後面排隊等待的線程等不到相應的cpu資源,從而引發線程飢餓

    07 掌控線程執行順序之多線程debug

        詳見視頻


    08 讀寫鎖特性及ReentrantReadWriteLock的使用

        特性:寫寫互斥、讀寫互斥、讀讀共享
        鎖降級:寫線程獲取寫入鎖後能夠獲取讀取鎖,而後釋放寫入鎖,這樣就從寫入鎖變成了讀取鎖,從而實現鎖降級的特性。


    09 源碼探祕之AQS如何用單一int值表示讀寫兩種狀態

        int 是32位,將其拆分紅兩個無符號short
        高位表示讀鎖          低位表示寫鎖
        0000000000000000   0000000000000000

        兩種鎖的最大次數均爲65535也便是2的16次方減去1

        讀鎖: 每次都從當前的狀態加上65536
        0000000000000000   0000000000000000
        ‭0000000000000001   0000000000000000‬
        -----------------------------------
        0000000000000001   0000000000000000‬
        0000000000000001   0000000000000000‬
        -----------------------------------
        0000000000000010   0000000000000000‬

        獲取讀鎖個數,將state整個無符號右移16位就可得出讀鎖的個數
                           0000000000000001 

        寫鎖:每次都直接加1
        0000000000000000   0000000000000000
        0000000000000000   0000000000000001
        -----------------------------------
        0000000000000000   0000000000000001

        獲取寫鎖的個數
        0000000000000000   0000000000000001
        ‭0000000000000000   1111111111111111‬    
        -----------------------------------    
        0000000000000000   0000000000000001


    10 深刻剖析ReentrantReadWriteLock之讀鎖源碼實現

        詳見視頻

    11 深刻剖析ReentrantReadWriteLock之寫鎖源碼實現

        詳見視頻
        0000000000000000   0000000000000001
        ‭0000000000000000   1111111111111111‬
        -----------------------------------    
        0000000000000000   0000000000000001

    12 鎖降級詳解
 

        鎖降級:寫線程獲取寫入鎖後能夠獲取讀取鎖,而後釋放寫入鎖,這樣就從寫入鎖變成了讀取鎖,從而實現鎖降級的特性。

        注意點:鎖降級以後,寫鎖並不會直接降級成讀鎖,不會隨着讀鎖的釋放而釋放,所以須要顯式地釋放寫鎖

        是否有鎖升級?
            在ReentrantReadWriteLock裏面,不存在鎖升級這一說法

        鎖降級的應用場景
            用於對數據比較敏感,須要在對數據修改以後,獲取到修改後的值,並進行接下來的其餘操做


    13 StampedLock原理及使用

        1.8以前,鎖已經那麼多了,爲何還要有StampedLock?
            通常應用,都是讀多寫少,ReentrantReadWriteLock 因讀寫互斥,故讀時阻塞寫,於是性能上上不去。可能會使寫線程飢餓

        StampedLock的特色
            全部獲取鎖的方法,都返回一個郵戳(Stamp),Stamp爲0表示獲取失敗,其他都表示成功;
            全部釋放鎖的方法,都須要一個郵戳(Stamp),這個Stamp必須是和成功獲取鎖時獲得的Stamp一致;
            StampedLock是不可重入的;(若是一個線程已經持有了寫鎖,再去獲取寫鎖的話就會形成死鎖)    
            支持鎖升級跟鎖降級
            能夠樂觀讀也能夠悲觀讀
            使用有限次自旋,增長鎖得到的概率,避免上下文切換帶來的開銷
            樂觀讀不阻塞寫操做,悲觀讀,阻塞寫得操做

        StampedLock的優勢
            相比於ReentrantReadWriteLock,吞吐量大幅提高

        StampedLock的缺點
            api相對複雜,容易用錯
            內部實現相比於ReentrantReadWriteLock複雜得多
            
        StampedLock的原理
            每次獲取鎖的時候,都會返回一個郵戳(stamp),至關於mysql裏的version字段
            釋放鎖的時候,再根據以前的得到的郵戳,去進行鎖釋放

        使用stampedLock注意點
            若是使用樂觀讀,必定要判斷返回的郵戳是不是一開始得到到的,若是不是,要去獲取悲觀讀鎖,再次去讀取

 

第五章--線程間的通訊
 

    1 wait、notify、notifyAll
        什麼時候使用
            在多線程環境下,有時候一個線程的執行,依賴於另一個線程的某種狀態的改變,這個時候,咱們就可使用wait與notify或者notifyAll
 
        wait跟sleep的區別
            wait會釋放持有的鎖,而sleep不會,sleep只是讓線程在指定的時間內,不去搶佔cpu的資源

        注意點
            wait notify必須放在同步代碼塊中, 且必須擁有當前對象的鎖,即不能取得A對象的鎖,而調用B對象的wait
            哪一個對象wait,就得調哪一個對象的notify

        notify跟notifyAll的區別
             nofity隨機喚醒一個等待的線程
             notifyAll喚醒全部在該對象上等待的線程


     2 等待通知經典模型之生產者消費者

         生產者消費者模型通常包括:生產者、消費者、中間商
         詳見視頻


     3 使用管道流進行通訊

         之內存爲媒介,用於線程之間的數據傳輸。
         主要有面向字節:【PipedOutputStream、PipedInputStream】、面向字符【PipedReader、PipedWriter】


     4 Thread.join通訊及其源碼淺析

         使用場景:線程A執行到一半,須要一個數據,這個數據須要線程B去執行修改,只有B修改完成以後,A才能繼續操做
         線程A的run方法裏面,調用線程B的join方法,這個時候,線程A會等待線程B運行完成以後,再接着運行


     5 ThreadLocal的使用

         線程變量,是一個以ThreadLocal對象爲鍵、任意對象爲值的存儲結構。爲每一個線程單獨存放一份變量副本,也就是說一個線程能夠根據一個ThreadLocal對象查詢到綁定在這個線程上的一個值。
         只要線程處於活動狀態而且ThreadLocal實例可訪問,那麼每一個線程都擁有對其本地線程副本的隱式引用變量一個線程消失後,它的全部副本線程局部實例受垃圾回收(除非其餘存在對這些副本的引用)

        通常用的比較多的是
        一、ThreadLocal.get: 獲取ThreadLocal中當前線程共享變量的值。
        二、ThreadLocal.set: 設置ThreadLocal中當前線程共享變量的值。
        三、ThreadLocal.remove: 移除ThreadLocal中當前線程共享變量的值。
        四、ThreadLocal.initialValue: ThreadLocal沒有被當前線程賦值時或當前線程剛調用remove方法後調用get方法,返回此方法值。


    6 Condition的使用

        能夠在一個鎖裏面,存在多種等待條件
        主要的方法
            await
            signal
            signalAll

 

第六章--原子類

    
    1 什麼是原子類

        一度認爲原子是不可分割的最小單位,故原子類能夠認爲其操做都是不可分割

        爲何要有原子類?

            對多線程訪問同一個變量,咱們須要加鎖,而鎖是比較消耗性能的,JDk1.5以後,
            新增的原子操做類提供了一種用法簡單、性能高效、線程安全地更新一個變量的方式,
            這些類一樣位於JUC包下的atomic包下,發展到JDk1.8,該包下共有17個類,
            囊括了原子更新基本類型、原子更新數組、原子更新屬性、原子更新引用

        1.8新增的原子類
            DoubleAccumulator、DoubleAdder、LongAccumulator、LongAdder、Striped64


    2 原子更新基本類型

        發展至JDk1.8,基本類型原子類有如下幾個:
            AtomicBoolean、AtomicInteger、AtomicLong、DoubleAccumulator、DoubleAdder、
            LongAccumulator、LongAdder
        大體能夠歸爲3類
            AtomicBoolean、AtomicInteger、AtomicLong  元老級的原子更新,方法幾乎如出一轍
            DoubleAdder、LongAdder  對Double、Long的原子更新性能進行優化提高
            DoubleAccumulator、LongAccumulator  支持自定義運算


    3 原子更新數組類型

        AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray


    4 原子地更新屬性
 

        原子地更新某個類裏的某個字段時,就須要使用原子更新字段類,Atomic包提供瞭如下4個類進行原子字段更新
        AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicStampedReference、AtomicReferenceFieldUpdater

        使用上述類的時候,必須遵循如下原則
        字段必須是volatile類型的,在線程之間共享變量時保證當即可見
        字段的描述類型是與調用者與操做對象字段的關係一致。
        也就是說調用者可以直接操做對象字段,那麼就能夠反射進行原子操做。
        對於父類的字段,子類是不能直接操做的,儘管子類能夠訪問父類的字段。
        只能是實例變量,不能是類變量,也就是說不能加static關鍵字。
        只能是可修改變量,不能使final變量,由於final的語義就是不可修改。
        對於AtomicIntegerFieldUpdater和AtomicLongFieldUpdater只能修改int/long類型的字段,不能修改其包裝類型(Integer/Long)。
        若是要修改包裝類型就須要使用AtomicReferenceFieldUpdater。


    5 原子更新引用

        AtomicReference:用於對引用的原子更新
        AtomicMarkableReference:帶版本戳的原子引用類型,版本戳爲boolean類型。
        AtomicStampedReference:帶版本戳的原子引用類型,版本戳爲int類型。


第七章--容器

    1 同步容器與併發容器
 

        同步容器
            Vector、HashTable -- JDK提供的同步容器類
            Collections.synchronizedXXX 本質是對相應的容器進行包裝

            同步容器類的缺點
                在單獨使用裏面的方法的時候,能夠保證線程安全,可是,複合操做須要額外加鎖來保證線程安全
            使用Iterator迭代容器或使用使用for-each遍歷容器,在迭代過程當中修改容器會拋出ConcurrentModificationException異常。想要避免出現ConcurrentModificationException,就必須在迭代過程持有容器的鎖。可是若容器較大,則迭代的時間也會較長。那麼須要訪問該容器的其餘線程將會長時間等待。從而會極大下降性能。
            若不但願在迭代期間對容器加鎖,可使用"克隆"容器的方式。使用線程封閉,因爲其餘線程不會對容器進行修改,能夠避免ConcurrentModificationException。可是在建立副本的時候,存在較大性能開銷。
            toString,hashCode,equalse,containsAll,removeAll,retainAll等方法都會隱式的Iterate,也便可能拋出ConcurrentModificationException。

        併發容器
            CopyOnWrite、Concurrent、BlockingQueue
            根據具體場景進行設計,儘可能避免使用鎖,提升容器的併發訪問性。
            ConcurrentBlockingQueue:基於queue實現的FIFO的隊列。隊列爲空,取操做會被阻塞
            ConcurrentLinkedQueue,隊列爲空,取得時候就直接返回空
            

LinkedBlockingQueue的使用及其源碼探祕
            在併發編程中,LinkedBlockingQueue使用的很是頻繁。因其能夠做爲生產者消費者的中間商

            add  實際上調用的是offer,區別是在隊列滿的時候,add會報異常
            offer  對列若是滿了,直接入隊失敗
            put("111"); 在隊列滿的時候,會進入阻塞的狀態

            
            remove(); 直接調用poll,惟一的區別即便remove會拋出異常,而poll在隊列爲空的時候直接返回null
            poll(); 在隊列爲空的時候直接返回null
            take(); 在隊列爲空的時候,會進入等待的狀態

 

第八章--併發工具類

    CountDownLatch
        await(),進入等待的狀態
        countDown(),計數器減一
        應用場景:啓動三個線程計算,須要對結果進行累加。

    CyclicBarrier--柵欄
        容許一組線程相互等待達到一個公共的障礙點,以後再繼續執行
        
        跟countDownLatch的區別
            CountDownLatch通常用於某個線程等待若干個其餘線程執行完任務以後,它才執行;不可重複使用
            CyclicBarrier通常用於一組線程互相等待至某個狀態,而後這一組線程再同時執行;可重用的

    Semaphore--信號量
        控制併發數量
        使用場景:接口限流

    Exchanger
        用於交換數據

        它提供一個同步點,在這個同步點兩個線程能夠交換彼此的數據。這兩個線程經過exchange方法交換數據, 若是第一個線程先執行exchange方法,它會一直等待第二個線程也執行exchange,當兩個線程都到達同步點時,這兩個線程就能夠交換數據,將本線程生產出來的數據傳遞給對方。所以使用Exchanger的重點是成對的線程使用exchange()方法,當有一對線程達到了同步點,就會進行交換數據。所以該工具類的線程對象是【成對】的。

第九章—線程池及Executor框架

    1 爲何要使用線程池?

        諸如 Web 服務器、數據庫服務器、文件服務器或郵件服務器之類的許多服務器應用程序都面向處理來自某些遠程來源的大量短小的任務。請求以某種方式到達服務器,這種方式多是經過網絡協議(例如 HTTP、FTP )、經過 JMS隊列或者可能經過輪詢數據庫。    無論請求如何到達,服務器應用程序中常常出現的狀況是:單個任務處理的時間很短而請求的數目倒是巨大的。每當一個請求到達就建立一個新線程,而後在新線程中爲請求服務,可是頻繁的建立線程,銷燬線程所帶來的系統開銷實際上是很是大的。

        線程池爲線程生命週期開銷問題和資源不足問題提供瞭解決方案。經過對多個任務重用線程,線程建立的開銷被分攤到了多個任務上。其好處是,由於在請求到達時線程已經存在,因此無心中也消除了線程建立所帶來的延遲。這樣,就能夠當即爲請求服務,使應用程序響應更快。並且,經過適當地調整線程池中的線程數目,也就是當請求的數目超過某個閾值時,就強制其它任何新到的請求一直等待,直到得到一個線程來處理爲止,從而能夠防止資源不足。

        風險與機遇
            用線程池構建的應用程序容易遭受任何其它多線程應用程序容易遭受的全部併發風險,
            諸如同步錯誤和死鎖,它還容易遭受特定於線程池的少數其它風險,諸如與池有關的死鎖、資源不足和線程泄漏。


    2 建立線程池及其使用

        詳見視頻


    3 Future與Callable、FutureTask

        Callable與Runable功能類似,Callable的call有返回值,能夠返回給客戶端,而Runable沒有返回值,通常狀況下,Callable與FutureTask一塊兒使用,或者經過線程池的submit方法返回相應的Future

        Future就是對於具體的Runnable或者Callable任務的執行結果進行取消、查詢是否完成、獲取結果、設置結果操做。get方法會阻塞,直到任務返回結果

        FutureTask則是一個RunnableFuture,而RunnableFuture實現了Runnbale又實現了Futrue這兩個接口


    4 線程池的核心組成部分及其運行機制

        corePoolSize:核心線程池大小 cSize
        maximumPoolSize:線程池最大容量  mSize
        keepAliveTime:當線程數量大於核心時,多餘的空閒線程在終止以前等待新任務的最大時間。
        unit:時間單位
        workQueue:工做隊列 nWorks
        ThreadFactory:線程工廠
        handler:拒絕策略

        運行機制
            經過new建立線程池時,除非調用prestartAllCoreThreads方法初始化核心線程,不然此時線程池中有0個線程,即便工做隊列中存在多個任務,一樣不會執行

            任務數X
            x <= cSize  只啓動x個線程

            x >= cSize && x < nWorks + cSize  會啓動 <= cSize 個線程 其餘的任務就放到工做隊列裏

            x > cSize && x > nWorks + cSize
                x-(nWorks) <= mSize  會啓動x-(nWorks)個線程
                x-(nWorks) > mSize   會啓動mSize個線程來執行任務,其他的執行相應的拒絕策略


    5 線程池拒絕策略

        AbortPolicy:該策略直接拋出異常,阻止系統正常工做
        CallerRunsPolicy:只要線程池沒有關閉,該策略直接在調用者線程中,執行當前被丟棄的任務(叫老闆幫你幹活)
        DiscardPolicy:直接啥事都不幹,直接把任務丟棄
        DiscardOldestPolicy:丟棄最老的一個請求(任務隊列裏面的第一個),再嘗試提交任務


    6 Executor框架

        經過相應的方法,能建立出6種線程池
        ExecutorService executorService = Executors.newCachedThreadPool();
        ExecutorService executorService1 = Executors.newFixedThreadPool(2);
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
        ExecutorService executorService2 = Executors.newWorkStealingPool();
        ExecutorService executorService3 = Executors.newSingleThreadExecutor();
        ScheduledExecutorService scheduledExecutorService1 = Executors.newSingleThreadScheduledExecutor();

        上面的方法最終都建立了ThreadPoolExecutor
        newCachedThreadPool:建立一個能夠根據須要建立新線程的線程池,若是有空閒線程,優先使用空閒的線程
        newFixedThreadPool:建立一個固定大小的線程池,在任什麼時候候,最多隻有N個線程在處理任務
        newScheduledThreadPool:能延遲執行、定時執行的線程池
        newWorkStealingPool:工做竊取,使用多個隊列來減小競爭
        newSingleThreadExecutor:單一線程的線程次,只會使用惟一一個線程來執行任務,即便提交再多的任務,也都是會放到等待隊列裏進行等待
        newSingleThreadScheduledExecutor:單線程能延遲執行、定時執行的線程池


    7 線程池的使用建議

        儘可能避免使用Executor框架建立線程池
            newFixedThreadPool  newSingleThreadExecutor
            容許的請求隊列長度爲 Integer.MAX_VALUE,可能會堆積大量的請求,從而致使 OOM。

            newCachedThreadPool newScheduledThreadPool
            容許的建立線程數量爲 Integer.MAX_VALUE,可能會建立大量的線程,從而致使 OOM

            爲何第二個例子,在限定了堆的內存以後,還會把整個電腦的內存撐爆
                建立線程時用的內存並非咱們制定jvm堆內存,而是系統的剩餘內存。(電腦內存-系統其它程序佔用的內存-已預留的jvm內存)

        建立線程池時,核心線程數不要過大

        相應的邏輯,發生異常時要處理

        submit 若是發生異常,不會當即拋出,而是在get的時候,再拋出異常
        execute 直接拋出異常


第十章--jvm與併發
 

    1 jvm內存模型

        硬件內存模型
            處理器--》高速緩存--》緩存一致性協議--》主存
        java內存模型
            線程《--》工做內存《--》save和load 《---》主存
        
        java內存間的交互操做
            (1)lock(鎖定):做用於主內存的變量,把一個變量標記爲一條線程獨佔狀態
            (2)unlock(解鎖):做用於主內存的變量,把一個處於鎖定狀態的變量釋放出來,釋放後的變量才能夠被其餘線程鎖定
            (3)read(讀取):做用於主內存的變量,把一個變量值從主內存傳輸到線程的工做內存中,以便隨後的load動做使用
            (4)load(載入):做用於工做內存的變量,它把read操做從主內存中獲得的變量值放入工做內存的變量副本中
            (5)use(使用):做用於工做內存的變量,把工做內存中的一個變量值傳遞給執行引擎
            (6)assign(賦值):做用於工做內存的變量,它把一個從執行引擎接收到的值賦給工做內存的變量
            (7)store(存儲):做用於工做內存的變量,把工做內存中的一個變量的值傳送到主內存中,以便隨後的write的操做
            (8)write(寫入):做用於主內存的變量,它把store操做從工做內存中的一個變量的值傳送到主內存的變量中

            上面8中操做必須知足如下規則
                一、不容許read和load、store和write操做之一單獨出現,即不容許一個變量從主內存讀取了但工做內存不接受,或者從工做內存發起回寫了但主內存不接受的狀況出現。
                二、不容許一個線程丟棄它的最近的assign操做,即變量在工做內存中改變了以後必須把該變化同步回主內存。
                三、不容許一個線程無緣由地(沒有發生過任何assign操做)把數據從線程的工做內存同步回主內存。
                四、一個新的變量只能在主內存中「誕生」,不容許在工做內存中直接使用一個未被初始化(load或assign)的變量,換句話說,就是對一個變量實施use、store操做以前,必須先執行過了assign和load操做。
                五、一個變量在同一時刻只容許一條線程對其進行lock操做,但lock操做能夠被同一條線程重複執行屢次,屢次執行lock後,只有執行相同次數的unlock操做,變量纔會被解鎖。
                六、若是對一個變量執行lock操做,那將會清空工做內存中此變量的值,在執行引擎使用這個變量前,須要從新執行load或assign操做初始化變量的值。
                七、若是一個變量事先沒有被lock操做鎖定,那就不容許對它執行unlock操做,也不容許去unlock一個被其餘線程鎖定住的變量。
                八、對一個變量執行unlock操做以前,必須先把此變量同步回主內存中(執行store、write操做)。


    2 先行發生原則 happens-before

        判斷數據是有有競爭、線程是否安全的主要依據
        1. 程序次序規則:同一個線程內,按照代碼出現的順序,前面的代碼先行於後面的代碼,準確的說是控制流順序,由於要考慮到分支和循環結構。

        2. 管程鎖定規則:一個unlock操做先行發生於後面(時間上)對同一個鎖的lock操做。

        3. volatile變量規則:對一個volatile變量的寫操做先行發生於後面(時間上)對這個變量的讀操做。

        4. 線程啓動規則:Thread的start( )方法先行發生於這個線程的每個操做。

        5. 線程終止規則:線程的全部操做都先行於此線程的終止檢測。能夠經過Thread.join( )方法結束、Thread.isAlive( )的返回值等手段檢測線程的終止。 

        6. 線程中斷規則:對線程interrupt( )方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生,能夠經過Thread.interrupt( )方法檢測線程是否中斷

        7. 對象終結規則:一個對象的初始化完成先行於發生它的finalize()方法的開始。

        8. 傳遞性:若是操做A先行於操做B,操做B先行於操做C,那麼操做A先行於操做C。

        爲何要有該原則?
            不管jvm或者cpu,都但願程序運行的更快。若是兩個操做不在上面羅列出來的規則裏面,那麼久能夠對他們進行任意的重排序。

            時間前後順序與先行發生的順序之間基本沒有太大的關係。


    3 指令重排序

        什麼是指令重排序?
            重排序是指編譯器和處理器爲了優化程序性能而對指令序列進行從新排序的一種手段。
        數據依賴性
            編譯器和處理器在重排序時,會遵照數據依賴性,編譯器和處理器不會改變存在數據依賴關係的兩個操做的執行順序。(僅針對單個處理器中執行的指令序列和單個線程中執行的操做,不一樣處理器之間和不一樣線程之間的數據依賴性不被編譯器和處理器考慮。)

            兩操做訪問同一個變量,其兩個操做中有至少一個寫操做,此時就存在依賴性
            寫後讀 a=0 b=a
            讀後寫 a=b b=1
            寫後寫 a=1 a=2

            a=1,b=1
            寫後讀 a=0 b=a  正確b=0 錯誤b=1
        as-if-serial原則
            無論怎麼重排序(編譯器和處理器爲了提升並行度),(單線程)程序的執行結果不能被改變。

        x=0,y=1
        x=1, y=0
        x=1, y=1
        x=0, y=0

 

第十一章—實戰
 

    1 數據同步接口--需求分析

        業務場景:
            通常系統,多數會與第三方系統的數據打交道,而第三方的生產庫,並不容許咱們直接操做。在企業裏面,通常都是經過中間表進行同步,即第三方系統將生產數據放入一張與其生產環境隔離的另外一個獨立的庫中的獨立的表,再根據接口協議,增長相應的字段。而我方須要讀取該中間表中的數據,並對數據進行同步操做。此時就須要編寫相應的程序進行數據同步。
        
        數據同步通常分兩種狀況
            全量同步:天天定時將當天的生產數據所有同步過來(優勢:實現簡單  缺點:數據同步不及時)
            增量同步:每新增一條,便將該數據同步過來(優勢:數據近實時同步  缺點:實現相對困難)

        我方須要作的事情:
            讀取中間表的數據,並同步到業務系統中(可能須要調用我方相應的業務邏輯)
        模型抽離
            生產者消費者模型
            生產者:讀取中間表的數據
            消費者:消費生產者生產的數據

        接口協議的制定
            1.取我方業務所須要的字段
            2.須要有字段記錄數據何時進入中間表
            3.增長相應的數據標誌位,用於標誌數據的同步狀態
            4.記錄數據的同步時間

        技術選型:
            mybatis、單一輩子產者多消費者、多線程併發操做

    2 中間表設計
        詳見視屏
    3 基礎環境搭建
        詳見視屏
 

    4 生產者代碼實現

        1:分批讀取中間表(10I),並將讀取到的數據狀態修改成10D(處理中)
        2:將相應的數據交付給消費者進行消費
            1:把生產完的數據,直接放到隊列裏,由消費者去進行消費
            2:把消費者放到隊列裏面,生產完數據,直接從隊列裏拿出消費者進行消費

 

併發編程及原理視頻教程,基礎理論+實戰,由淺入深,層層深刻,剖析併發編程原理
更多課程資料能夠查看https://xdclass.net/#/coursecatalogue?video_id=17
贈送大量的學習資料以及乾貨

相關文章
相關標籤/搜索