Java內存模型與多線程

Java內存模型與多線程

1、Java內存模型

1.1內存模型

提到java內存模型則需要先了解什麼是內存模型。其實內存模型在大部分高級語言中都是有的,它主要記錄程序在處理、執行程序時時如何分配內存,如何管理變量、函數、方法等程序內容的。

計算機的三大核心部件是CPU、內存、外存。內存的主要作用就是存放CPU的運算結果,並與外部設備,如磁盤等進行數據交換。計算機的所有運輸操作都由運算器完成,但運輸的結果不能直接交給外部設備,這些結果就會存到內存中。

內存是CPU可以直接尋址的部件,提供了比較充足的資源用於存儲。內存可以看成一個很長的盒子,每個位置都可以存放一個變量,變量的長度爲一個字節,內從的容量就是可以存放的字節數。如何合理分配這些內存則是一個比較重要的問題。這就是所謂的內存模型。操作系統也會有自己對應的內存模型,而對於一門語言而言,也是會有其對應的內存模型,例如C語言有其對應的內存模型,模型規定了一個程序的源代碼、函數、變量等各保存在哪些位置,有一個比較完整和高效的內存模型,可以提高程序的執行效率。

內存的最小單位是字節,每個字節是8個bit,這與操作系統的位數無關。這裏多提一句,操作系統的位數實際上是設定CPU一次運算的處理能力。簡單說,32位CPU的輸入可以有32個bit位,一次可以處理32位(以及32位以內)的int運算,而64位則可以支持最大64位的int運算(long long)。由於內存是可以由CPU直接尋址得到的,所以32位的系統一次尋址的最大範圍就是0~2^32-1,所以32位操作系統的最大內存數是4G,當然如果CPU的部分尋址位被其他硬件設備佔用,則能夠支持的容量會更小(所以看到有些32位系統最大都是3G左右的內存)。而64位系統最大是2^64-1,就目前的技術,是完全足夠使用的。

 

1.2 Java內存模型

Java與C語言不同的是它是一個跨平臺語言,可以使編譯後的Java字節碼不再依賴操作系統和編譯器。但實際上,字節碼的運行還是極度依賴平臺下的JVM的。JVM規範定義了JAVA內存模型,來屏蔽掉各種操作系統、虛擬機實現廠商和硬件的內存訪問差異,以實現JAVA程序在所有操作系統和平臺上能夠實現一次編寫、到處運行的效果。

JVM將內存分爲主內存和工作內存,主內存在線程之間共享,所有變量存儲在主內存中。每個線程有自己獨立的工作內存。它保存被該線程使用的變量的主內存拷貝,所有對主內存變量的操作,其實都是在自己的工作內存中進行,而不能直接操作主內存中變量或其他變量副本,線程間的變量訪問通過主內存完成。

JAVA內存模型定義了八種操作來完成主內存和工作內存的變量訪問,具體如下:

lock:主內存變量,把一個變量標識爲某個線程獨佔的狀態;

unlock:主內存變量,把一個處於鎖定狀態變量釋放出來,被釋放後的變量纔可以被其它線程鎖定;

read:主內存變量,把一個變量的值從主內存傳輸到線程的工作內存中,以便隨後的load動作使用;

load:工作內存變量,把read讀取到的主內存中的變量值放入工作內存的變量拷貝中;

use:工作內存變量,把工作內存中變量的值傳遞給java虛擬機執行引擎,每當虛擬機遇到一個需要使用到變量值的字節碼指令時將會執行該操作;

assign:工作內存變量,把從執行引擎接收到的變量的值賦值給工作變量,每當虛擬機遇到一個給變量賦值的字節碼時將會執行該操作;

store:工作內存變量,把工作內存中一個變量的值傳送到主內存中,以便隨後的write操作使用;

write:主內存變量,把store操作從工作內存中得到的變量值放入主內存的變量中。

1.3重排序

在執行程序時爲了提高性能,編譯器和處理器常常會對指令做重排序。重排序分三種類型:

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

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

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

從java源代碼到最終實際執行的指令序列,會分別經歷上述三種重排序。

上述的1屬於編譯器重排序,2和3屬於處理器重排序。這些重排序都可能會導致多線程程序出現內存可見性問題。對於編譯器,JMM的編譯器重排序規則會禁止特定類型的編譯器重排序(不是所有的編譯器重排序都要禁止)。對於處理器重排序,JMM的處理器重排序規則會要求java編譯器在生成指令序列時,插入特定類型的內存屏障(memory barriers,intel稱之爲memory fence)指令,通過內存屏障指令來禁止特定類型的處理器重排序(不是所有的處理器重排序都要禁止)。

JMM屬於語言級的內存模型,它確保在不同的編譯器和不同的處理器平臺之上,通過禁止特定類型的編譯器重排序和處理器重排序,爲程序員提供一致的內存可見性保證。

