系統學習java高併發

編程語言 Java 程序員 編譯器 最前沿編程諮詢 最前沿編程諮詢 2017-09-27

AVA服務端或者後端需要大量的高併發計算,所以高併發在JAVA服務端或者後端編程中顯的格外重要了。

首先需要有幾個概念:

1.同步和異步

同步異步是來形容方法的一次調用的,同步必須等等方法調用結束後才可以繼續後續的操作,而異步方法調用就會返回(真正的執行一般在另外一個線程中)就可以繼續後續操作了。

2.併發和並行

這兩個概念都是表示2個或者多個任務一起執行,而併發側重的是多任務交替執行,就是一個時間點就只有一個任務(時間碎片很小),而並行是真正意義的同時執行(某個時間碎片有大於1個任務在執行)。

3.臨界區

臨界區這個概念非常重要,就是多個線程都會操作到的,是一個公共資源或者共享的數據,但是每次操作只能一個線程使用而一旦臨界區資源被佔用其他的線程必須等待該資源的釋放,在並行程序中,臨界區資源都是受保護的,如果不保護就會出現問題,達不到預期的效果。

4.阻塞和非阻塞

阻塞和非阻塞是形容多個線程之間的相互影響的(前提是多個線程而不是一個),一個線程佔用了臨界區資源那麼其他線程必須在臨界區之外等待,阻塞是操作系統層面掛起,上下文切換了,所以性能不高。阻塞如果一個線程一直佔用不釋放資源,那麼其他需要該臨界區資源都必須一直等。非阻塞就是運行多個線程同時進入臨界區,只要保證不把數據修改壞就行。

由於臨界區的存在,多線程併發必須受到控制。

根據控制併發的策略,大概可以分為一下幾種:

阻塞、無飢餓、無障礙、無鎖、無等待。

  • 阻塞上面已經解釋了。

  • 由於線程直接的具有優先級,如果線程調度會優先調用優先級高的,那麼優先級低的可能一直無法執行,就會飢餓,如果鎖是公平的,都是按照新進先出就不存在飢餓了就是無飢餓。

  • 無障礙,阻塞其實是悲觀鎖,就是多線程一起修改臨界區數據可能會被修改壞,所以每次只能一個人進行修改,其他需要等待,而無障礙的表示的一種非阻塞調度,他是一種樂觀鎖,他任務多個線程一起修改臨界區數據也未必會把臨界區數據修改壞,所以可以放開讓多線程都進來,一種寬進嚴出的策略。如果發現一個線程在臨界區操作遇到數據競爭,產生衝突,他就會回滾操作,進行重試,可能會出現死鎖的情況 a依賴b b依賴a 都不斷重試。

  • 無鎖,是在無障礙的前提上面加一個約束,就是保證有一個線程可以勝出的,可能存在飢餓問題。

  • 無等待,是在無鎖的前提上面加一個約束,就是保證所有線程都可以在有限步內完成。

JAVA的內存模型(JMM),由於併發程序比串行程序複雜很多,在併發程序下,數據訪問一致性和安全性該如何保證呢?所以還需要在定義一些規則,保證多線程之間可以有效地、正確地協同工作,而JMM就是為此而生的。

JMM關鍵技術點都是圍繞多線程的原子性、可見性、有序性來建立的。

關於JMM的原子性、可見性、有序性後續併發系列都會詳細解釋。

什麼是線程?

線程是進程的一個實體,是CPU調度和分派的基本單位,它是比進程更小的能獨立運行的基本單位。線程自己基本上不擁有系統資源,只擁有一點在運行中必不可少的資源(如程序計數器,一組寄存器和棧),但是它可與同屬一個進程的其他的線程共享進程所擁有的全部資源。

