Java高并发程序设计前言
生活随笔
收集整理的這篇文章主要介紹了
Java高并发程序设计前言
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
首先我們先介紹一些基本概念,我們為什么要使用并行,并行的大環境是什么樣子的,以及和并行相關的一些概念,最后我們會介紹兩個和性能,兩個相關的定律,要有一定的JAVA使用經驗,我們首先來探討一個非常重要的問題,為什么我們要寫并行的代碼,并行代碼之所有有這個需求呢,主要是基于兩個原因,第一個是業務的要求,比方說,我們有一個服務器,他要處理多個客戶端的請求,那么一種比較通俗的做法呢,每個客戶端使用一個線程去做,當然我們也可以使用一個線程去處理多個客戶端請求,但是這樣我們可能要處理一些客戶端之間的一個調度問題,那這樣對我們的代碼實現呢,就會有一些復雜的地方,比如說我們的java虛擬機,當我們JAVA虛擬機起來之后呢,JAVA虛擬機在后面運行了好多的線程,其中有一個是啟動我們main函數的主線程,這些線程共同維護了虛擬機的運作,之所以使用這么多線程的原因呢,是因為我們業務模塊上需要,比如及時編譯,他就是為了編譯而存在的,GC線程就是為了做GC而存在的,我們主函數main線程就是為了運行我們的代碼而存在的,如果沒有線程這個概念,我們不使用并行,我們只能在一個串行的程序當中,同時去實現一些功能,比較自然的做交替的,并不是一個執行完了,執行另外一個,而是會有交叉,如果我們要完成自己來處理,那么會是非常困難的,我們會涉及到很多的任務調度等等,那么使用線程呢,他就可以很好地,解決這個問題,調度的問題呢,交給操作系統去做,對我們應用開發來講呢,只需要簡單的處理一些,業務上的模塊就行了,因此使用并行,使用多線程,一個重要的原因呢,業務上需要一個執行單元,所以為了有這么一個執行單元呢,我們很自然的想到了線程這個問題,不用進程,進程太大,你一個進程創建和銷毀的開銷呢,比線程大的多,所以相對來講呢,所以我們就選擇更低消費的一個實體,這個是一個重要的原因,我們主要探討的是第二個原因,就是性能,使用多線程的程序呢,多核CPU的性能呢,一般是要比單線程的性能要好一些,但是對于并行程序,他是好是壞,是利大于弊,其實還是有一些探討的,比如說,大家看這個就是我們的大神
它是linux的創建人,它基于linux系統做了一些修改,來創建linux,所以像這種大神級的任務,他提出了一個什么意見呢,忘掉那該死的并行,他有這么一句話,有多么奇葩的想象力,才能想象并行計算的用武之地,言下之意呢,是說這個并行計算其實就沒有用,其中一個重要的原因呢,并行的代碼要比串行的代碼要多得多,并行的代碼是很難維護的,所以對并行也提出質疑,這個東西根本就沒有多大用處,那么事實上是不是這樣子呢,我羅在這里的技術都討論過,不排除媒體對他斷章取義,因為批評MINIX也道歉了,那么JAVA他也批判過,看到的是JAVA引擎在走下坡路,因為他別無去處,98年的時候說了這句話,2011年的時候說過,我不關心JAVA,多么可怕的語言,事實上同樣的話用在了C++上面,他說C++是門可怕的語言,他在做git的時候也是用C,并沒有選中C++,包括XML,和Solaris,他都批判過,Solaris很簡單,Solaris是linux的競爭對手,他希望他死掉,那么這個一個對技術不斷批判的一個人物,他在說并行不行的時候呢
并行計算只有在圖像處理和服務端編程可以用,并且他在這兩個領域確實有著大量的使用,但是在其他地方,并行計算毫無建樹,事實上他說這句話是很有道理的,因為圖像處理,它是屬于計算密集型的,這個領域,你單個CPU計算可能會有點累,當你需要去處理大量的計算的時候呢,能夠明顯提高系統的性能,第二個領域就是服務端編程,比如服務器,后臺在運作的一個程序,因為這些程序他往往也需要大量的處理,往往也是計算密集型的,比如你做一個數據挖掘,做一些數據分析,他也有可能像不同的服務器去請求數據,因此呢他在這兩個領域呢,JAVA語言的主要工作場景呢,也就是在服務端進行,因此呢從這個角度來看,為什么要使用并行呢,是因為業務上的原因,第二個呢是并行程序在多核CPU上,確實可以提高性能,在現在這個大環境下來說,JAVA應用到服務端編程,與并行主要執行領域是一致的,用于用戶UI的操作,并行程序,并行多線程,其實對于用戶的價值并不是很大
并行程序流行開來的重要原因呢,多核CPU的產生,為什么會有多核CPU呢,那是因為摩爾定律在單核CPU發展已經失效了,根據摩爾定律的說法呢,每18個月到24個月,就是一年半到兩年的時間,我們芯片的性能是會提高一倍的,翻一翻,但事實上是什么情況呢,在2004年因特爾就宣布徹底取消了4GHZ的計劃,在他當時的基礎上面,他要做一個主頻是4GHZ的芯片呢,他做不出來,現在已經是2015年了,現在能夠買的主頻呢,基本上也就是4GHZ多一點點,是到頂了,絕大部分還是留在3.8G,4G以下,4GZ芯片是很少的,而且賣得很貴,而且再往上發展的可能性呢,看起來也不大,從這里可以看到,10年時間過去了,CPU的主頻,基本上已經沒有太大的變化,那么按照摩爾定律,我們如果按24個月算的話,那你10年過去了,應該翻了5翻,那也應該是32倍,如果那個時候能夠達到3G,現在CPU的主頻,按照摩爾定律的說法呢,達到100G也沒問題,實際上我們離100G還有很遠,因此計算機的發展呢,實際上在這個地方已經受到了一個瓶頸,所以摩爾定律在芯片的發展呢,已經失效了,如果計算沒法提高性能,那我們應該怎么辦呢,人們就很聰明了,既然你沒有辦法提高你單個CPU的性能,那我們就在一個CPU里面呢,我塞很多的核進去,因此多核CPU就興起了,單核CPU的瓶頸之后,導致我們往多核CPU發展,這個并不是我們的選擇,而是一條沒有選擇的道路,因為我們做不出來了,預計將來68核,他也會產生,說不定現在也有,后端也有64核CPU的使用了,如果將來的CPU內核越來越多,那么很顯然并行的需求也會越來越大
那我們再來看唐納德他所說的一句話,這也是計算機中的一個頂級科學家,他說在我看來,這種現象,并發,或多或少是因為硬件設計者已經無計可施了,他們將摩爾定律的責任呢,推脫給了軟件開發者,也就是說,因為我沒有辦法提高CPU的主頻,所以那你就去用多個CPU,來做這件事情,換個角度來說,如果我們現在有10G,100G的CPU,其實并行不像現在這么重要,因為我們完全可以寫在一個串行的程序當中,他反正是運行的很快,我們也可以降低編程難度,在多核CPU上的可見性,這樣的問題產生了,但是事實上我們沒有,我們沒有這樣的CPU,所以我們不得不去做并行這件事情
這是因為,我們剛才說過的,需要并行是因為業務模型的需要
下面來看一下并行的幾個比較重要的概念,這里會介紹同步,異步,并發,并行,臨界區,阻塞和非阻塞,饑餓,活鎖,并行級別的概念
首先我們來看一下同步和異步,同步異步是對方法調用而言的,如果一個方法調用是同步的,那在這個時間軸上,我們可以看到,同步調用呢,它會等待這個方法返回,這個方法實現多久,他就等待多久,異步調用呢它會瞬間返回,異步他返回很快,異步調用返回很快,并不表示說你,這個調用就完成了,它會在后臺啟一個線程,一般都是啟一個線程,慢慢地做他的事情,所以異步調用呢,是指我一個函數調用下去,我馬上就能夠返回,但是我返回之后呢,這個程序我調用的請求呢并沒有做完,但是我不影響我繼續做我下面的事情,在我異步調用之后,還能夠做我下面的其他的事情,我異步調用的工作內容,他在另外一個線程當中,慢慢去做,那這就是異步調用和同步調用,異步調用和同步調用的含義
下面我們來看一下并發和并行的區別,一般來說呢,不需要特別區分并發和并行,他們有什么特別重要的區別,因為對我們外界看來呢,并發和并行他的外在表象,基本上是一致的,并發他的英文單詞,Concurrency,并行叫做Parallelism,這樣的程序叫做并行,兩個線程,或者兩個進程也好,他們同時在里面執行,這叫并行,什么叫并發呢,并發是指我一會做這個事情,一會做這件,他有一個調度的過程,這個叫并發,對于單個CPU來講,他不可能出現并行的情況,單個CPU他同一個時間只能做同一件事情,它是分時的在做調度,調度這所有的任務,但是對于多個CPU來講呢,他就是一個并行的,兩個CPU,每個CPU做一件事情,這兩個程序就是并行執行,但對于外在表象而言,我們看到這兩件任務都是同時執行的,所以一般來說呢,我們不用特別去關注,但是在有些場合呢,我們還是需要去確認一下,在 一般來說,這兩個概念對于我們來說呢,是一樣的
下面我們來看一下臨界區,對于多線程程序來講呢,臨界區是一個非常重要的概念,我們提到臨界區呢,就表示他是一個公共的區域,也就是說,所有的線程,他都能夠訪問這個公共的區域,所有的線程都能夠訪問他,因為所有的線程都能夠訪問他,當然多個線程去訪問這個臨界區呢,那可能會破壞掉,比如你一個人去寫數據還沒有寫完,另外一個線程又去里面去寫數據,那么著兩個數據一疊加,就可能會產生錯誤的數據,因此呢臨界區呢,它是一個需要被控制的區域,我希望多個線程進入臨界區,有可能把我數據改壞掉,所以每一次我只希望有一個線程可以進去,那當線程進去之后呢,其他線程還想進入到臨界區里面來呢,需要進入阻塞隊列進行等待,等到什么時候為止呢,等待我臨界區里的線程,釋放了這個鎖,其他可以在等待隊列當中呢,再取一個線程,這里講的是臨界區的控制方法,臨界區本身就是有共享資源,當多線程訪問的時候呢,就關注他額外不會被破壞的數據
下面我們來看一下阻塞和非阻塞,阻塞和非阻塞通常用來形容多線程間的相互影響,如果一個線程占用了臨界區,其他線程不能夠再進去,就是阻塞,因為其他線程要在臨界區之外做等待,阻塞的含義呢是指線程操作系統層面,被掛起,所以阻塞的方式呢,他一般性能不會太好,根據一般的統計呢,如果你一個線程在操作層面,把你調出去了,做了上下文切換了,所以這不是一個特別好的方法,但是是一個非常簡單的方法,所有責任都推給了操作系統,去幫我們調度,雖然說他的性能不高,能把這件事情做得很好,阻塞它會有一個問題,如果說我有個線程,一直占著不釋放資源,其他要使用臨界區的資源呢,都會不能工作,都會阻在那邊,那么非阻塞是指允許很多線程同時進入臨界區,那這個就是非阻塞,我一個線程進入臨界區之后,我該線程也能夠進去,我只要保證不把數據改壞,就可以了
下面我們再來看一下死鎖,節饑餓和活鎖,對于阻塞的程序來講,如果是進入臨界區,就有可能發生死鎖的現象,比如這四輛小車,就發生了死鎖,每輛小車所占的一條路呢,我們認為是一個資源,A車把這條路給堵了,B車把這條路給堵了,C車把這條路堵了,D車把這條路堵了,但是A需要這條路,C需要這條路,D需要這條路,B需要這條路,這樣就導致ABCD,沒有人能夠繼續往下走,除非我們把其中一個給停止掉,就是把D車挪走,C車就能走,B車也能走,A車也能走,這就是死鎖,死鎖就是使得整個程序卡死在里邊,不再提供服務了,如果我們系統出現了這個問題呢,死鎖雖然說不是一個好現象,但是死鎖它是一個靜態的一個問題,也就是說一旦發生死鎖呢,他不會占用CPU,所以這是一個靜態的問題,相對來講還是比較好分析的,與死鎖相對應的還有一個活鎖,活鎖是什么意思呢,我們舉個例子,比方說,我們電梯門開了,電梯門開了之后呢,里面的人要出來,假設電梯里有一個人,電梯外也有一個人,電梯里面的那個人呢,要出來,電梯外面那個人要進去,那有可能碰到的問題是說,你面對面撞在了一起,你們兩個人把路都堵了,那你可能要避讓他,所以你就往左邊考了一靠,想避開他,他的想法是跟你一樣的了,他也避開你,他就往右邊靠了一靠避開你,因為你們兩個是面對面的,所以你往左邊靠,他往右邊靠的時候呢,你們兩個又把路給堵上了,然而你這個時候發現不對,所以你又往右邊靠,他發現不對,他呢再往左邊靠,然后你們又把路給堵上了,與此往復,不停的兩個人,一會往左一會往右,不停的把這個路給堵上,作為人來講,你發現這種情況呢,我們會停下來,眼神交流一下,或者我們停下來不動,讓另外一個人先過,這個問題又解決了,因為人是有智力的,但是如果這個情況發生在程序上,發生在線程上面呢,你就不會這么走運了,一個線程如果他搶占到資源之后,他發現另外的資源沒有辦法拿到,這個時候他為了避免死鎖,如果死鎖產生的一個必要條件,你搶占了資源而不釋放,如果你搶占了資源而釋放了呢,就沒有死鎖這種說法,因為他沒有辦法拿到所有的資源,因此他把自己釋放的那塊資源也給釋放掉,而這個時候另外一個線程,做了同樣的事情,他們需要相同的資源,比如他們都需要資源A,一個線程搶了資源A,另外一個資源搶了資源B,然而他們發現都沒法工作,把資源同時釋放掉,釋放了之后呢,他們一看,資源A和資源B都出來了,然后一個線程拿了資源B,另外一個線程拿了資源A,然后一看,我又沒法工作了,然后他們又互相謙讓,又把這個資源放出去,與此反復,這個資源在多個線程之間跳來跳去,活鎖一旦發生,這個問題就比死鎖更難查,因為它是一個動態的一個問題,并不是像死鎖那樣,現在就進在里面不動了,線程不停的在動,不停的在重試,但是永遠不能夠成功,或者說要花很長的時間才能夠成功,性能也會受到很大的影響,那就是活鎖,那么有關饑餓呢,因為某種原因,比如你這個線程優先級可能很低,別的線程優先級很高,所以在調度的時候呢,就調度不到你,結果操作系統只調度優先級高的線程,因為我優先級很低,所以我就調度不到,我就不能繼續往下執行,這個時候我就會餓死,因為我分不到足夠的資源,那另外的一種情況呢,就比如我們在競爭的時候呢,我競爭這個數據,原子修改,做這個原子操作,我總是失敗,總是失敗,那么這個時候我也有可能會被餓死,因為我不能往下再走了,這個是多線程當中,可能會碰到的一些問題
下面我們再來看幾個重要的概念,有關并發的概念,一般并發呢,阻塞和非阻塞,這個概念我們之前就有講過,對于非阻塞來講呢,我們可以進一步分為無障礙的,無鎖,無等待,下面我們就這幾個級別來做一個講解
下面我們就對這幾個級別來做一個簡單的了解,阻塞就是指一個線程進入臨界區以后呢,其他線程就必須在這個臨界區之外進行等待,等這個線程出來之后呢,他們才能夠進去執行
第二個就是無障礙,無障礙是非阻塞,他并不要求一個線程進入臨界區之后,其他線程在外面等待,線程是可以自如的進入臨界區,那么跟阻塞相比呢,認為他是一種悲觀的策略,它會認為說,大家一起修改這個數據呢,很有可能把這個數據改壞的,所以每次我只能允許一個人去修改,而這個非阻塞調度呢,相對來講比較樂觀,如果大家一起修改,也未必把這個數據改壞,所以我可以放開讓大家都進來,但是它是一種寬進嚴出的策略,進的時候所有線程都能夠進入臨界區,包括讀也好,寫也好,但是你出來的時候呢,就不一定了,如果他發現線程,臨界區的操作呢,跟別人產生了沖突,那么他就會回滾這條數據,比如我們要去讀取一對坐標,x,y,坐標系統去讀取x,y,他先讀X,再讀Y,當他讀到Y的時候,他發現有其他的線程,改了我這個X,這個時候他就會認為,我再把這個Y讀出來是沒有用的,我可能會讀到一個錯誤的數據,所以他就會重試,再去重新的讀取數據,直到自己讀到的X,Y呢,是沒有問題的,所以他是一個不斷重試的一種策略,所有的線程都相當于在,拿去一個系統當前的快照,他們會一直嘗試讀取到的快照,有效地為止
第二個級別是無鎖,我們剛剛說的無障礙呢,他是說,我所有的線程都進入臨界區,但是如果發生了競爭,他并不保證臨界區當中的線程能夠順利的出來,因為如果他發現自己的數據呢,每次讀取或者每次操作,總是跟別人產生了沖突,他就不停的嘗試不停的嘗試,如果有10個線程,線程1他修改了以后,改了部分數據,結果他被線程2干擾了,線程2被線程3干擾了,依次類推,線程2又干擾了線程10,如果他們之間都是彼此干擾的,最終會導致所有的線程呢,系統的性能會受到比較大的影響,必須在無障礙的上面呢,加上一個約束,無鎖他必須首先是無障礙的,也就是說你所有的線程呢,都必須能夠進入這個臨界區,但是無鎖他加了一個限制條件是說,我保證在我這一次競爭當中,有一個線程是必然能勝出的,那這樣他就能夠保證說,臨界區當中的線程呢,至少有一個是能夠順利走出去的,而不至于所有的全在里面陣亡掉,如果至少有一個線程能夠出去,假設里面有100個線程,第一個線程競爭順利,我們走出了臨界區,其他99個再競爭,因為每次競爭肯定保證有一個能夠勝利,我們再出去一個,剩下98個再競爭,使得你這個系統,至少是能夠順暢的進行下去,那這就是無鎖,下面這段代碼是比較典型的在JAVA當中,無鎖計算的這么一段代碼,在后面我們無鎖的程序呢,會在我們后續的課程當中呢,感覺鋪天蓋地就是無鎖的計算
我們下面再來看一下無等待,無鎖剛才已經講了,它是說,我保證每一次至少有一個線程,它是能夠在有限步當中完成操作,那么其他的線程呢,在不停的競爭,知道有一個線程為止,那無等待呢,他就更近一步,所以無等待他要求你首先無鎖,也就是他保證你能進,他要求所有的線程都能夠在有限步上完成,這個要求提的很高,意味著說,你任何線程都能夠進入進去,都能夠無障礙的進入臨界區,并且呢你任何線程呢,都能夠在若干步當中,有限步當中,離開臨界區,這就會使得你系統的運行呢,會變得非常的順暢,那么無等待可以說是并行最高級別的了,基本上是可以讓每個系統發揮到最好的效率,那么無等待他必然是無饑餓的,因為你所有的線程,都能夠有限步的完成,因此你不會永久的出不去,所以他一定是無饑餓的,那無等待的一個典型案例呢,就比方說,我們有讀寫兩個線程,如果說我的讀線程,只有讀線程,沒有寫線程,那所有的讀線程之間呢,必然是無等待的,原因很簡單,因為你所有的進程進來,不能進,因為你讀你不會修改數據,所以數據都是一致的,所有的讀都是無等待的,但是說如果你有一個寫在里面,那么由于寫線程會有修改數據呢,所以我們可以提供一種算法,比如有一種算法會這樣做,因為我寫會影響到讀,所以我在每次寫之前呢,我把數據全部先拷貝一份副本,我僅拿到一份副本,然后我修改這個副本,而不是修改原始數據,修改數據的過程呢,可能需要一點時間,但是由于我修改的是我副本的數據,而不是你的原始數據,所以這個修改的過程,也不影響線程沖突,因此在這種情況下呢,讀線程一樣是無等待的,他們都能在有限步中完成操作,所有的寫線程,因為每個線程也都是寫的副本,他們的寫也是無等待的,他們都不需要去做同步,最后需要做同步的是什么呢,只是將寫完之后的數據呢,再回寫到覆蓋原始數據的,而覆蓋原始數據的,是非常非常快的,因為我們不需要做大量的操作,或者是一個指針,或者是一個引用,做一個替換而已,那不管哪個寫線程勝出,總是能夠保證,這個替換上去的數據呢,是一致的,并不會像其他的寫的算法一樣,我可能會把自己寫壞,因為大家寫的只是副本,所以必然是安全的,這種方式呢,這個就是等待的一個典型的一個實現,當然無等待的實現是比較麻煩的,而且他有一些技巧性的東西,相對來講呢,無鎖的使用呢,會更加的廣泛一些
阿姆達爾他還有一個公式,N個處理器時的,這個加速比,等于一個處理器的加速比,這個公式在這里,處理器的個數是n,我使用N個處理器的時間,是我使用一個處理器的時間,乘上,程序的串行化比例,有多少程序是串行化的,F是串行程序的比重,1-F是并行程序的比重,大家可以看到,我串行程序的比例,加上并行的比例,我并行程序除以n,就是我并行所消耗的時間,加上我串行所消耗的時間,你乘以我原本一個CPU所消耗的時間,就是我n個CPU所消耗的時間,加速比呢,就是我優化前的時候,除以我優化后的時候,我們也可以把這個例子帶到這個公式里面去,阿姆達爾告訴我們一個什么問題呢,他就是說,增加處理器的個數,并不能是我們的系統加速比提高,比如我們在這個地方,如果我們的n很大,如果你增加處理器的個數n很大,如果你n很大,但是如果你F很小,導致的結果呢,你這個數字是一個很小的數字,加速比并不會上升,那我們就合理的調整我們的F,串行化比例,和n之間的關系呢,才能夠使得加速比比較好,就是你增加CPU是沒有用個,如果你程序總體上,串行F總是接近1,下面我們來看一下古斯塔夫森定律,他是類似的
他也是說明處理器個數,串行比例,加速比之間的個數,他這么講的,他說,程序的執行時間呢,可以分為兩部分,串行時間和并行時間,那程序執行需要花多少時間呢,串行的部分,加上并行的部分,總的執行時間呢,a加上n乘以b,n是處理器的個數,那這個總時間呢,我串行執行的時候,總的時間,a加b呢,指的就是,因此呢我的加速比呢,加速前的時間,除以加速后的時間,F是串行在程序中的比例,加速比跟CPU數量的n,看來是有一個直接的線性關系,當我這個F很小的時候,就是串行比例很小,并行比例很高的時候,基本上加速比,CPU的數量呢,基本上成一個線性的關系,基本上強調是說,只要你能夠把這個程序,足夠的并行化,那加速比就會和你CPU成一個正比,你CPU個數越多,你加速比就越高,從這個角度說明,他們加速比的角度呢,是不同的,說明你光加CPU,是沒有用的,你必須提高串行化比例才行,總和這兩個定律呢,其實我們的結論是說,你要想讓這個程序,通過串行,通過多核,來提高你的性能,那你要做的是兩件事,第一處理好你的n,第二處理好你的F
?
超強干貨來襲 云風專訪:近40年碼齡,通宵達旦的技術人生總結
以上是生活随笔為你收集整理的Java高并发程序设计前言的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Redisson分布式锁实战-2:解决w
- 下一篇: 无锁-1