1.4 volatile關鍵字

volatile是一個類型修飾符,它是被設計用來修飾被不同線程併發訪問和改變的變量。作爲關鍵字,volatile確保本條指令不會被編譯器優化。很多時候,編譯器會自動對一些看上去比較笨的代碼進行優化,而多線程中,很多看似笨重實際卻很關鍵的語句會被莫名優化導致bug。

當一個變量被volatile修飾後,它將具備兩種特性:

1. 線程可見性:當一個線程修改了被volatile修飾的變量後,無論是否加鎖,其它線程都可以立即看到最新的修改,而普通變量卻做不到這點;

2. 禁止指令重排序優化,普通的變量僅僅保證在該方法的執行過程中所有依賴賦值結果的地方都能獲取正確的結果,而不能保證變量賦值操作的順序與程序代碼的執行順序一致。舉個簡單的例子說明下指令重排序優化問題:

package jpbirdy.thread;
 
import java.util.concurrent.TimeUnit;
 
/**
 * Created by jpbirdy on14-11-5.
 */
public class VolatileTest
{
 
    private static booleanstop ;
 
    public static voidmain(String[] args) throws InterruptedException
    {
        Thread work = newThread(new Runnable()
        {
            @Override
            public void run()
            {
                int i = 0;
                while(!stop)
                {
                    i++;
                    try
                    {
                       TimeUnit.SECONDS.sleep(i);
                    }
                    catch(InterruptedException e)
                    {
                       e.printStackTrace();
                    }
                }
            }
        });
        work.start();
       TimeUnit.SECONDS.sleep(3);
        stop = true;
    }
}


上述代碼希望能夠在3s後停止線程。但實際上並不能停止。實際上JVM對語句進行了重排序,將while語句重寫爲:

if(!stop) while(true){……}

1.5 CAS指令和原子類

互斥同步最主要的問題就是進行線程阻塞和喚醒所帶來的性能問題,因此這種同步被稱爲阻塞同步,它屬於一種悲觀的併發策略,我們稱之爲悲觀鎖。隨着硬件和操作系統指令集的發展和優化,產生了非阻塞同步,被稱爲樂觀鎖。簡單的說就是先進行操作,操作完成之後再判斷下看看操作是否成功,是否有併發問題,如果有進行失敗補償,如果沒有就算操作成功,這樣就從根本上避免了同步鎖的弊端。

目前,在JAVA中應用最廣泛的非阻塞同步就是CAS,在IA64、X86指令集中通過cmpxchg指令完成CAS功能,在sparc-TSO中由case指令完成,在ARM和PowerPC架構下,需要使用一對Idrex/strex指令完成。

從JDK1.5以後,可以使用CAS操作,該操作由sun.misc.Unsafe類裏面的compareAndSwapInt()和compareAndSwapLong()等方法包裝提供。通常情況下sun.misc.Unsafe類對於開發者是不可見的,因此,JDK提供了很多CAS包裝類簡化開發者的使用,例如AtomicInteger等。

使用JAVA自帶的Atomic原子類,可以避免同步鎖帶來的併發訪問性能降低的問題,減少犯錯的機會,減少了鎖的應用,降低了頻繁使用同步鎖帶來的性能下降。

2、Java多線程


併發的實現可以通過多種方式來實現,例如:單進程-單線程模型,通過在一臺服務器上啓多個進程實現多任務的並行處理。但是在JAVA語言中,通過是通過單進程-多線程的模型進行多任務的併發處理。因此,我們有必要熟悉一下JAVA的線程。

大家都知道,線程是比進程更輕量級的調度執行單元,它可以把進程的資源分配和調度執行分開,各個線程可以共享內存、IO等操作系統資源,但是又能夠被操作系統發的內核線程或者進程執行。各線程可以獨立的啓動、運行和停止,實現任務的解耦。

主流的操作系統都提供了線程實現,目前實現線程的方式主要有三種,分別是:

1.  內核線程(KLT)實現,這種線程由內核來完成線程切換,內核通過線程調度器對線程進行調度,並負責將線程任務映射到不同的處理器上;

2.  用戶線程實現(UT),通常情況下,用戶線程指的是完全建立在用戶空間線程庫上的線程,用戶線程的創建、啓動、運行、銷燬和切換完全在用戶態中完成,不需要內核的幫助,因此執行性能更高;

3.  混合實現:將內核線程和用戶線程混合在一起使用的方式。

由於虛擬機規範並沒有強制規定JAVA的線程必須使用哪種方式實現,因此,不同的操作系統實現的方式也可能存在差異。對於SUN的JDK,在Windows和Linux操作系統上採用了內核線程的實現方式,在Solaris版本的JDK中,提供了一些專有的虛擬機線程參數,用於設置使用哪種線程模型。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章