線程狀態轉換

  1. 新建狀態(New):新創建了一個線程對象。

  2. 就緒狀態(Runnable):線程對象創建後,其他線程調用了該對象的start()方法。該狀態的線程位於可運行線程池中,變得可運行,等待獲取CPU的使用權。

  3. 運行狀態(Running):就緒狀態的線程獲取了CPU,執行程序代碼。

  4. 阻塞狀態(Blocked):阻塞狀態是線程因為某種原因放棄CPU使用權,暫時停止運行。直到線程進入就緒狀態,才有機會轉到運行狀態。

    阻塞的情況分三種:

  • 等待阻塞:運行的線程執行wait()方法,JVM會把該線程放入等待池中。

  • 同步阻塞:運行的線程在獲取對象的同步鎖時,若該同步鎖被別的線程佔用,則JVM會把該線程放入鎖池中。

  • 其他阻塞:運行的線程執行sleep()或join()方法,或者發出了I/O請求時,JVM會把該線程置為阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入就緒狀態。

  1. 死亡狀態(Dead):線程執行完了或者因異常退出了run()方法,該線程結束生命週期。

    Java線程:創建與啟動

    在java中要想實現多線程,有兩種手段,一種是繼續Thread類,另外一種是實現Runable接口。

  2. 擴展java.lang.Thread類。此類中有個run()方法,應該注意其用法:

    public void run()

    如果該線程是使用獨立的Runnable運行對象構造的,則調用該Runnable對象的run方法;否則,該方法不執行任何操作並返回。Thread的子類應該重寫該方法。

  3. 實現java.lang.Runnable接口。

    public void run()

    使用實現接口Runnable的對象創建一個線程時,啟動該線程將導致在獨立執行的線程中調用對象的run方法。方法run的常規協定是,它可能執行任何所需的操作。

特別說明:線程創建的時候,優秀的編碼建議,指定線程的名稱,在dump線程的時候這樣不會是Thread-1這種了,而是自己取的線程名稱。

啟動線程在線程的Thread對象上調用start()方法,而不是run()或者別的方法。


線程的優先級

每一個 Java 線程都有一個優先級,這樣有助於操作系統確定線程的調度順序。

Java 線程的優先級是一個整數,其取值範圍是 1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。

默認情況下,每一個線程都會分配一個優先級 NORM_PRIORITY(5)。

具有較高優先級的線程對程序更重要,並且應該在低優先級的線程之前分配處理器資源。但是,線程優先級不能保證線程執行的順序,而且非常依賴於平臺。

## java線程常用方法詳解

1. sleep()

使當前線程(即調用該方法的線程)暫停執行一段時間,讓其他線程有機會繼續執行,但它並不釋放對象鎖。也就是說如果有synchronized同步快,其他線程仍然不能訪問共享數據。注意該方法要捕捉異常。

例如有兩個線程同時執行(沒有synchronized)一個線程優先級為MAX_PRIORITY,另一個為MIN_PRIORITY,如果沒有Sleep()方法,只有高優先級的線程執行完畢後,低優先級的線程才能夠執行;但是高優先級的線程sleep(500)後,低優先級就有機會執行了。總之,sleep()可以使低優先級的線程得到執行的機會,當然也可以讓同優先級、高優先級的線程有執行的機會。

2. join()

join()方法使調用該方法的線程在此之前執行完畢,也就是等待該方法的線程執行完畢後再往下繼續執行。注意該方法也需要捕捉異常。

3. yield()

該方法與sleep()類似,只是不能由用戶指定暫停多長時間,並且yield()方法只能讓同優先級的線程有執行的機會。

4. interrupt()

  • 如果當前線程沒有中斷它自己(這在任何情況下都是允許的),則該線程的 checkAccess 方法就會被調用,這可能拋出 SecurityException。如果線程在調用 Object 類的 wait()、wait(long) 或 wait(long, int) 方法,或者該類的 join()、join(long)、join(long, int)、sleep(long) 或 sleep(long, int) 方法過程中受阻,則其中斷狀態將被清除,它還將收到一個 InterruptedException。

  • 如果該線程在可中斷的通道上的 I/O 操作中受阻,則該通道將被關閉,該線程的中斷狀態將被設置並且該線程將收到一個 ClosedByInterruptException。

  • 如果該線程在一個 Selector 中受阻,則該線程的中斷狀態將被設置,它將立即從選擇操作返回,並可能帶有一個非零值,就好像調用了選擇器的 wakeup 方法一樣。

  • 如果以前的條件都沒有保存,則該線程的中斷狀態將被設置。中斷一個不處於活動狀態的線程不需要任何作用。

