1.問題java
2.關鍵詞node
同步,異步,阻塞,非阻塞,並行,併發,臨界區,競爭條件,指令重排,鎖,amdahl,gustafsongolang
3.全文概要算法
上一篇咱們介紹分佈式系統的知識體系,因爲單機的性能上限緣由咱們纔不得不發展分佈式技術。那麼話說回來,若是單機的性能沒能最大限度的榨取出來,就盲目的就建設分佈式系統,那就有點本末倒置了。並且上一篇咱們給的忠告是若是有可能的話,不要用分佈式,意思是說若是單機性能知足的話,就不要折騰複雜的分佈式架構。若是說分佈式架構是宏觀上的性能擴展,那麼高併發則是微觀上的性能調優,這也是上一篇性能部分拆出來的大專題。本文將從線程的基礎理論談起,逐步探究線程的內存模型,線程的交互,線程工具和併發模型的發展。掃除關於併發編程的諸多模糊概念,重新構建併發編程的層次結構。數據庫
4.基礎理論編程
4.1基本概念緩存
開始學習併發編程前,咱們須要熟悉一些理論概念。既然咱們要研究的是併發編程,那首先應該對併發這個概念有所理解纔是,而說到併發咱們確定要要討論一些並行。安全
而後咱們須要再瞭解一下同步和異步的區別:性能優化
接着咱們再瞭解一個重要的概念:網絡
因爲共享數據的出現,必然會致使競爭,因此咱們須要再瞭解一下:
若是兩個操做都在等待某個共享資源並且都互不退讓就會形成死鎖:
4.2併發級別
理想狀況下咱們但願全部線程都一塊兒並行飛起來。可是CPU數量有限,線程源源不斷,總得有個先來後到,不一樣場景須要的併發需求也不同,好比秒殺系統咱們須要很高的併發程度,可是對於一些下載服務,咱們須要的是更快的響應,併發反而是其次的。因此咱們也定義了併發的級別,來應對不一樣的需求場景。
4.3量化模型
首先,多線程不意味着併發,但併發確定是多線程或者多進程。咱們知道多線程存在的優點是可以更好的利用資源,有更快的請求響應。可是咱們也深知一旦進入多線程,附帶而來的是更高的編碼複雜度,線程設計不當反而會帶來更高的切換成本和資源開銷。可是整體上咱們確定知道利大於弊,這不是廢話嗎,否則誰還願意去搞多線程併發程序,可是如何衡量多線程帶來的效率提高呢,咱們須要藉助兩個定律來衡量。
兩面列舉了這兩個定律來衡量系統改善後提高效率的量化指標,具體的應用咱們在下文的線程調優會再詳細介紹。
5.內存模型
宏觀上分佈式系統須要解決的首要問題是數據一致性,一樣,微觀上併發編程要解決的首要問題也是數據一致性。貌似咱們搞了這麼多年的鬥爭都是在公關一致性這個世界性難題。既然併發編程要從微觀開始,那麼咱們確定要對CPU和內存的工做機理有所瞭解,尤爲是數據在CPU和內存直接的傳輸機制。
5.1總體原則
探究內存模型以前咱們要拋出三個概念:
5.2邏輯內存
咱們談的邏輯內存也便是JVM的內存格局。JVM將操做系統提供的物理內存和CPU緩存在邏輯分爲堆,棧,方法區,和程序計數器。在《從宏觀微觀角度淺析JVM虛擬機》 一文咱們詳細介紹了JVM的內存模型分佈,併發編程咱們主要關注的是堆棧的分配,由於線程都是寄生在棧裏面的內存段,把棧裏面的方法邏輯讀取到CPU進行運算。
5.3物理內存
而實際的物理內存包含了主存和CPU的各級緩存還有寄存器,而爲了計算效率,CPU每每回就近從緩存裏面讀取數據。在併發的狀況下就會形成多個線程之間對共享數據的錯誤使用。
5.4內存映射
因爲可能發生對象的變量同時出如今主存和CPU緩存中,就可能致使了以下問題:
因爲線程內的變量對棧外是不可見的,可是成員變量等共享資源是競爭條件,全部線程可見,就會出現以下當一個線程從主存拿了一個變量1修改後變成2存放在CPU緩存,還沒來得及同步回主存時,另一個線程又直接從主存讀取變量爲1,這樣就出現了髒讀。
如今咱們弄清楚了線程同步過程數據不一致的緣由,接下來要解決的目標就是如何避免這種狀況的發生,通過大量的探索和實踐,咱們從概念上不斷的革新好比並發模型的流水線化和無狀態函數式化,並且也提供了大量的實用工具。接下來咱們從無到有,先了解最簡單的單個線程的一些特色,弄清楚一個線程有多少能耐後,才能深入認識多個線程一塊兒打交道會出現什麼幺蛾子。
6.線程單元
6.1狀態
咱們知道應用啓動體現的就是靜態指令加載進內存,進而進入CPU運算,操做系統在內存開闢了一段棧內存用來存放指令和變量值,從而造成了進程。而其實咱們的JVM也就是一個進程並且,而線程是進程的最小單位,也就是說進程是由不少個線程組成的。而因爲進程的上下文關聯的變量,引用,計數器等現場數據佔用了打段的內存空間,因此頻繁切換進程須要整理一大段內存空間來保存未執行完的進程現場,等下次輪到CPU時間片再恢復現場進行運算。這樣既耗費時間又浪費空間,因此咱們纔要研究多線程。畢竟因爲線程乾的活畢竟少,工做現場數據畢竟少,因此切換起來比較快並且暫用少許空間。而線程切換直接也須要遵照必定的法則,否則到時候把工做現場破壞了就沒法恢復工做了。
線程狀態
咱們先來研究線程的生命週期,看看Thread類裏面對線程狀態的定義就知道
public enum State { /** * Thread state for a thread which has not yet started. */ NEW, /** * Thread state for a runnable thread. A thread in the runnable * state is executing in the Java virtual machine but it may * be waiting for other resources from the operating system * such as processor. */ RUNNABLE, /** * Thread state for a thread blocked waiting for a monitor lock. * A thread in the blocked state is waiting for a monitor lock * to enter a synchronized block/method or * reenter a synchronized block/method after calling * {@link Object#wait() Object.wait}. */ BLOCKED, /** * Thread state for a waiting thread. * A thread is in the waiting state due to calling one of the * following methods: * <ul> * <li>{@link Object#wait() Object.wait} with no timeout</li> * <li>{@link #join() Thread.join} with no timeout</li> * <li>{@link LockSupport#park() LockSupport.park}</li> * </ul> * * <p>A thread in the waiting state is waiting for another thread to * perform a particular action. * * For example, a thread that has called <tt>Object.wait()</tt> * on an object is waiting for another thread to call * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on * that object. A thread that has called <tt>Thread.join()</tt> * is waiting for a specified thread to terminate. */ WAITING, /** * Thread state for a waiting thread with a specified waiting time. * A thread is in the timed waiting state due to calling one of * the following methods with a specified positive waiting time: * <ul> * <li>{@link #sleep Thread.sleep}</li> * <li>{@link Object#wait(long) Object.wait} with timeout</li> * <li>{@link #join(long) Thread.join} with timeout</li> * <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li> * <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li> * </ul> */ TIMED_WAITING, /** * Thread state for a terminated thread. * The thread has completed execution. */ TERMINATED; }
生命週期
線程的狀態:NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED。註釋也解釋得很清楚各個狀態的做用,而各個狀態的轉換也有必定的規則須要遵循的。
6.2動做
介紹完線程的狀態和生命週期,接下來我瞭解的線程具有哪些經常使用的操做。首先線程也是一個普通的對象Thread,全部的線程都是Thread或者其子類的對象。那麼這個內存對象被建立出來後就會放在JVM的堆內存空間,當咱們執行start()方法的時候,對象的方法體在棧空間分配好對應的棧幀來往執行引擎輸送指令(也便是方法體翻譯成JVM的指令集)。
線程操做
線程分組
爲了管理線程,因而有了線程組的概念,業務上把相似的線程放在一個ThreadGroup裏面統一管理。線程組表示一組線程,此外,線程組還能夠包括其餘線程組。線程組造成一個樹,其中除了初始線程組之外的每一個線程組都有一個父線程。線程被容許訪問它本身的線程組信息,但不能訪問線程組的父線程組或任何其餘線程組的信息。
守護線程
一般狀況下,線程運行到最後一條指令後則完成生命週期,結束線程,而後系統回收資源。或者單遇到異常或者return提早返回,可是若是咱們想讓線程常駐內存的話,好比一些監控類線程,須要24小時值班的,因而咱們又創造了守護線程的概念。
setDaemon()傳入true則會把線程一直保持在內存裏面,除非JVM宕機不然不會退出。
線程優先級
線程優先級其實只是對線程打的一個標誌,但並不意味這高優先級的必定比低優先級的先執行,具體還要看操做系統的資源調度狀況。一般線程優先級爲5,邊界爲[1,10]。
/** * The minimum priority that a thread can have. */ public final static int MIN_PRIORITY = 1;/** * The default priority that is assigned to a thread. */ public final static int NORM_PRIORITY = 5; /** * The maximum priority that a thread can have. */ public final static int MAX_PRIORITY = 10;
本節介紹了線程單元的轉態切換和經常使用的一些操做方法。若是隻是單線程的話,其餘都不必研究這些,重頭戲在於多線程直接的競爭配合操做,下一節則重點介紹多個線程的交互須要關注哪些問題。
7.線程交互
其實上一節介紹的線程狀態切換和線程操做都是爲線程交互作準備的。否則若是隻是單線程徹底不必搞什麼通知,恢復,讓步之類的操做了。
7.1交互方式
線程交互也就是線程直接的通訊,最直接的辦法就是線程直接直接通訊傳值,而間接方式則是經過共享變量來達到彼此的交互。
7.2線程安全
咱們最關注的仍是經過共享變量來達到交互的方式。線程若是都各自幹活互不搭理的話天然相安無事,但多數狀況下線程直接須要打交道,並且須要分享共享資源,那麼這個時候最核心的就是線程安全了。
什麼是線程安全?
當多個線程訪問同一個對象時,若是不用考慮這些線程在運行時環境下的調度和交替運行,也不須要進行額外的同步,或者在調用方進行任何其餘的協調操做,調用這個對象的行爲均可以獲取正確的結果,那這個對象是線程安全的。(摘自《深刻Java虛擬機》)
如何保證線程安全?
咱們最先接觸線程安全多是JDK提供的一些號稱線程安全的容器,好比Vetor較ArrayList是線程安全,HashTable較HashMap是線程安全?其實線程安全類並不表明也不等同線程安全的程序,而線程不安全的類一樣能夠完成線程安全的程序。咱們關注的也就是寫出線程安全的程序,那麼如何寫出線程安全的代碼呢?下面列舉了線程安全的主要設計技術:
無狀態
這個有點函數式編程的味道,下文併發模式會介紹到,總之就是線程只有入參和局部變量,若是變量是引用的話,確保變量的建立和調用生命週期都發生在線程棧內,就能夠確保線程安全。
無共享狀態
徹底要求線程無狀態比較難實現,必要的狀態是沒法避免的,那麼咱們就必須維護不一樣線程之間的不一樣狀態,這但是個麻煩事。幸虧咱們有ThreadLocal這個神器,該對象跟當前線程綁定,並且只對當前線程可見,完美解決了無共享狀態的問題。
不可變狀態
最後實在沒辦法避免狀態共享,在線程之間共享狀態,最怕的就是沒法確保能維護好正確的讀寫順序,並且多線程確實也沒法正確維護好這個共享變量。那麼咱們索性粗暴點,把共享的狀態定位不可變,好比價格final修飾一下,這樣就達到安全狀態共享。
消息傳遞
一個線程一般也不是全部步驟都須要共享狀態,而是部分環節才須要的,那麼咱們把共享狀態的代碼拆開,無共享狀態的那部分天然不用關心,而共享狀態的小段代碼,則經過加入消息組件來傳遞狀態。這個設計到併發模式的流水線編程模式,下文併發模式會重點介紹。
線程安全容器
JUC裏面提供大量的併發容器,涉及到線程交互的時候,使用安全容器能夠避免大部分的錯誤,並且大大下降了代碼的複雜度。
synchronized同步
該關鍵字確保代碼塊同一時間只被一個線程執行,在這個前提下再設計符合線程安全的邏輯
其做用域爲
volatile約束
volatile確保每次操做都能強制同步CPU緩存和主存直接的變量。並且在編譯期間能阻止指令重排。讀寫併發狀況下volatile也不能確保線程安全,上文解析內存模型的時候有提到過。
這節咱們論述了編寫線程安全程序的指導思想,其中咱們提到了JDK提供的JUC工具包,下一節將重點介紹併發編程經常使用的趁手工具。
8.線程工具
前文咱們介紹了內存理論和線程的一些特徵,你們都知道併發編程容易出錯,並且出了錯還很差調試排查,幸虧JDK裏面集成了大量實用的API工具,咱們能熟悉這些工具,寫起併發程序來也事半功倍。
工具篇其實就是對鎖的不斷變種,適應更多的開發場景,提升性能,提供更方便的工具,從最粗暴的同步修飾符,到靈活的可重入鎖,到寬鬆的條件,接着到容許多個線程訪問的信號量,最後到讀寫分離鎖。
8.1同步控制
因爲大多數的併發場景都是須要訪問到共享資源的,爲了保證線程安全,咱們不得已採用鎖的技術來作同步控制,這節咱們介紹的是適用不一樣場景各類鎖技術。
ReentrantLock
可重入互斥鎖具備與使用synchronized的隱式監視器鎖具備相同的行爲和語義,但具備更好擴展功能。
ReentrantLock由最後成功鎖定的線程擁有,並且還未解鎖。當鎖未被其餘線程佔有時,線程調用lock()將返回而且成功獲取鎖。若是當前線程已擁有鎖,則該方法將當即返回。這能夠使用方法isHeldByCurrentThread()和getHoldCount()來檢查。
構造函數接受可選的fairness參數。當設置爲true時,在競爭條件下,鎖定有利於賦予等待時間最長線程的訪問權限。不然,鎖將不保證特定的訪問順序。在多線程訪問的狀況,使用公平鎖比默認設置,有着更低的吞吐量,可是得到鎖的時間比較小並且能夠避免等待鎖致使的飢餓。可是,鎖的公平性並不能保證線程調度的公平性。所以,使用公平鎖的許多線程中的一個能夠連續屢次得到它,而其餘活動線程沒有進展而且當前沒有持有鎖。不定時的tryLock()方法不遵循公平性設置。即便其餘線程正在等待,若是鎖可用,它也會成功。
Condition
Condition從擁有監控方法(wait,notify,notifyAll)的Object對象中抽離出來成爲獨特的對象,高效的讓每一個對象擁有更多的等待線程。和鎖對比起來,若是說用Lock代替synchronized,那麼Condition就是用來代替Object自己的監控方法。
Condition實例跟Object自己的監控類似,一樣提供wait()方法讓調用的線程暫時掛起讓出資源,知道其餘線程通知該對象轉態變化,纔可能繼續執行。Condition實例來源於Lock實例,經過Lock調用newCondition()便可。Condition較Object原生監控方法,能夠保證通知順序。
Semaphore
鎖和同步塊同時只能容許單個線程訪問共享資源,這個明顯有些單調,部分場景其實能夠容許多個線程訪問,這個時候信號量實例就派上用場了。信號量邏輯上維持了一組許可證, 線程調用acquire()阻塞直到許可證可用後才能執行。 執行release()意味着釋放許可證,實際上信號量並無真正的許可證,只是採用了計數功能來實現這個功能。
ReadWriteLock
顧名思義讀寫鎖將讀寫分離,細化了鎖的粒度,照顧到性能的優化。
CountDownLatch
這個鎖有點「關門放狗」的意思,尤爲在咱們壓測的時候模擬實時並行請求,該實例將線程積累到指定數量後,調用countDown()方法讓全部線程同時執行。
CyclicBarrier
CyclicBarrier是增強版的CountDownLatch,上面講的是一次性「關門放狗」,而循環柵欄則是集齊了指定數量的線程,在資源都容許的狀況下同時執行,而後下一批一樣的操做,周而復始。
LockSupport
LockSupport是用來建立鎖和其餘同步類的基本線程阻塞原語。 LockSupport中的park() 和 unpark() 的做用分別是阻塞線程和解除阻塞線程,並且park()和unpark()不會遇到「Thread.suspend 和 Thread.resume所可能引起的死鎖」問題。由於park() 和 unpark()有許可的存在;調用 park() 的線程和另外一個試圖將其 unpark() 的線程之間的競爭將保持活性。
8.2線程池
線程池總覽
線程多起來的話就須要管理,否則就會亂成一鍋。咱們知道線程在物理上對應的就是棧裏面的一段內存,存放着局部變量的空間和待執行指令集。若是每次執行都要從頭初始化這段內存,而後再交給CPU執行,效率就有點低了。假如咱們知道該段棧內存會被常常用到,那咱們就不要回收,建立完就讓它在棧裏面呆着,要用的時候取出來,用完換回去,是否是就省了初始化線程空間的時間,這樣是咱們搞出線程池的初衷。
其實線程池很簡單,就是搞了個池子放了一堆線程。既然咱們搞線程池是爲了提升效率,那就要考慮線程池放多少個線程比較合適,太多了或者太少了有什麼問題,怎麼拒絕多餘的請求,除了異常怎麼處理。首先咱們來看跟線程池有關的一張類圖。
線程池歸結起來就是這幾個類的使用技巧了,重點關注ThreadPoolExecutor和Executors便可。
建立線程池
萬變不離其宗,建立線程池的各類馬甲方法最後都是調用到這方法裏面,包含核心線程數,最大線程數,線程工廠,拒絕策略等參數。其中線程工廠則能夠實現自定義建立線程的邏輯。
public interface ThreadFactory { Thread newThread(Runnable r); }
建立的核心構造方法ThreadPoolExecutor.java 1301
/** * Creates a new {@code ThreadPoolExecutor} with the given initial * parameters. * * @param corePoolSize the number of threads to keep in the pool, even * if they are idle, unless {@code allowCoreThreadTimeOut} is set * @param maximumPoolSize the maximum number of threads to allow in the * pool * @param keepAliveTime when the number of threads is greater than * the core, this is the maximum time that excess idle threads * will wait for new tasks before terminating. * @param unit the time unit for the {@code keepAliveTime} argument * @param workQueue the queue to use for holding tasks before they are * executed. This queue will hold only the {@code Runnable} * tasks submitted by the {@code execute} method. * @param threadFactory the factory to use when the executor * creates a new thread * @param handler the handler to use when execution is blocked * because the thread bounds and queue capacities are reached * @throws IllegalArgumentException if one of the following holds:<br> * {@code corePoolSize < 0}<br> * {@code keepAliveTime < 0}<br> * {@code maximumPoolSize <= 0}<br> * {@code maximumPoolSize < corePoolSize} * @throws NullPointerException if {@code workQueue} * or {@code threadFactory} or {@code handler} is null */ public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
拒絕策略包含:
/** 實際上並未真正丟棄任務,可是線程池性能會降低 * A handler for rejected tasks that runs the rejected task * directly in the calling thread of the {@code execute} method, * unless the executor has been shut down, in which case the task * is discarded. */ public static class CallerRunsPolicy implements RejectedExecutionHandler /** 粗暴中止拋異常 * A handler for rejected tasks that throws a * {@code RejectedExecutionException}. */ public static class AbortPolicy implements RejectedExecutionHandler /** 悄無聲息的丟棄拒絕的任務 * A handler for rejected tasks that silently discards the * rejected task. */ public static class DiscardPolicy implements RejectedExecutionHandler /** 丟棄最老的請求 * A handler for rejected tasks that discards the oldest unhandled * request and then retries {@code execute}, unless the executor * is shut down, in which case the task is discarded. */ public static class DiscardOldestPolicy implements RejectedExecutionHandler
包括Executors.java中的建立線程池的方法,具體實現也是經過ThreadPoolExecutor來建立的。
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); } public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); } public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { return new ScheduledThreadPoolExecutor(corePoolSize); } public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); }
調用線程池
ThreadPoolExecutor.java 1342
/** 同步執行線程,出現異常打印堆棧信息 * Executes the given task sometime in the future. The task * may execute in a new thread or in an existing pooled thread. * * If the task cannot be submitted for execution, either because this * executor has been shutdown or because its capacity has been reached, * the task is handled by the current {@code RejectedExecutionHandler}. * * @param command the task to execute * @throws RejectedExecutionException at discretion of * {@code RejectedExecutionHandler}, if the task * cannot be accepted for execution * @throws NullPointerException if {@code command} is null */public void execute(Runnable command)/** * 異步提交線程任務,出現異常沒法同步追蹤堆棧,本質上也是調用execute()方法 */public <T> Future<T> submit(Runnable task, T result) { if (task == null) throw new NullPointerException(); RunnableFuture<T> ftask = newTaskFor(task, result); execute(ftask); return ftask; }
線程池優化
線程池已是咱們使用線程的一個優化成果了,而線程池自己的優化其實就是根據實際業務選擇好不一樣類型的線程池,預估併發線程數量,控制好線程池預留線程數(最大線程數通常設爲2N+1最好,N是CPU核數),這些涉及CPU數量,核數還有具體業務。
另外咱們還注意到ForkJoinPool繼承了AbstractExecutorService,這是在JDK7才加上去的,目的就是提升任務派生出來更多任務的執行效率,由上圖的繼承關係咱們能夠知道跟普通線程池最大的差別是執行的任務類型不一樣。
public void execute(ForkJoinTask<?> task) { if (task == null) throw new NullPointerException(); externalPush(task); }public void execute(Runnable task) { if (task == null) throw new NullPointerException(); ForkJoinTask<?> job; if (task instanceof ForkJoinTask<?>) // avoid re-wrap job = (ForkJoinTask<?>) task; else job = new ForkJoinTask.RunnableExecuteAction(task); externalPush(job); }
8.3併發容器
其實咱們平常開發大多數併發場景直接用JDK 提供的線程安全數據結構足矣,下面列舉了經常使用的列表,集合等容器,具體就不展開講,相信你們都用得很熟悉了。
9.線程調優
9.1性能指標
回想一下,當咱們在談性能優化的時候,咱們可能指的是數據庫的讀寫次數,也可能指網站的響應時間。一般咱們會用QPS,TPS,RT,併發數,吞吐量,更進一步的還會對比CPU負載來衡量一個系統的性能。
固然咱們知道一個系統的吞吐量和響應時間跟外部網絡,分佈式架構等都存在強關聯,性能優化也跟各級緩存設計,數據冗餘等架構有很大關係,假設其餘方面咱們都已經完成了,聚焦到本文咱們暫時關心的是單節點的性能優化。畢竟一屋不掃何以掃天下,總體系統的優化也有賴於各個節點的調優。從感官上來談,當請求量不多的時候,咱們能夠很輕鬆的經過各類緩存優化來提升響應時間。可是隨着用戶激增,請求次數的增長,咱們的服務也對應着須要併發模型來支撐。可是一個節點的併發量有個上限,當達到這個上限後,響應時間就會變長,因此咱們須要探索併發到什麼程度纔是最優的,才能保證最高的併發數,同時響應時間又能保持在理想狀況。因爲咱們暫時不關注節點之外的網絡狀況,那麼下文咱們特指的RT是指服務接收到請求後,完成計算,返回計算結果經歷的時間。
單線程
單線程狀況下,服務接收到請求後開始初始化,資源準備,計算,返回結果,時間主要花在CPU計算和CPU外的IO等待時間,多個請求來也只能排隊一個一個來,那麼RT計算以下
RT = T(cpu) + T(io)
QPS = 1000ms / RT
多線程
單線程狀況很好計算,多線程狀況就複雜了,咱們目標是計算出最佳併發量,也就是線程數N
單核狀況:N = [T(cpu) + T(io)] / T(cpu)
M核狀況:N = [T(cpu) + T(io)] / T(cpu) * M
因爲多核狀況CPU未必能所有使用,存在一個資源利用百分比P
那麼併發的最佳線程數 N = [T(cpu) + T(io)] / T(cpu) M P
吞吐量
咱們知道單線程的QPS很容易算出來,那麼多線程的QPS
QPS = 1000ms / RT N = 1000ms / T(cpu) + T(io) [T(cpu) + T(io)] / T(cpu) M P= 1000ms / T(cpu) M P
在機器核數固定狀況下,也便是併發模式下最大的吞吐量跟服務的CPU處理時間和CPU利用率有關。CPU利用率不高,就是一般咱們聽到最多的抱怨,壓測時候qps都打滿了,可是cpu的load就是上不去。併發模型中多半個共享資源有關,而共享資源又跟鎖息息相關,那麼大部分時候咱們想對節點服務作性能調優時就是對鎖的優化,這個下一節會提到。
前面咱們是假設機器核數固定的狀況下作優化的,那假如咱們把緩存,IO,鎖都優化了,剩下的還有啥空間去突破呢?回想一下咱們談基礎理論的時候提到的Amdahl定律,公式以前已經給出,該定律想表達的結論是隨着核數或者處理器個數的增長,能夠增長優化加速比,可是會達到上限,並且增長趨勢愈發不明顯。
9.2鎖優化
說真的,咱們並不喜歡鎖的,只不過因爲臨界資源的存在不得已爲之。若是業務上設計能避免出現臨界資源,那就沒有鎖優化什麼事了。可是,鎖優化的一些原則仍是要說一說的。
時間
既然咱們並不喜歡鎖,那麼就按需索取,只在核心的同步塊加鎖,用完立馬釋放,減小鎖定臨界區的時間,這樣就能夠把資源競爭的風險降到最低。
粒度
進一步看,有時候咱們核心同步塊能夠進一步分離,好比只讀的狀況下並不須要加鎖,這時候就能夠用讀寫鎖各自的讀寫功能。
還有一種狀況,有時候咱們反而會當心翼翼的處處加鎖來防止意外出現,可能出現三個同步塊加了三個鎖,這也形成CPU的過多停頓,根據業務其實能夠把相關邏輯合併起來,也就是鎖粗化。
鎖的分離和粗化具體還得看業務如何操做。
尺度
除了鎖暫用時間和粒度外,還有就是鎖的尺度,仍是根據業務來,能用共享鎖定的狀況就不要用獨享鎖。
死鎖
這個不用說都知道,死鎖防不勝防,咱們前面也介紹不少現成的工具,好比可重入鎖,還有線程本地變量等方式,均可以必定程度避免死鎖。
9.3JVM鎖機制
咱們在代碼層面把鎖的應用都按照安全法則作到最好了,那接下來要作的就是下鑽到JVM級別的鎖優化。具體實現原理咱們暫不展開,後續有機會再搞個專題寫寫JVM鎖實現。
自旋鎖(Spin Lock)
自旋鎖的原理很是簡單。若是持有鎖的線程能夠在短期內釋放鎖資源,那麼等待競爭鎖的那些線程不須要在內核狀態和用戶狀態之間進行切換。 它只須要等待,而且鎖能夠在釋放鎖以後當即得到鎖。這能夠避免消耗用戶線程和內核切換。
可是,自旋鎖讓CPU空等着什麼也不幹也是一種浪費。 若是自旋鎖的對象一直沒法得到臨界資源,則線程也沒法在沒有執行實際計算的狀況下一致進行CPU空轉,所以須要設置自旋鎖的最大等待時間。若是持有鎖的線程在旋轉等待的最大時間沒有釋放鎖,則自旋鎖線程將中止旋轉進入阻塞狀態。
JDK1.6開啓自旋鎖 -XX:+UseSpinning,1.7以後控制器收回到JVM自主控制
偏向鎖(Biased Lock)
偏向鎖偏向於第一個訪問鎖的線程,若是在運行過程當中,同步鎖只有一個線程訪問,不存在多線程爭用的狀況,則線程是不須要觸發同步的,這種狀況下,就會給線程加一個偏向鎖。若是在運行過程當中,遇到了其餘線程搶佔鎖,則持有偏向鎖的線程會被掛起,JVM會消除它身上的偏向鎖,將鎖恢復到標準的輕量級鎖。
JDK1.6開啓自旋鎖 -XX:+UseBiasedLocking,1.7以後控制器收回到JVM自主控制
輕量級鎖(Lightweight Lock)
輕量級鎖是由偏向鎖升級來的,偏向鎖運行在一個線程進入同步塊的狀況下,當第二個線程加入鎖競爭的時候,偏向鎖就會升級爲輕量級鎖。
重量級鎖(Heavyweight Lock)
若是鎖檢測到與另外一個線程的爭用,則鎖定會膨脹至重量級鎖。也就是咱們常規用的同步修飾產生的同步做用。
9.4無鎖
最後其實我想說的是,雖然鎖很符合咱們人類的邏輯思惟,設計起來也相對簡單,可是擺脫不了臨界區的限制。那麼咱們不妨換個思路,進入無鎖的時間,也就是咱們可能會增長業務複雜度的狀況下,來消除鎖的存在。
CAS策略
著名的CAS(Compare And Swap),是多線程中用於實現同步的原子指令。 它將內存位置的內容與給定值進行比較,而且只有它們相同時,纔將該內存位置的內容修改成新的給定值。 這是做爲單個原子操做完成的。 原子性保證了新值是根據最新信息計算出來的; 若是在此期間該值已被另外一個線程更新,則寫入將失敗。 操做的結果必須代表它是否進行了替換; 這能夠經過簡單的Boolean來響應,或經過返回從內存位置讀取的值(而不是寫入它的值)來完成。
也就是一個原子操做包含了要操做的數據和給定認爲正確的值進行對比,一致的話就繼續,不一致則會重試。這樣就在沒有鎖的狀況下完成併發操做。
咱們知道原子類 AtomicInteger內部實現的原理就是採用了CAS策略來完成的。
AtomicInteger.java 132
/** * Atomically sets the value to the given updated value * if the current value {@code ==} the expected value. * * @param expect the expected value * @param update the new value * @return {@code true} if successful. False return indicates that * the actual value was not equal to the expected value. */public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }
相似的還有AtomicReference.java 115
/** * Atomically sets the value to the given updated value * if the current value {@code ==} the expected value. * @param expect the expected value * @param update the new value * @return {@code true} if successful. False return indicates that * the actual value was not equal to the expected value. */public final boolean compareAndSet(V expect, V update) { return unsafe.compareAndSwapObject(this, valueOffset, expect, update); }
有興趣的同窗能夠再瞭解一下Unsafe的實現,進一步能夠了解Distuptor無鎖框架。
10.併發模型
前面咱們大費周章的從併發的基礎概念到多線程的使用方法和優化技巧。但都是戰術層面的,本節咱們試着從戰略的高度來擴展一下併發編程的世界。可能大多數狀況下咱們談併發都會想到多線程,可是本節咱們要打破這種思惟,在徹底不用搞多線程那一套的狀況下實現併發。
首先咱們用」多線程模式「來回顧前文所講的全部關於Thread衍生出來的定義,開發和優化的技術。
多線程模式
單位線程完成完整的任務,也便是一條龍服務線程。
流水線模型
介紹完傳統多線程工做模式後,咱們來學習另一種併發模式,傳統的多線程工做模式,理解起來很直觀,接下來咱們要介紹的另一種併發模式看起來就不那麼直觀了。
流水線模型,特色是無狀態線程,無狀態也意味着無需競爭共享資源,無需等待,也就是非阻塞模型。流水線模型顧名思義就是流水線上有多個環節,每一個環節完成本身的工做後就交給下一個環節,無需等待上游,周而復始的完成本身崗位上的一畝三分地就行。各個環節之間交付無需等待,完成便可交付。
而工廠的流水線也不止一條,因此有多條流水線同時工做。
不一樣崗位的生產效率是不同的,因此不一樣流水線之間也能夠發生協同。
咱們說流水線模型也稱爲響應式模型或者事件驅動模型,其實就是流水線上上游崗位完成生產就通知下游崗位,因此完成了一個事件的通知,每完成一次就通知一下,就是響應式的意思。
流水線模型整體的思想就是縱向切分任務,把任務裏面耗時太久的環節單獨隔離出來,避免完成一個任務須要耗費等待的時間。在實現上又分爲Actors和Channels模型
因爲各個環節直接不直接交互,因此上下游之間並不知道對方是誰,比如不一樣環節直接用的是幾條公共的傳送帶來接收物品,各自只須要把完成後的半成品扔到傳送帶,即便後面流水線優化了,去掉中間的環節,對於個體崗位來講也是無感知的,它只是周而復始的從傳送帶拿物品來加工。
流水線的優缺點:
優點:
劣勢:
因爲流水線模式跟人類的順序執行思惟不同,比較費解,那麼有沒有辦法讓咱們編碼的時候像寫傳統的多線程代碼同樣,而運行起來又是流水線模式呢?答案是確定的,好比基於Java的Akka/Reator/Vert.x/Play/Qbit框架,或者golang就是爲流水線模式而生的併發語言,還有nodeJS等等。
流水線模型的開發實踐能夠參考流水線模型實踐。
其實流水線模型背後用的也仍是多線程來實現,只不過對於傳統多線程模式下咱們須要當心翼翼來處理跟蹤資源共享問題,而流水線模式把之前一個線程作的事情拆成多個,每個環節再用一條線程來完成,避免共享,線程直接經過管道傳輸消息。
這一塊展開也是一個專題,主要設計NIO,Netty和Akka的編程實踐,先佔坑後面補上。
函數式模型
函數式並行模型相似流水線模型,單一的函數是無狀態的,因此避免了資源競爭的複雜度,同時每一個函數相似流水線裏面的單一環境,彼此直接經過函數調用傳遞參數副本,函數以外的數據不會被修改。函數式模式跟流水線模式相輔相成逐漸成爲更爲主流的併發架構。具體的思想和編程實踐也是個大專題,篇幅限制本文就先不展開,擬在下個專題中詳細介紹《函數式編程演化》。
11.總結
因爲CPU和I/O自然存在的矛盾,傳統順序的同步工做模式致使任務阻塞,CPU空等着沒有執行,浪費資源。多線程爲突破了同步工做模式的狀況下浪費CPU資源,即便單核狀況下也能將時間片拆分紅單位給更多的線程來輪詢享用。多線程在不一樣享狀態的狀況下很是高效,無論協同式仍是搶佔式都能在單位時間內執行更多的任務,從而更好的榨取CPU資源。
可是多數狀況下線程之間是須要通訊的,這一核心場景緻使了一系列的問題,也就是線程安全。內存被共享的單位因爲被不一樣線程輪番讀取寫入操做,這種操做帶來的後果每每是寫代碼的人類沒想到的,也就是併發帶來的髒數據等問題。解決了資源使用效率問題,又帶來了新的安全問題,如何解決?悲觀方式就是對於存在共享內存的場景,不管如何只贊成同一時刻一個線程操做,也就是同步操做方法或者代碼段或者顯示加鎖。或者volatile來使共享的主存跟每條線程的工做內存同步(每次讀都從主存刷新,每次寫完都刷到主存)
要保證線程安全:
線程安全後,要考慮的就是效率問題,若是不解決效率問題,那還幹嗎要多線程。。。
若是全部線程都很自覺,快速執行完就跑路,那就是咱們的理想狀況了。可是,部分線程又臭又長(I/O阻塞),不能讓一直賴在CPU不走,就把他上下文(線程號,變量,執行到哪等數值的快照)保存到內存,而後讓它滾蛋下一個線程來。可是切換太快的話也不合適,畢竟每次保存線程的做案現場也要花很多時間的,單位時間執行線程數要控制在一個適當的個數。建立線程也是一項很吃力的工做,一個線程就是在棧內存裏面開闢一段內存空間,根據字節碼分配臨時變量空間,不一樣操做系統一般不同。不能頻繁的建立銷燬線程。那就搞個線程池出來,用的時候拿出來,用完扔回去,簡單省事。可是線程池的建立也有門道,不能無限建立否則就失去意義了。操做系統有必定上限,線程池太多線程內存爆了,系統奔潰,因此須要一個機制。容納1024個線程,多了排隊再多了扔掉。回到線程切換,因爲建立線程耗費資源,切換也花費,有時候切換線程的時間甚至比讓線程待在cpu無所事事更長,那就給加個自旋鎖,就是讓它本身再cpu打滾啥事不幹,一下子輪到它裏面就能幹活。
既然多線程同步又得加鎖耗資源,不一樣步又有共享安全問題。那能不能把這些鎖,共享,同步,要注意的問題封裝起來。搞出一個異步的工做機制,不用管底層的同步問題,只管業務問題。傳統是工匠幹活一根筋幹完,事件驅動是流水線,把一件事拆分紅多個環節,每一個環節有惟一標識,各個環節批量生產,在流水線對接。這樣在CPU單獨幹,不共享,不阻塞,幹完本身的通知管工,高效封裝了內部線程的運行規則,把業務關係暴露給管理者。
爲了讓學習變得輕鬆、高效,今天給你們免費分享一套阿里架構師傳授的一套教學資源。幫助你們在成爲架構師的道路上披荊斬棘。
這套視頻課程詳細講解了(Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化、分佈式架構)等這些成爲架構師必備的內容!加扣羣:926611793 便可免費領取
本文主要將的數基於JAVA的傳統多線程併發模型,下面例牌給出知識體系圖。
看完了別忘記點個收藏哦!