【Java高併發系列】之走進併發世界

你們好,我是小菜,一個渴望在互聯網行業作到蔡不菜的小菜。可柔可剛,點贊則柔,白嫖則剛! 死鬼~看完記得給我來個三連哦!緩存

本文主要介紹 Java並行的入門安全

若有須要,能夠參考markdown

若有幫助,不忘 點贊多線程

忘掉那該死的並行

在2014年末的 Avoiding ping pong論壇上,Linus Torvalds 提出了一個大相徑庭的觀點,他說:「忘掉那該死的並行吧!」併發

(原文: Give it up . The whole "parallel computing is the future" is a bunch of crock)app

看到這個消息,忽然內心一緊,還沒記住就要我忘記豈不美滋滋異步

可是想要作到不菜的小蔡,發現事情並不簡單~ 開發中咱們都想用多線程來處理程序,難道不是爲了讓程序變快嗎,這TM讓我爲難了呀!函數

什麼是並行呢?

並行程序會比串行程序更容易適應業務需求學習

簡單來說就是:一家三口,你去上學,老媽在家幹家務,老爸上班賺錢。在同一個時間段,三我的在作不一樣的事情,讓生活變得更加美滿。若是是串行的狀況,就是一我的要身兼多職,一我的幹三我的的活,你說這可咋整。優化

專業來說就是:Java虛擬機是很忙的,除了要執行 main 函數主線程外,還要作 JIT 編譯,垃圾回收等待。那這些事情在虛擬機內部都是單獨的一個線程,一塊兒操做,每一個任務相互獨立,更容易理解和維護。

忘掉是不可能忘掉的,先不說我還有沒有記住,那麼不忘掉就要更努力的使用好它,來吧,牽着小菜的手,我們一塊兒征服它!

幾個重要概念

同步(Synchronous)和異步(Asynchronous)

同步異步 一般用來形容一次方法的調用。

同步和異步方法調用
同步和異步方法調用

同步: 同步方法調用一旦開始,調用者必須等到方法執行結束,才能繼續後續的行爲。

異步:異步方法就像是一個消息傳遞,一旦開始,方法調用就會當即返回,調用者就能夠繼續後續的操做。執行方法一般會在另一個線程中執行,不會阻礙到調用者的工做。

簡單來說就是:同步的話就是你去車站買票,必須排隊等待,排到的時候才能進行買票,而後去作其餘事情。異步的話就是你能夠在網上買票,完成支付後,你的票也到手了,期間你也能夠作其餘事情。

併發(Concurrency)和並行(Parallelism)

併發並行 是兩個特別容易混淆的概念。

並行和併發
並行和併發

並行:是真正意義上的多個任務 「同時執行」

併發:多個任務交替執行,多個任務之間可能仍是串行的。

實際開發中:若是系統內只有一個 CPU,這個時候使用多進程或者多線程執行任務,那麼這些任務不多是真實並行的,而是併發,採用時間片輪轉的方式。

臨界區

臨界區 是用來表示一種公共資源或者是一種共享數據,能夠被多個線程共同使用。可是每一次只能有一個線程使用它,一旦臨界區資源被佔用,其餘線程想要使用這個線程就必需要等待。

簡單來說就是:有一臺打印機,打印機一次只能執行一個任務,若是兩我的同時要使用打印機,那麼 A 同窗只能等 B 同窗使用完打印機,才能打印本身的材料。

在並行程序中,臨界區資源就是要保護的對象。

阻塞(Blocking)和非阻塞(Non-Blocking)

阻塞非阻塞 用來形容多線程間的相互影響。

阻塞:A 同窗佔用了打印機,B 同窗想要使用打印機就必需要等待 A 同窗使用完成後才能使用打印機。若是 A 同窗一直佔用着打印機不願讓別人用,那麼就會致使其餘同窗沒法正常工做。

非阻塞:A 同窗佔用了打印機,可是妨礙不到 B 同窗的正常工做,B 同窗能夠去作其餘事情。

死鎖(DeadLock)、飢餓(Starvation)和活鎖(LiveLock)

死鎖
死鎖

死鎖:如圖上四個線程相互等待,構成環形。他們彼此之間都不肯意釋放本身擁有的資源,那麼這個狀態將永遠持續下去,誰都不可能出圈。

飢餓:A 同窗在食堂窗口打飯,B 同窗在後面排隊,這個時候來了 C、D...好幾個同窗直接插隊在了 B 同窗的後面,後續若是有同窗來繼續在 B 同窗前面插隊,這樣致使的結果就是 B 同窗永遠打不到飯,那麼就會出現飢餓的現象。此外,若是某一個線程一直佔着關鍵資源不放,致使其餘須要這個資源的線程沒法正常執行,這種狀況也是飢餓的一種。

活鎖:一條走廊上,A 同窗想要經過,迎面走來了 B 同窗,可是很不巧的是兩個同窗相互擋住,這時候 A 同窗往右邊讓路,B 同窗也往右邊讓路,A 同窗又往左邊讓路,B 同窗也往左邊讓路,反覆後,最終仍是會讓出一條路。可是兩個線程碰見這種狀況,就沒有人類那麼智能,它們會相互堵上,資源在兩個線程間不停的跳動,致使沒有一個線程能夠拿到資源,這就是活鎖的狀況。