5. interrupted()

測試當前線程是否已經中斷。線程的中斷狀態 由該方法清除。換句話說,如果連續兩次調用該方法,則第二次調用將返回 false(在第一次調用已清除了其中斷狀態之後,且第二次調用檢驗完中斷狀態前,當前線程再次中斷的情況除外)。線程中斷被忽略,因為在中斷時不處於活動狀態的線程將由此返回 false 的方法反映出來。

一個線程在未正常結束之前, 被強制終止是很危險的事情. 因為它可能帶來完全預料不到的嚴重後果. 所以你看到Thread.suspend, Thread.stop等方法都被Deprecated了。

那麼不能直接把一個線程搞掛掉, 但有時候又有必要讓一個線程死掉, 或者讓它結束某種等待的狀態 該怎麼辦呢? 優雅的方法就是, 給那個線程一箇中斷信號, 讓它自己決定該怎麼辦. 比如說, 在某個子線程中為了等待一些特定條件的到來, 你調用了Thread.sleep(10000), 預期線程睡10秒之後自己醒來, 但是如果這個特定條件提前到來的話, 你怎麼通知一個在睡覺的線程呢? 又比如說, 主線程通過調用子線程的join方法阻塞自己以等待子線程結束, 但是子線程運行過程中發現自己沒辦法在短時間內結束, 於是它需要想辦法告訴主線程別等我了. 這些情況下, 就需要中斷.中斷是通過調用Thread.interrupt()方法來做的. 這個方法通過修改了被調用線程的中斷狀態來告知那個線程, 說它被中斷了. 對於非阻塞中的線程, 只是改變了中斷狀態, 即Thread.isInterrupted()將返回true; 對於可取消的阻塞狀態中的線程, 比如等待在這些函數上的線程, Thread.sleep(), Object.wait(), Thread.join(), 這個線程收到中斷信號後, 會拋出InterruptedException, 同時會把中斷狀態置回為false.

//Interrupted的經典使用代碼  public void run(){  try{  ....  while(!Thread.currentThread().isInterrupted()&& more work to do){  // do more work;  }  }catch(InterruptedException e){  // thread was interrupted during sleep or wait  }  finally{  // cleanup, if required  }  } 

很顯然,在上面代碼中,while循環有一個決定因素就是需要不停的檢查自己的中斷狀態。當外部線程調用該線程的interrupt 時,使得中斷狀態置位即變為true。這是該線程將終止循環,不在執行循環中的do more work了。這說明: interrupt中斷的是線程的某一部分業務邏輯,前提是線程需要檢查自己的中斷狀態(isInterrupted())。但是當線程被阻塞的時候,比如被Object.wait, Thread.join和Thread.sleep三種方法之一阻塞時。調用它的interrput()方法。可想而知,沒有佔用CPU運行的線程是不可能給自己的中斷狀態置位的。這就會產生一個InterruptedException異常。

