多線程實戰

線程基礎:

  1. <線程安全>多個線程訪問同一個資源的時候,默認是以排隊的方式進行處理,而且,線程執行的順序不是看書寫的順序,而是看CPU怎麼去分配線程的。 所以,線程會不斷輪詢鎖,會造成鎖競爭。

  2. <多個線程多把鎖> 多個線程多把鎖,每個線程都拿到自己指定的鎖,分別獲得鎖之後,執行synchronized方法體的內容。 兩個線程,線程獲得的就是兩個不同的鎖,他們互不影響。有一種情況下是相同的鎖,即在靜態方法上添加synchronized關鍵字上,表示鎖定.class類,使用的是類級別的鎖。

  3. <對象鎖的同步和異步>

    1. 同步就是存在共享的資源。沒有共享的資源就沒有必要同步。
    2. 異步就是獨立的,相互之間不受到任何的約束。例如使用http的時候,當頁面發起ajax請求時,還可以繼續瀏覽和操作頁面的內容,兩者之間沒有任何的關係。
    3. 關鍵點:原子性(同步)、可見性。
  4. <髒讀>

    1. 對於對象的同步和異步的方法,在設計的時候,需要考慮問題的整體性,不然就會出現數據不一致的錯誤。在oracle中也有這樣的ACID,保證數據的正確性,當獲取數據的時候,若是其他的客戶端修改了這個值,也不會返回給用戶看,始終看到的還是舊值。
  5. <Synchronized其他概念>

    1. 方法重入鎖:可以鎖方法,再去調用鎖方法,可以不斷的嵌入。
    2. 父子類重入鎖: 子類調用父類的方法,這樣是線程安全的。
  6. <Synchronized注意細節>

    1. 注意鎖的粒度,只鎖共享資源的代碼區域 ,不要過多的鎖住代碼,會造成性能降低,大量的線程等待。
    2. 不要使用String常量作爲鎖,會出現各種問題。
    3. 使用String對象作爲鎖的時候,不要在線程中修改鎖,這樣會導致鎖失效,因爲string 對象已經被修改了。
    4. 使用一個對象作爲鎖的時候,可以更改屬性值,鎖不會失效。不能修改對象的引用
  7. volatile:

    1. 概念:主要作用是讓變量在多個線程之間可見。
    2. 常見問題:就是線程啓動之後,對於線程屬性的修改,不會生效。主要原因就是因爲JDK1.5之後對於線程的優化,每一個線程在啓動之後,都會產生一個自己的獨立運行空間,保存需要的屬性,所以,在外部修改的屬性,無法影響到線程自己的屬性。
    3. 線程執行流程:線程啓動的時候,會開闢一塊空間給線程空間使用,存放主內存的屬性,線程執行引擎默認只會從線程空間獲取數據,不會到主內存獲取數據。而使用了Volatile關鍵字之後,當數據發生改變時,則強制要求線程引擎去主內存中獲取數據。
    4. 缺點:只是保證了線程之間的可見性,無法保證線程的原子性。 可是使用Atomic*類來彌補(atomic類只保證本身方法的原子性,並不保證多次操作的原子性)。
  8. 線程之間的通信

    1. wait和notify:兩者都是object的方法,兩者必須配合synchronized使用,wait釋放鎖,notify不釋放鎖,notify了不會立馬通知到wait。
    2. CountDownLatch:不需要使用synchronized,可以實時的通知。
    3. ThreadLocal:是一種多線程之間併發訪問變量的解決方案。不提供鎖,而是,使每個線程都有一個變量的副本(空間換時間的手法)。在併發不是很高的時候,加鎖的性能更好;但是在高併發下或是競爭激烈的情況下,ThreadLocal可以有效降低鎖競爭。
    4. 安全的單列模式: 1.靜態內部類 2.double check(兩次判斷obj是否爲null)
  9. 併發類容器:

    1. ConcurrentHashMap; ConcurrentSkipListMap(彌補ConcurrentHashMap的不可以排序,相當treeMap)。 把Map分爲16個segment(段)。每段相當於一個hashTable。段之間是隔離的,是線程安全的。降低 了鎖的粒度。
    2. copy-on-write容器:有兩種實現CopyOnWriteArrayList和CopyOnWriteArraySet。
    3. Queue:
      1. ConcurrentLinkedQueue爲代表的高性能Queue。 通過無鎖的方式,實現高併發下的高性能。它是一個基於鏈接節點的無界線程安全隊列。該隊列的元素遵循先進先出的原則。頭是最先加入的,尾是最後加入的。不允許null元素。
      2. BlockingQueue爲代表的阻塞隊列。
      3. DelayQueue: 給放入的元素,設置超時時間。 放入的元素類型必須實現Delayed接口。需要重寫兩個方法getDelay(判斷是否到截止時間);comcompareTo();調用take的時候,不是立刻取出元素,而是到了getDelay爲負數的時候,才取出數據。
      4. PriorityBlockingQueue: 並不是放入元素的時候,就排好序,而是在take的時候,會根據優先級,拿取到最高優先級的數據。
      5. SynchronousQueue: 是一個沒有存儲空間的queue,只有先take,才能add,add也不是添加到容器中,而是直接給take使用。
  10. 線程設計模式:

    1. Future模式:重在異步模式的體現。
      1. 需求: 接收到請求,直接返回,然後後臺自己處理數據,最後返回真實的數據,當用戶真正去看數據的時候,就獲取真實數據。 不會讓用戶一直處於等待數據處理完成狀態。
      2. 實現:就是請求時直接返回假數據,使用線程在後臺實行操作真實數據,當真的需要真實數據的時候,再去調用準備好的數據。
    2. Master-Worker模式:細化任務,使用多線程,做任務。
      1. 需求:系統由兩類進程協作進行。master負責接收和分配任務。worker負責處理子任務,worker處理完任務,再返回給master,進行歸納和總結。從而提高系統的吞吐量。
      2. 實現:
        1. master的工作:1. 使用ConcurrentLinkedQueue去承載所有的任務。 2. 使用HashMap<String, Thread> 去承載所有的worker; 3. 使用ConcurrentHashMap<String, Object>承載每一個併發處理任務的結果集。
        2. Worker的工作:1. 首先得實現Runnable接口; 2. 每一個worker對象需要有master的ConcurrentLinkedQueue的引用; 3. 每一個worker對象需要有master的ConcurrentHashMap的引用;
        3. worker從master的queue去取任務,然後,把自己放入HashMap中,然後最後的結果存入master的ConcurrentHashMap中。
  11. 線程池:

    1. Executors線程池工廠。使用它來創建各種線程池。
      1. newFixedThreadPool(): 返回一個固定數量的線程池,線程池中線程數量始終不變。當有一個任務提交時,若線程空閒,立刻執行,若沒有,則暫存在一個任務隊列中等待空閒的線程執行。
      2. newSingleExecutor(): 創建一個線程的線程池,當有一個任務提交時,若線程空閒,立刻執行,若沒有,則暫存在一個任務隊列中等待空閒的線程執行。
      3. newCachedThreadPool(): 返回一個可根據實際情況調整線程數量的線程池,不限制最大數量。若用空閒的線程執行任務,若無任務則不創建線程。並且每一個線程會在60秒後自動回收。
      4. newScheduleThreadPool(): 返回一個ScheduleExecutorService對象,但該線程可以指定線程的數量。
      5. ThreadPoolExecutor:自定義線程池。 會根據使用的是有界隊列還是無界隊列進行區分。
        1. 有界隊列:若有新的任務需要執行,如果線程池實際數量小於corePoolSize,則優先創建線程。若大於corePoolSize,則會將任務加入隊列。若隊列已滿,則在總線程數不大於maximumPoolSize的前提下,創建新的線程。若線程數大於 maximumPoolSize,則執行拒絕策略,或是其它自定義行爲。
        2. 無界隊列:與有界隊列相比,除非線程的資源耗盡,否則,不會出現無界隊列任務入隊失敗的現象。當新任務來時,系統的線程數小於corePoolSize,則新建線程執行任務。當達到corePoolSize時,就不會再增加了。若沒有空閒的線程資源,則會把任務放入到隊列中,直到耗盡系統內存爲止。
        3. JDK拒絕策略:
          1. AbortPolicy:直接拋出異常,阻止系統正常運行。
          2. CallerRunPolicy:只要線程池未關閉,該策略直接在調用者線程中,運行當前被丟棄的任務。
          3. DiscardOldestPolicy:丟棄最老的任務,嘗試再次提交當前任務。
          4. DiscardPolicy:丟棄任務,不給任何的處理。
    2. Executors的submit和execute的區別:
      1. submit可以傳入實現了Callable的接口的實例。
      2. submit方法有返回值。 返回值是一個Future對象,通過其中的get方法,可以知道任務是否執行結束。結束的時候,get方法返回的是null。
  12. 併發包下常用的工具類(concurrent.util):

    1. CountDownLacth: 常用於監聽某些初始化任務,等初始化完成之後,通知主線程繼續工作。
      1. 實例化countDownLatch的時候,傳遞的參數,則是需要進行多少次countDown,才能喚醒await;
    2. CyclicBarrier: 所有的線程準備好,同時執行。實例化CyclicBarrier的時候,傳遞的參數,則是需要進行多少次await,才能一起執行自己線程的內容。一個線程沒有準備好,其它線程也無法執行。 兩者的區別在於:CountDownLacth針對一個線程,CyclicBarrier針對多個線程。
    3. Callable:
    4. Future:適合在處理非常耗時的業務中,可以有效的減小系統的響應時間,提高系統的吞吐量。TutureTask:對象的get是異步執行的。
    5. 信號量(Semaphore): 新系統上線之前,需要對系統的訪問進行合理的評估。保證資源不會被浪費,也不會缺少。
      1. PV(page view): 網站的總訪問量,頁面瀏覽量或點擊量,用戶刷新一次就會被記錄一次。
      2. UV(unique visitor): 訪問網頁的一臺電腦客戶端爲一個訪客,一般來講,時間上以0-24點之內相同的IP只能記錄一次。
      3. QPS(query per second): 每秒查詢數,很大程度上代表了系統業務上的繁忙程度,每次請求的背後,可能對應着多次磁盤I/O,多次網絡請求,多個CPU時間片等等。我們可以通過qps非常直觀的瞭解到系統當前的業務情況。一旦當前的qps超過了設定的閾值,可以考慮增加機器對集羣的擴容,以免壓力過大導致 宕機,可以根據前期的壓力測試得到估值,在結合後期的綜合運維情況,估算出閾值。
      4. RT(response time): 請求的響應時間。
      5. 容量評估:一般來說,我們進行多輪壓力測試之後,可以對系統進行峯值評估,採用所謂的80/20原則,即80%的訪問請求將在20%的時間達到。峯值qps=(總pv * 80%)/ (60 * 60 * 24 * 20%)。然後在將總的峯值qps除以單臺機器所能承受的最高的qps值,就是所需要機器的數量:機器數 = 總的峯值qps / 壓測得出的單機極限qps。
    6. Semaphore: 指定的參數,就是能被多少個線程同時訪問,可以達到一定的限流操作,通過acquire和release進行獲取和釋放訪問許可。 但是,更好的使用redis和nginx來做。
    7. 鎖(Lock):除了可以使用synchronized進行鎖操作,還可以使用Lock對象。
      1. 重入鎖(ReentrantLock): 在需要進行同步的代碼部分加上鎖定,但是最後一定要釋放鎖定,不然會造成鎖永遠無法釋放,其他線程永遠無法進入的結果。 使用的格式就是try, finally結構,保證不管代碼是否正確執行,都保證了鎖能夠正確釋放。
      2. Lock鎖的通信: 對比synchronized的wait和notify。 Lock使用condition來進行鎖之間的通信。獲取condition是lock.newCondition; 對應的就是wait對應await,notify對應的是signal; 一個Lock可以產生多個condition,所以多線程之間的交互非常靈活。可以使得部分線程喚醒,其他線程則等待。
      3. 公平鎖和非公平鎖:公平鎖的性能低於非公平鎖,因爲公平鎖需要維護順序,非公平鎖是任意順序的。
        1. Lock lock = new ReentrantLock(boolean isFair);
        2. 用法:
          1. tryLock(): 嘗試獲得鎖,使用true和false返回結果。
          2. tryLock(): 在給定時間內獲取鎖,使用true和false返回結果。
          3. isFair(): 是否是公平鎖。
          4. isLocked(): 是否鎖定。
          5. getHoldCount():查詢當前線程保持此鎖的個數,也就是調用Lock的次數。
          6. lockInterruptibly(): 優先響應中斷的鎖。
      4. 讀寫鎖(ReentrantReadWriteLock): 適合在讀多寫少的場景下,實現讀寫分離。其本質是分成了兩個鎖,一個讀鎖和一個寫鎖,在讀鎖的情況下,多個線程可以併發的進行訪問,但是在寫鎖的情況下,只能一個一個順序的訪問。《口訣:讀讀共享,寫寫互斥,讀寫互斥》。 通過ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); 獲得讀鎖和寫鎖: ReadLock readLock = lock.readLock(); WriteLock writeLock = lock.writeLock();
      5. 分佈式鎖:需要通過第三方框架,如zk之類的,保證多個線程之間代碼鎖。
  13. 高併發的處理架構:

    1. 網絡方面。
    2. 服務業務方面:劃分不同的模塊,使用nginx/lvs分流 。
    3. java層面上: 限制流量。
  14. Disruptor:

    1. 概況: 完全運行在內存中,使用的是時間驅動方式,能夠再無鎖的的情況下實現網絡的Queue的併發操作。是一個高性能的異步處理框架,輕量級的JMS,也是一個觀察者模式的實現,或是監聽模式的實現。
    2. 常用術語:
      1. RingBuffer: 負責存儲和更新Disruptor中的流通的數據。
      2. Sequence:使用sequence來表示一個特殊組件處理的序號。每一個消費者都維護着一個Sequence。大部分的併發代碼依賴在這些Sequence值的運轉,因此sequence支持多種當前爲AtomicLong類的特性。
      3. Sequencer:這是Disputor的核心。實現了這個接口的兩種生產者(單/多生產者)均實現了所有的併發算法,爲了在生產者和消費者之間進行快速準確的數據傳遞。
      4. SequenceBarrier:由Sequencer生成,並且包含了已經發布的Sequence的引用,這些sequence源於Sequencer和一些獨立的消費者的Sequence。它包含了決定是否有供消費者來消費的Event的邏輯。
      5. WaitStrategy:決定一個消費者如何等待生產者將Event置入到Disruptor中。
      6. Event:從生產者到消費者過程中所處理的數據單元。用戶自定義。
      7. EventProcess:主要事件循環,處理Disruptor中的Event,並且擁有消費者的Sequence。它有一個實現類是BatchEventProcessor,包含Event loop的有效實現,並且將回調到一個EventHandler接口的實現對象。
      8. EventHandler:由用戶實現並且代表了Disruptor中的一個消費者接口。
      9. Producer:由用戶實現,它調用RingBuffer來插入事件(Event),在Disruptor中沒有相應的實現代碼,由用戶實現。
      10. WorkProcess:確保每一個Sequence只被一個processor消費,在同一個workPool中的處理多個workProcess不會消費同樣的Sequence。
      11. WorkerPool: 一個WorkProcessor池,其中workProcessor將消費Sequence,所以任務可以實現workHandler接口的worker之間移交。
      12. LifecycleAware:當BatchEventProcess啓動和停止時,於實現這個接口用於接收通知。
      13. 應用場景:可以參考(併發網站:http://ifeve.com/disruptor/)
        1. 可以順序執行,一個Event在不斷在各個handler中處理。
        2. 可以先順序,再並行的執行。
        3. 簡單的實現生產者-消費者:可以只用RingBuffer。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章