併發級別

併發級別 能夠分爲:

  • 阻塞

當一個線程是阻塞的時候,在其餘線程釋放資源以前,當前線程沒法繼續執行。例如使用synchronized或者重入鎖以前,咱們獲得的就是阻塞的線程。

  • 無飢餓

若是線程之間存在優先級,那麼線程調度的時候總會傾向於高優先級的線程,也就是不公平的。

非公平鎖和公平鎖
非公平鎖和公平鎖

非公平鎖:系統容許高優先級的線程插隊,這樣有可能會致使低優先級線程產生飢餓。

公平鎖:按照先來後到的順序,無論新來的優先級多高,就必須排隊,那麼飢餓就不會產生。

  • 無障礙

無障礙是一種最弱的非阻塞調度。兩個線程若是無障礙地執行,不會由於臨界區的問題致使一方掛起。

若是說阻塞悲觀策略,那麼非阻塞 就是 樂觀策略。無障礙的多線程程序並不是可以順利執行,若是臨界區資源嚴重衝突的時候,那麼全部線程都會回滾本身的操做,致使沒有一個線程可以走出臨界區。

可使用 CAS(Compare And Set) 策略來實現無障礙的可行性。設置一個 一致性標誌,線程在操做以前,先讀取並保存這個標誌,操做完成後,再次讀取這個標誌,判斷是否被修改,若是是一致則說明資源訪問沒有衝突。若是不一致,則說明資源可能在操做過程當中與其餘線程發生衝突,須要重試操做。

所以,任何線程對資源有操做的過程當中,都應該更新這個一致性標誌,表示數據再也不安全。

  • 無鎖

無鎖的並行都是無障礙的。在無鎖的狀況下,任何線程都能對臨界區進行訪問,不一樣的是,無鎖的併發保證必然有一個線程可以在有限步內完成操做離開臨界區

  • 無等待

無鎖只要求一個線程可以在有限步內完成操做離開臨界區,而無等待則在無鎖的基礎上更進一步擴展,它要求全部的線程都必須在有限步內完成

一種典型的無等待結構就是RCU(Read Copy Update),它的基本思想是,在讀取的時候能夠不加控制,在寫數據的時候,先取得原始數據的副本,修改完成後,再寫回數據

JMM(Java Memory Model)

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

原子性(Atomicity)

原子性是指一個操做是不可中斷的。即便是在多個線程一塊兒執行的時候,一個操做一旦開始,就不會被其餘線程干擾。

簡單來說就是:有一個靜態全局變量 i ,兩個線程同時對它賦值,A 線程給它賦值爲 1,B 線程給它賦值爲 2,那麼無論以任何方式操做,i 的值不是 1 就是 2,兩個線程之間是沒有任何干擾的。

注意:若是使用的是 long 類型而不是 int 類型,可能就會出現問題。由於long類型的讀寫不是原子性的(long類型有64位)

可見性(Visibility)

可見性是指一個線程修改了某一個共享變量的值時,其餘線程可以當即知道這個值發生修改。可見性問題對於串行的系統是不存在的,由於你在任何一個操做步驟中修改了某個變量,後續的步驟中讀到的必定是修改後的變量。

可見性問題
可見性問題

兩個線程共享變量,因爲編譯器優化或硬件優化的緣故,B 線程將變量作了優化,將其設置在了緩存cache中或寄存器中,這個時候若是 A 線程對變量進行了修改,那麼 B 線程將沒法意識到這個改動,依然會讀取存儲在緩存中的舊值

有序性(Ordering)

對於一個線程的執行代碼而言,咱們老是習慣性地任務代碼是從前日後依次執行的。固然,這是針對於整個程序只有一個線程的狀況下。在多線程的狀況下,程序在執行的時候可能會出現亂序,也就是說寫在前面的代碼,會在後面執行。這是由於程序執行時會進行指令重排,重排後的指令與原指令的順序未必一致。

若是 A 線程首先執行了 writer()方法,緊接着 B 線程執行了 reader()方法,這個時候發生指令重排,那麼 B 線程在執行i = a + 1的時候就不能看到a = 1了。

指令重排
指令重排

這裏須要注意的是: 對於一個線程來講,它看到的指令執行順序必定是一致的(不然應用根本沒法正常工做)。指令重排的前提就是:保證串行語義的一致性

哪些指令不能重排(Happen-Before規則)

  • 程序順序原則:一個線程內保證語義的串行性
  • volatile規則:volatile變量的寫先於讀發生,這保證了 volatile 變量的可見性
  • 鎖規則:解鎖(unlock)必然發生在隨後的加鎖(lock)前
  • 傳遞性:A 先 於 B,B 先於 C,那麼 A 必然先於 C
  • 線程的 start() 方法先於它的每個動做
  • 線程的中斷 interput() 先於被中斷線程的代碼
  • 對象的構造函數的執行,結束先於 finalize() 方法
看完不讚,都是壞蛋
看完不讚,都是壞蛋

今天的你多努力一點,明天的你就能少說一句求人的話!

我是小菜,一個和你一塊兒學習的男人。 💋

相關文章
相關標籤/搜索