/* * 如果線程被阻塞,它便不能核查共享變量,也就不能停止。這在許多情況下會發生,例如調用 * Object.wait()、ServerSocket.accept()和DatagramSocket.receive()時,他們都可能永 * 久的阻塞線程。即使發生超時,在超時期滿之前持續等待也是不可行和不適當的,所以,要使 * 用某種機制使得線程更早地退出被阻塞的狀態。很不幸運,不存在這樣一種機制對所有的情況 * 都適用,但是,根據情況不同卻可以使用特定的技術。使用Thread.interrupt()中斷線程正 * 如Example1中所描述的,Thread.interrupt()方法不會中斷一個正在運行的線程。這一方法 * 實際上完成的是,在線程受到阻塞時拋出一箇中斷信號,這樣線程就得以退出阻塞的狀態。更 * 確切的說,如果線程被Object.wait, Thread.join和Thread.sleep三種方法之一阻塞,那麼, * 它將接收到一箇中斷異常(InterruptedException),從而提早地終結被阻塞狀態。因此, * 如果線程被上述幾種方法阻塞,正確的停止線程方式是設置共享變量,並調用interrupt()(注 * 意變量應該先設置)。如果線程沒有被阻塞,這時調用interrupt()將不起作用;否則,線程就 * 將得到異常(該線程必須事先預備好處理此狀況),接著逃離阻塞狀態。在任何一種情況中,最 * 後線程都將檢查共享變量然後再停止。下面示例描述了該技術。 * */  package Concurrency.Interrupt;  class Example3 extends Thread {  volatile boolean stop = false;  public static void main(String args[]) throws Exception {  Example3 thread = new Example3();  System.out.println("Starting thread...");  thread.start();  Thread.sleep(3000);  System.out.println("Asking thread to stop...");  /* * 如果線程阻塞,將不會檢查此變量,調用interrupt之後,線程就可以儘早的終結被阻 * 塞狀 態,能夠檢查這一變量。 * */  thread.stop = true;  /* * 這一方法實際上完成的是,在線程受到阻塞時拋出一箇中斷信號,這樣線程就得以退 * 出阻 塞的狀態 * */  thread.interrupt();  Thread.sleep(3000);  System.out.println("Stopping application...");  System.exit(0);  }  public void run() {  while (!stop) {  System.out.println("Thread running...");  try {  Thread.sleep(2000);  } catch (InterruptedException e) {  // 接收到一箇中斷異常(InterruptedException),從而提早地終結被阻塞狀態  System.out.println("Thread interrupted...");  }  }  System.out.println("Thread exiting under request...");  }  }  /* * 把握幾個重點:stop變量、run方法中的sleep()、interrupt()、InterruptedException。串接起 * 來就是這個意思:當我們在run方法中調用sleep(或其他阻塞線程的方法)時,如果線程阻塞的 * 時間過長,比如10s,那在這10s內,線程阻塞,run方法不被執行,但是如果在這10s內,stop被 * 設置成true,表明要終止這個線程,但是,現在線程是阻塞的,它的run方法不能執行,自然也就 * 不能檢查stop,所 以線程不能終止,這個時候,我們就可以用interrupt()方法了:我們在 * thread.stop = true;語句後調用thread.interrupt()方法, 該方法將在線程阻塞時拋出一箇中斷 * 信號,該信號將被catch語句捕獲到,一旦捕獲到這個信號,線程就提前終結自己的阻塞狀態,這 * 樣,它就能夠 再次運行run 方法了,然後檢查到stop = true,while循環就不會再被執行,在執 * 行了while後面的清理工作之後,run方法執行完 畢,線程終止。 * */ 

當代碼調用中須要拋出一個InterruptedException, 你可以選擇把中斷狀態復位, 也可以選擇向外拋出InterruptedException, 由外層的調用者來決定。

不是所有的阻塞方法收到中斷後都可以取消阻塞狀態, 輸入和輸出流類會阻塞等待 I/O 完成,但是它們不拋出 InterruptedException,而且在被中斷的情況下也不會退出阻塞狀態。

嘗試獲取一個內部鎖的操作(進入一個 synchronized 塊)是不能被中斷的。

有關線程的同步操作,在後續章節會詳細講解到。

首先需要說說線程安全?關於線程安全一直在提,比如StringBuilder和StringBuffer有什麼區別? 經常就會出現關於線程安全與線程非安全,可能一直在提自己沒有細細想想,如果忽然問你啥是線程安全的概念?可能你需要短暫停頓幾秒,線程不安全就是不提供數據訪問保護,有可能出現多個線程先後更改數據造成所得到的數據是髒數據,其實關於線程安全的定義我想不到好的,百度了下,也沒有發現一個特別好的解釋,我就選擇一個相對來說還可以的解釋吧 ,線程安全就是多線程訪問時,採用了加鎖機制,當一個線程訪問該類的某個數據時,進行保護,其他線程不能進行訪問直到該線程讀取完,其他線程才可使用。不會出現數據不一致或者數據汙染。我覺得該描述也不完全正確,因為現在控制併發的策略很多不僅僅是加鎖機制,也可以不用加鎖,我覺得這樣可能比較合適的解釋,就是多個線程都會操作到的,是一個公共資源或者共享的數據,但是每次操作只能一個線程使用而一旦臨界區資源被佔用其他的線程必須等待該資源的釋放,在並行程序中,臨界區資源都是受保護的那麼就是線程安全,不包含的就是線程不安全的。

由於併發程序要比串行程序複雜很多,一個最重要的原因就是併發程序下訪問的一致性和安全性將會受到嚴重挑戰,如何保證一個線程可以看到正確的數據呢?因此我們需要深入瞭解並行機制的前提下,在定義一些規則來保證多線程直接有效的,正確的協同工作,而Java內存模型(JMM)就是來做這些事情的。

JMM模型都是圍繞著多線程的原子性、可見性、和有序性來說的。這塊內容過於複雜,自己水平有些,可能理解的有些偏差,希望到時候大家幫忙指出來。

原子性是指一個操作是不可中斷的,即使在多線程一起執行的時候,一個操作如果開始就不會別其他線程干擾到。原文是:

Atomicity

  • Accesses and updates to the memory cells corresponding to fields of any type except long or double are guaranteed to be atomic. This includes fields serving as references to other objects. Additionally, atomicity extends to volatile long and double. (Even though non-volatile longs and doubles are not guaranteed atomic, they are of course allowed to be.)

  • Atomicity guarantees ensure that when a non-long/double field is used in an expression, you will obtain either its initial value or some value that was written by some thread, but not some jumble of bits resulting from two or more threads both trying to write values at the same time. However, as seen below, atomicity alone does not guarantee that you will get the value most recently written by any thread. For this reason, atomicity guarantees per se normally have little impact on concurrent program design.

long型字段和double型字段在32位hotspot可能不是原子性的,該如何證明呢?(作為一個思考題後續章節會進行解答,並且附上程序說明),在64位hotspot下面long型字段和double型字段都是原子性的。如果32位hotspot下volatile long 和volatile double也具有原子性 。為什麼在32為hotspot加了volatile long型字段和double型字段字段就一定具有原型性了呢?,這與volatile的特性有關,當我們聲明共享變量為volatile後,對這個變量的讀、寫會很特別,理解volatile的好方法就是把對volatile變量的單個讀寫堪稱使用同一

鎖對這些單個讀寫操作做了同步,鎖的語義決定了臨界區代碼的執行具有原子性,所以在32為hotspot加了volatile long型字段和double型字段字段就一定具有原型性了,後續還會說volatile的,很難理解的關鍵詞。

有序性,如果不是為了優化,為了性能,一般代碼的執行順序就是我們寫的順序從先到後一行一行執行,但是為了提高性能,我們需要優化,可能就會修改這些原先的順序了,

目前的編譯器和處理器常常會對指令做重排的。

  1. 編譯器優化的重排序。編譯器在不改變單線程語義的前提下,可以重新安排語句的執行順序。

  2. 指令級並行的重排序。現代處理器採用了指令級並行技術來將多條指令重疊執行。如果不存在數據依賴性,處理器可以改變語句對應機器指令的執行順序。

  3. 內存系統的重排序。由於處理器換用緩存和讀/寫緩衝區,這使得加載和存儲操作看上去可能是在亂序中執行。上述的1屬於編譯器重排序,2和3屬於處理器重排序。

這些重排序可能會導致多線程程序出現內存可見性問題。對於編譯器,JMM的編譯器重排序規則會禁止特定類型的編譯器重排序(不是所有的編譯器重排序都要禁止)。對於處理器重排序,JMM的處理器重排序規則會要求Java編譯器在生成指令序列時,插入特定類型的內存屏障指令,通過內存屏障指令來禁止特定類型的處理器重排序。關於內存屏障後續等深入瞭解了在來聊聊。

可見性是指當一個線程修改了某個共享變量的值,其他線程是否能夠立即知道關於這個值的修改,可見性是一個複雜的綜合性的問題,有一些關於緩存優化或者硬件優化會導致可見性的問題之外,上面提到的關於指令重排也會影響到可見性問題。

還有幾個概念介紹:

happens-before簡介

從JDK 5開始,Java使用新的JSR-133內存模型(除非特別說明,本文針對的都是JSR-133內存模型)。JSR-133使用happens-before的概念來闡述操作之間的內存可見性。在JMM中,如果一個操作執行的結果需要對另一個操作可見,那麼這兩個操作之間必須要存在happens-before關係。這裡提到的兩個操作既可以是在一個線程之內,也可以是在不同線程之間。

與程序員密切相關的happens-before規則如下:

  • 程序順序規則:一個線程中的每個操作,happens-before於該線程中的任意後續操作。

  • 監視器鎖規則:對一個鎖的解鎖,happens-before於隨後對這個鎖的加鎖。

  • volatile變量規則:對一個volatile域的寫,happens-before於任意後續對這個volatile域的讀。

  • 傳遞性:如果A happens-before B,且B happens-before C,那麼A happens-before C。

    注意:兩個操作之間具有happens-before關係,並不意味著前一個操作必須要在後一個操作之前執行!happens-before僅僅要求前一個操作(執行的結果)對後一個操作可見,且前一個操作按順序排在第二個操作之前。happens-before的定義很微妙,一個happens-before規則對應於一個或多個編譯器和處理器重排序規則。對於Java程序員來說,happens-before規則簡單易懂,它避免Java程序員為了理解JMM提供的內存可見性保證而去學習複雜的重排序規則以及這些規則的具體實現方法。

數據依賴性

如果兩個操作訪問同一個變量,且這兩個操作中有一個為寫操作,此時這兩個操作之間就存在數據依賴性。數據依賴分為下列3中類型:

  1. 寫後讀,a=1;b=a;寫一個變量之後,再讀這個變量。

  2. 寫後寫,a=1;a=2;寫一個變量之後,再寫這個變量。

  3. 讀後寫,a=b;b=1;讀一個變量之後,再寫這個變量。

上面3種情況,只要重排序兩個操作的執行順序,程序的執行結果就會被改變。前面提到過,編譯器和處理器可能會對操作做重排序。編譯器和處理器在重排序時,會遵守數據依賴性,編譯器和處理器不會改變存在數據依賴關係的兩個操作的執行順序。

這裡所說的數據依賴性僅針對單個處理器中執行的指令序列和單個線程中執行的操作,不同處理器之間和不同線程之間的數據依賴性不被編譯器和處理器考慮。

as-if-serial語義

as-if-serial語義的意思是:不管怎麼重排序(編譯器和處理器為了提高並行度),(單線程)程序的執行結果不能被改變。編譯器、runtime和處理器都必須遵守as-if-serial語義。

為了遵守as-if-serial語義,編譯器和處理器不會對存在數據依賴關係的操作做重排序,因為這種重排序會改變執行結果。但是,如果操作之間不存在數據依賴關係,這些操作就可能被編譯器和處理器重排序。

as-if-serial語義把單線程程序保護了起來,遵守as-if-serial語義的編譯器、runtime和處理器共同為編寫單線程程序的程序員創建了一個幻覺:單線程程序是按程序的順序來執行的。as-if-serial語義使單線程程序員無需擔心重排序會干擾他們,也無需擔心內存可見性問題。

關於final和volatile後續章節介紹,很複雜很綜合性的知識,今天這些內容很難理解,希望對大家有所幫助。

轉:http://www.cnblogs.com/lirenzuo/category/1086094.html

1、具有1-5工作經驗的,面對目前流行的技術不知從何下手,

需要突破技術瓶頸的。2、在公司待久了,過得很安逸,

但跳槽時面試碰壁。需要在短時間內進修、跳槽拿高薪的。

3、如果沒有工作經驗,但基礎非常紮實,對java工作機制,

常用設計思想,常用java開發框架掌握熟練的。

4、覺得自己很牛B,一般需求都能搞定。

但是所學的知識點沒有系統化,很難在技術領域繼續突破的。

5. 群號:高級架構群 606187239備註好信息!

6.阿里Java高級大牛直播講解知識點,分享知識,

多年工作經驗的梳理和總結,帶著大家全面、

科學地建立自己的技術體系和技術認知!

相關推薦

推薦中...