多線程並發之java記憶體模型JMM,多線程java模型jmm
多線程概念的引入是人類又一次有效壓寨電腦的體現,而且這也是非常有必要的,因為一般運算過程中涉及到資料的讀取,例如從磁碟、其他系統、資料庫等,CPU的運算速度與資料讀取速度有一個嚴重的不平衡,期間如果按一條線程執行將會在很多節點產生阻塞,使計算效率低下。另外,伺服器端是java最擅長的領域,作為伺服器必須要能同時響應多個用戶端的請求,同樣需要多線程的支援。在多線程情況下,高並發將帶來資料的共用與競爭問題,tomcat作為中介軟體將多線程並發等細節盡量封裝起來處理,使使用者對多線程透明,更多地關注業務處理即可。但如果想要深入剖析tomcat我們有必要深入瞭解多線程並發技術,此章節將介紹多線程並發的一些底層原理及JDK並發包的實現的相關知識,並不會對所有具體的並發類的使用方法進行介紹,針對一些在Tomcat中使用的類會稍微詳細闡述。
1、Java記憶體模型——JMM
在多核時代,如何提高CPU的效能成為了一個永恒的話題,而這個話題的討論主要就是如何定義一個高效能的記憶體模型,記憶體模型用於定義處理器的各層緩衝與共用記憶體的同步機制及線程和記憶體互動的規則。
Java的世界也有屬於它自己的記憶體模型,Java記憶體模型,即Java Memory Model,簡稱JMM。由於Java被定義成一種跨平台的語言,所以在記憶體的描述上面也要能是跨平台的,Java虛擬機器試圖定義一種統一的記憶體模型,能將各種底層硬體及作業系統的記憶體訪問差異進行封裝,使Java程式在不同硬體及作業系統上都能達到相同的並發效果。它描述了程式中各個變數之間的關係,包括執行個體域、靜態域、資料元素及在實際電腦系統中將變數儲存到記憶體和從記憶體中取出變數的底層細節。
為更好理解JMM的工作機制,由圖2-5-1-1帶入,從整體上看有幾個比較重要的主體,主存、工作記憶體、變數、變數副本、線程等。首先看主存與工作記憶體及他們的關係,主存儲存了java程式的所有變數,當然這個變數不包括局部變數和方法參數,而工作記憶體則包含了這些變數的副本;其次是線程與工作記憶體的關係,每個線程都有一個屬於自己的工作記憶體,不同線程之間的工作記憶體是互相不可見的,且線程對變數的操作也只能是針對自己的工作記憶體;最後是關於線程之間的通訊機制,由於線程直接不可直接傳遞,假如一條線程對一個變數進行重新賦值,那麼只能通過如下途徑讓另外一條線程知道,線程一將變數改變反應到主存中,線程二再從主存中讀取,這樣就基本完成了線程之間的通訊了。
JMM定義額八個操作來完成工作記憶體與主存的通訊。假如一條線程準備對一個變數進行新的賦值操作,它可能會先用lock操作鎖住主存中的某個變數(不讓其他線程獲得此變數的鎖,直至使用unlock操作釋放該變數的鎖),接著使用read操作將變數從主存獨到工作記憶體,緊接著load操作將得到的變數值放到工作記憶體中的變數副本,use操作則將變數值傳給線程執行引擎進行運算操作,assign操作把新的變數值從線程執行引擎中傳遞到工作記憶體,繼續往下,store操作則把變數值從工作記憶體傳送到主存中,接著write操作將得到的值寫入主存相應的變數中,最後使用unlock操作釋放變數的鎖。
圖2-5-1-1 java記憶體模型JMM
Java記憶體模型具有三個特性:原子性、可見度和有序性。
① 原子性,java記憶體模型保證了read、load、assign、use、store、write六個操作具有原子性,我們可以認為除了long和double類型外,對其他基礎資料型別 (Elementary Data Type)所對應的記憶體單元的訪問讀寫都是原子的。但由於這個原子性的顆粒度太小,通常情況下我們需要更大顆粒度的原子性,這時就需要用鎖來保證了。
② 可見度,在java記憶體模型中,簡單說如果一條線程更改了共用變數的值,而其他線程能馬上知道這個更改,我們則說這個變數具有可見度。一般來說有四種方式能保證變數的可見度,分別為volatile、synchronized、final和鎖。首先談談volatile,被此關鍵詞聲明的變數,每當有任何更改時都將立即同步到主存中,而每個線程要使用這個變數時都要重新從主存重新整理到工作記憶體,這樣就確保了變數的可見度(當然,普通變數最終也會同步到主存,再由主存同步到每個線程的工作記憶體,只是這個最終可能比較“長久”,不能保證可見度);由於synchronized底層也是通過鎖進行實現,所以synchronized和鎖的本質是一樣的,當一個線程釋放一個鎖時,將會強制重新整理工作記憶體中的變數值到主存中,而當另一個線程擷取此鎖的時候將會強制重新裝載此變數值,當然這兩個線程擷取的是同一個鎖,這樣就保證了變數的可見度;被final聲明的變數一旦完成初始化,其他線程就能看到這個final變數。其實,可見度其實可以看成是一種機制,線程在進入/退出同步塊程式時,它將發送/接收一個變數的更改。
③ 有序性,有序性指線上程內看方法的執行,所有的指令都是有序的,都按照一種串列方式執行,而線上程內觀察其他線程,所有指令都是無序的,指令都可能交叉執行。Java中提供了volatile和synchronized兩個關鍵詞保證線程之間操作的有序性,而這個有序性僅僅是相對的,volatile禁止指令重排序,synchronized則保證持有同一個鎖的同步塊只能串列運行。
JMM可以說是Java的基礎,它的定義將直接影響JVM及java多線程實現的機制,要想深入瞭解多線程並發中的相關問題現象,對Java記憶體模型的深入研究是必不可少的。它的定義必須考慮下面幾個方面,其一是如何更加有效地提高線程的效能效率;其二是如何將底層物理硬體及作業系統的差異屏蔽掉提供統一的對外概念;最後是如何使它的模型既嚴謹又寬鬆,保證語義不會產生歧義和一些最佳化擴充。
java多線程中比較全面的方法與功可以注釋,有執行個體是最好的了,
淺談java記憶體模型
不同的平台,記憶體模型是不一樣的,但是jvm的記憶體模型規範是統一的。java的多線程並發問題最終都會反映在java的記憶體模型上,所謂安全執行緒無非要控制多個線程對某個資源的有序訪問或修改。java的記憶體模型,要解決兩個主要的問題:可見度和有序性。我們都知道電腦有快取的存在,處理器並不是每次處理資料都是取記憶體的。JVM定義了自己的記憶體模型,屏蔽了底層平台記憶體管理細節,對於java開發人員,要解決的是在jvm記憶體模型的基礎上,如何解決多線程的可見度和有序性。
那麼,何謂可見度? 多個線程之間是不能互相傳遞資料通訊的,它們之間的溝通只能通過共用變數來進行。Java記憶體模型(JMM)規定了jvm有主記憶體,主記憶體是多個線程共用的。當new一個對象的時候,也是被分配在主記憶體中,每個線程都有自己的工作記憶體,工作記憶體儲存了主存的某些對象的副本,當然線程的工作記憶體大小是有限制的。當線程操作某個對象時,執行順序如下:
(1) 從主存複製變數到當前工作記憶體 (read and load)
(2) 執行代碼,改變共用變數值 (use and assign)
(3) 用工作記憶體資料重新整理主存相關內容 (store and write) JVM規範定義了線程對主存的操作指令:read,load,use,assign,store,write。當一個共用便變數在多個線程的工作記憶體中都有副本時,如果一個線程修改了這個共用變數,那麼其他線程應該能夠看到這個被修改後的值,這就是多線程的可見度問題。
那麼,什麼是有序性呢 ?線程在引用變數時不能直接從主記憶體中引用,如果線程工作記憶體中沒有該變數,則會從主記憶體中拷貝一個副本到工作記憶體中,這個過程為read-load,完成後線程會引用該副本。當同一線程再度引用該欄位時,有可能重新從主存中擷取變數副本(read-load-use),也有可能直接引用原來的副本 (use),也就是說 read,load,use順序可以由JVM實現系統決定。
線程不能直接為主存中中欄位賦值,它會將值指定給工作記憶體中的變數副本(assign),完成後這個變數副本會同步到主儲存區(store- write),至於何時同步過去,根據JVM實現系統決定.有該欄位,則會從主記憶體中將該欄位賦值到工作記憶體中,這個過程為read-load,完成後線程會引用該變數副本,當同一線程多次重複對欄位賦值時,比如:
for(int i=0;i<10;i++)
a++;
線程有可能只對工作記憶體中的副本進行賦值,只到最後一次賦值後才同步到主儲存區,所以assign,store,weite順序可以由JVM實現系統決定。假設有一個共用變數x,線程a執行x=x+1。從上面的描述中可以知道x=x+1並不是一個原子操作,它的執行過程如下:
1 從主存中讀取變數x副本到工作記憶體
2 給x加1
3 將x加1後的值寫回主 存
如果另外一個線程b執行x=x-1,執行過程如下:
1 從主存中讀取變數x副本到工作記憶體
2 給x減1
3 將x減1後的值寫回主存
那麼顯然,最終的x的值是不可靠的。假設x現在為10,線程a加1,線程b減1,從表面上看,似乎最終x還是為10,但是多線程情況下會有這種情況發生:
1:線程a從主存讀取x副本到工作記憶體,工作記憶體中x值為10
2:線程b從主存讀取x副本到工作記憶體,工作記憶體中x值為10
3:線程a將工作記憶體中x加1,工作記憶體中x值為11
4:線程a將......餘下全文>>
java多線程並發的問題
回答這個問題需要先弄清楚線程的概念和線程的生命週期。
線程:是指程式碼的一次執行,是動態過程。樓主在定義OneTh這個實現Runnable介面類的時候肯定複寫了他的run()方法。onet1和onet2是兩個線程,也就是說雖然他們的run()方法相同,但是是執行了兩次的。
電腦中CPU的調度過程:現在的電腦看上去能同時實現多任務,像是一邊上QQ,一邊聽音樂,還可以一邊上網。但電腦中的CPU只有一個,它沒有分身術,不可能真正意義上實現同時運行這麼多程式。而是採用了一種時間片輪轉的方式,為每個應用程式賦予極短的時間,然後高速的在不同的程式間切換,至於每次切換到那個程式,這個要由CPU和線程的優先順序來決定。
線程的生命週期:建立時是初始化了這個線程,調用start方法時,是讓這個線程進入了可運行狀態,注意是可運行,不是正在運行。就像上面說的,在某一時刻CPU具體要運行誰是由CPU和線程的優先順序決定的。當線程被CPU運行時,就會開始執行run方法,但可能執行到一半時,CPU又被其他可運行線程搶走,而只能暫停執行。
JAVA程式線程的運行:在我們使用java命令來運行程式時,這時候已經開始了兩個線程,一個是main()方法的線程,一個是記憶體回收的線程。當樓主調用start方法開啟另外兩個線程時。這時候由於CPU來決定運行哪個線程。所以雖然noet1是先開啟的,但在執行noet1時,CPU可能又去跑去執行main線程了,然後就會開啟onet2.
還有我覺得主線程結束了,只不過其他兩個線程仍在繼續運行。所以會列印出結果。
樓主如果還有什麼不明白的話可以繼續問或者相互討論。