Java源碼解析——volatile

1 定義及作用

1.1英文釋義

1.2百度百科

1.3維基百科

譯:

當被用於修飾變量時,Java volatile關鍵字可以保證:

       (1)在所有版本的Java中,對volatile關鍵字修飾的變量的讀寫存在全局排序。這意味着每個訪問volatile修飾字段的線程都                  會去讀取字段的當前值,而不是繼續使用(可能存在地)字段的緩存值。(但是,並不能保證 讀寫volatile修飾字段與                  讀寫常規字段 之間的相對順序,這意味着它通常不是有用的線程結構。)

       (2)在Java 5或更高版本中,對volatile關鍵字修飾字段的讀和寫建立了happens-before原則,非常像獲取和釋放互斥鎖一                    樣。這個原則僅僅提供保證:一個特定語句的寫內存操作對另一個特定語句是可見的。

        volatile屬性是線性化的。讀取易volatile修飾的字段就像獲取鎖:將導致工作內存(參考後面Java內存模型小節)中緩存的變量副本無效,從內存中重新讀取該字段的當前值。對volatile修飾字段的寫就像釋放鎖:修改後的值將被寫回內存。

1.4官方釋義


譯:Java編程語言中一個被用於修飾變量的關鍵詞,被volatile修飾的變量具有被同時運行的線程異步修改的特性。

        綜上所述,volatile是Java(此處只針對java)編程語言中預定義的關鍵字,它被用作修飾java成員變量(屬性),在java多線程環境中,能保證代碼(代碼最終會被編譯器編譯爲機器操作指令)執行的有序性和操作結果在內存中的可見性。

 

2 背景知識補充

       爲更好地理解java volatile關鍵字的含義以及深層原理,本節將補充介紹以下知識:爲更好地理解java volatile關鍵字的含義以及深層原理,本節將補充介紹以下知識:

(1)從操作系統層面,介紹線程的實現方式以及實現細節;

(2)從Java層面,介紹java線程創建和啓動細節;

(3)計算機內存模型和Java內存模型;

       讀完(1)小節,對於操作系統如何實現線程會有一個大致地瞭解。第(2)小節,將通過跟蹤源碼的方式,向你展示java線程的創建以及啓動過程,並結合第(1)的內容,得到一個重要結論:java線程和操作系統的內核線程是一一映射的關係,即“用戶級線程與內核控制線程的連接”小節中的一對一模型。第(3)小節將首先詳細介紹計算機內存模型,包括如下內容:CPU結構,CPU緩存,CPU緩存一致性協議MESI,爲提升CPU性能而引入的StoreBuffer和InvalidQueue數據結構,引入StoreBuffer和InvalidQueue對CPU緩存一致性帶來的問題以及通過內存屏障方式帶來的解決方案。

 

2.1操作系統---線程實現方式及線程實現

2.1.1線程實現方式

        線程已在許多系統中實現,但各系統的實現方式並不完全相同。在有的系統中,特別是一些數據庫管理系統如Infomix,所實現的是用戶級線程(UserLevel Threads);而另一些系統(如Macintosh和OS/2 操作系統)所實現的是內核支持(KernelSupported Threads); 還有一些系統如Solaris操作系統,則同時實現了這兩種類型的線程。

2.1.11內核支持線程

       對於通常的進程,無論是系統進程還是用戶進程,進程的創建、撤消,以及要求由系統設備完成的I/O 操作,都是利用系統調用而進入內核,再由內核中的相應處理程序予以完成的。進程的切換同樣是在內核的支持下實現的。因此我們說,不論什麼進程,它們都是在操作系統內核的支持下運行的,是與內核緊密相關的。這裏所謂的內核支持線程KST(Kernel Supported Threads),也都同樣是在內核的支持下運行的,即無論是用戶進程中的線程,還是系統進程中的線程,他們的創建、撤消和切換
等也是依靠內核,在內核空間實現的。此外,在內核空間還爲每一個內核支持線程設置了一個線程控制塊,內核是根據該控制塊而感知某線程的存在,並對其加以控制。

       這種線程實現方式主要有如下四個優點:

       (1) 在多處理器系統中,內核能夠同時調度同一進程中多個線程並行執行;
       (2) 如果進程中的一個線程被阻塞了,內核可以調度該進程中的其它線程佔有處理器運行,也可以運行其它進程中的線程;
       (3) 內核支持線程具有很小的數據結構和堆棧,線程的切換比較快,切換開銷小;
       (4) 內核本身也可以採用多線程技術,可以提高系統的執行速度和效率。

       內核支持線程的主要缺點是:對於用戶的線程切換而言,其模式切換的開銷較大,在同一個進程中,從一個線程切換到另一個線程時,需要從用戶態轉到內核態進行,這是因爲用戶進程的線程在用戶態運行,而線程調度和管理是在內核實現的,系統開銷較大。

2.1.12用戶級線程

        用戶級線程ULT(User Level Threads)僅存在於用戶空間中。對於這種線程的創建、撤消、線程之間的同步與通信等功能,都無須利用系統調用來實現。對於用戶級線程的切換,通常發生在一個應用進程的諸多線程之間,這時,也同樣無須內核的支持。由於切換的規則遠比進程調度和切換的規則簡單,因而使線程的切換速度特別快。可見,這種線程是與內核無關的。我們可以爲一個應用程序建立多個用戶級線程。在一個系統中的用戶級線程的數目可以達到數百個至數千個。由於這些線程的任務控制塊都是設置在用戶空間,而線程所執行的操作也無須內核的幫助,因而內核完全不知道用戶級線程的存在。

        值得說明的是,對於設置了用戶級線程的系統,其調度仍是以進程爲單位進行的。在採用輪轉調度算法時,各個進程輪流執行一個時間片,這對諸進程而言似乎是公平的。但假如在進程A中包含了一個用戶級線程,而在另一個進程B中含有100 個用戶級線程,這樣,進程A中線程的運行時間將是進程B中各線程運行時間的100倍;相應地,其速度要
快上100 倍。

        假如系統中設置的是內核支持線程,則調度便是以線程爲單位進行的。在採用輪轉法調度時,是各個線程輪流執行一個時間片。同樣假定進程A 中只有一個內核支持線程,而在進程B中有100 個內核支持線程。此時進程B可以獲得的CPU時間是進程A的100倍,且進程B可使100 個系統調用併發工作。

        使用用戶級線程方式有許多優點,主要表現在如下三個方面:

        (1) 線程切換不需要轉換到內核空間,對一個進程而言,其所有線程的管理數據結構均在該進程的用戶空間中,管理線程切換的線程庫也在用戶地址空間運行。因此,進程不必切換到內核方式來做線程管理,從而節省了模式切換的開銷,也節省了內核的寶貴資源。
        (2) 調度算法可以是進程專用的。在不干擾操作系統調度的情況下,不同的進程可以根據自身需要,選擇不同的調度算法對自己的線程進行管理和調度,而與操作系統的低級調度算法是無關的。
        (3) 用戶級線程的實現與操作系統平臺無關,因爲對於線程管理的代碼是在用戶程序內的,屬於用戶程序的一部分,所有的應用程序都可以對之進行共享。因此,用戶級線程甚至可以在不支持線程機制的操作系統平臺上實現。

        用戶級線程實現方式的主要缺點在於如下兩個方面:

        (1) 系統調用的阻塞問題。在基於進程機制的操作系統中,大多數系統調用將阻塞進程,因此,當線程執行一個系統調用時,不僅該線程被阻塞,而且進程內的所有線程都會被阻塞。而在內核支持線程方式中,則進程中的其它線程仍然可以運行。
        (2) 在單純的用戶級線程實現方式中,多線程應用不能利用多處理機進行多重處理的優點。內核每次分配給一個進程的僅有一個CPU,因此進程中僅有一個線程能執行,在該線程放棄CPU之前,其它線程只能等待。

2.1.13組合方式

        有些操作系統把用戶級線程和內核支持線程兩種方式進行組合,提供了組合方式ULT/KST 線程。在組合方式線程系統中,內核支持多KST線程的建立、調度和管理,同時,也允許用戶應用程序建立、調度和管理用戶級線程。一些內核支持線程對應多個用戶級線程,程序員可按應用需要和機器配置對內核支持線程數目進行調整,以達到較好的效果。組合方式線程中,同一個進程內的多個線程可以同時在多處理器上並行執行,而且在阻塞一個線程時,並不需要將整個進程阻塞。所以,組合方式多線程機制能夠結合KST和ULT兩者的優點,並克服了其各自的不足。

2.1.2線程實現

        不論是進程還是線程,都必須直接或間接地取得內核的支持。由於內核支持線程可以直接利用系統調用爲它服務,故線程的控制相當簡單;而用戶級線程必須藉助於某種形式的中間系統的幫助方能取得內核的服務,故在對線程的控制上要稍複雜些。

2.1.21內核支持線程的實現

       在僅設置了內核支持線程的OS中,一種可能的線程控制方法是,系統在創建一個新進程時,便爲它分配一個任務數據區PTDA(Per Task Data Area),其中包括若干個線程控制塊TCB空間,如圖2-15所示。在每一個TCB中可保存線程標識符、優先級、線程運行的CPU狀態等信息。雖然這些信息與用戶級線程TCB中的信息相同,但現在卻是被保存在內核空間中。

                                                                      

       每當進程要創建一個線程時,便爲新線程分配一個TCB,將有關信息填入該TCB中,併爲之分配必要的資源,如爲線程分配數百至數千個字節的棧空間和局部存儲區,於是新創建的線程便有條件立即執行。當PTDA
中的所有TCB 空間已用完,而進程又要創建新的線程時,只要其所創建的線程數目未超過系統的允許值(通常爲數十至數百個),系統可再爲之分配新的TCB空間;在撤消一個線程時,也應回收該線程的所有資源和TCB。可見,內核支持線程的創建、撤消均與進程的相類似。在有的系統中爲了減少創建和撤消一個線程時的開銷,在撤消一個線程時,並不立即回收該線程的資源和TCB,當以後再要創建一個新線程時,便可直接利用已被撤消但仍保持有資源和TCB的線程作爲新線程。

       內核支持線程的調度和切換與進程的調度和切換十分相似,也分搶佔式方式和非搶佔方式兩種。在線程的調度算法上,同樣可採用時間片輪轉法、優先權算法等。當線程調度選中一個線程後,便將處理機分配給它。當然,線程在調度和切換上所花費的開銷,要比進程的小得多。

2.1.22用戶級線程的實現

       用戶級線程是在用戶空間實現的。所有的用戶級線程都具有相同的結構,它們都運行在一箇中間系統的上面。當前有兩種方式實現的中間系統,即運行時系統和內核控制線程。

       1) 運行時系統(Runtime System)

       所謂“運行時系統”,實質上是用於管理和控制線程的函數(過程)的集合,其中包括用於創建和撤消線程的函數、線程同步和通信的函數以及實現線程調度的函數等。正因爲有這些函數,才能使用戶級線程與內核無關。運行時系統中的所有函數都駐留在用戶空間,並作爲用戶級線程與內核之間的接口。

       在傳統的OS中,進程在切換時必須先由用戶態轉爲核心態,再由核心來執行切換任務;而用戶級線程在切換時則不需轉入核心態,而是由運行時系統中的線程切換過程來執行切換任務。該過程將線程的CPU狀態保存在該線程的堆棧中,然後按照一定的算法選擇一個處於就緒狀態的新線程運行,將新線程堆棧中的CPU狀態裝入到CPU相應的寄存器中,一旦將棧指針和程序計數器切換後,便開始了新線程的運行。由於用戶級線程的切換無需進入內核,且切換操作簡單,因而使用戶級線程的切換速度非常快。

       不論在傳統的OS 中,還是在多線程OS 中,系統資源都是由內核管理的。在傳統的OS中,進程是利用OS 提供的系統調用來請求系統資源的,系統調用通過軟中斷(如trap)機制進入OS內核,由內核來完成相應資源的分配。用戶級線程是不能利用系統調用的。當線程需要系統資源時,是將該要求傳送給運行時系統,由後者通過相應的系統調用來獲得系統資源的。

       2) 內核控制線程

       這種線程又稱爲輕型進程LWP(Light Weight Process)。每一個進程都可擁有多個LWP,同用戶級線程一樣,每個LWP都有自己的數據結構(如TCB),其中包括線程標識符、優先級、狀態,另外還有棧和局部存儲區等。它們也可以共享進程所擁有的資源。LWP 可通過系統調用來獲得內核提供的服務,這樣,當一個用戶級線程運行時,只要將它連接到一個LWP上,此時它便具有了內核支持線程的所有屬性。這種線程實現方式就是組合方式。

       在一個系統中的用戶級線程數量可能很大,爲了節省系統開銷,不可能設置太多的LWP,而把這些LWP 做成一個緩衝池,稱爲“線程池”。用戶進程中的任一用戶線程都可以連接到LWP池中的任何一個LWP上。爲使每一用戶級線程都能利用LWP與內核通信,可以使多個用戶級線程多路複用一個LWP,但只有當前連接到LWP上的線程才能與內核通信,其餘進程或者阻塞,或者等待LWP。而每一個LWP都要連接到一個內核級線程上,這樣,通過LWP可把用戶級線程與內核線程連接起來,用戶級線程可通過LWP來訪問內核,但內核所看到的總是多個LWP 而看不到用戶級線程。亦即,由LWP 實現了在內核與用戶級線程之間的隔離,從而使用戶級線程與內核無關。圖2-16 示出了利用輕型進程作爲中間系統時用戶級線程的實現方法。

                                                

       當用戶級線程不需要與內核通信時,並不需要LWP;而當要通信時,便需藉助於LWP,而且每個要通信的用戶級線程都需要一個LWP。例如,在一個任務中,如果同時有5 個用戶級線程發出了對文件的讀、寫請求,這就需要有5 個LWP 來予以幫助,即由LWP 將對文件的讀、寫請求發送給相應的內核級線程,再由後者執行具體的讀、寫操作。如果一個任務中只有4 個LWP,則只能有4 個用戶級線程的讀、寫請求被傳送給內核線程,餘下的一個用戶級線程必須等待。

       在內核級線程執行操作時,如果發生阻塞,則與之相連接的多個LWP也將隨之阻塞,進而使連接到LWP上的用戶級線程也被阻塞。如果進程中只包含了一個LWP,此時進程也應阻塞。這種情況與前述的傳統OS一樣,在進程執行系統調用時,該進程實際上是阻塞的。但如果在一個進程中含有多個LWP,則當一個LWP阻塞時,進程中的另一個LWP可繼續執行;即使進程中的所有LWP全部阻塞,進程中的線程也仍然能繼續執行,只是不能再去訪問內核。

2.1.23用戶級線程與內核控制線程的連接

       實際上,在不同的操作系統中,實現用戶級線程與內核控制線程的連接有三種不同的模型:一對一模型、多對一模型和多對多模型。

       1) 一對一模型

       該模型是爲每一個用戶線程都設置一個內核控制線程與之連接,當一個線程阻塞時,允許調度另一個線程運行。在多處理機系統中,則有多個線程並行執行。該模型並行能力較強,但每創建一個用戶線程相應地就需要創建一個內核線程,開銷較大,因此需要限制整個系統的線程數。Windows 2000、Windows NT、OS/2 等系統上都實現了該模型。

       2) 多對一模型

       該模型是將多個用戶線程映射到一個內核控制線程,爲了管理方便,這些用戶線程一般屬於一個進程,運行在該進程的用戶空間,對這些線程的調度和管理也是在該進程的用戶空間中完成。當用戶線程需要訪問內核時,纔將其映射到一個內核控制線程上,但每次只允許一個線程進行映射。該模型的主要優點是線程管理的開銷小,效率高,但當一個線程在訪問內核時發生阻塞,則整個進程都會被阻塞,而且在多處理機系統中,一個進程的多個線程無法實現並行。

       3) 多對多模型

       該模型結合上述兩種模型的優點,將多個用戶線程映射到多個內核控制線程,內核控制線程的數目可以根據應用進程和系統的不同而變化,可以比用戶線程少,也可以與之相同。

 

2.2Java線程創建和啓動細節

       本小節內容將通過源碼追蹤的方式,瞭解java線程的創建以及啓動過程。使用源碼版本爲openjdk-jdk8-b120,可從Github獲取所用源碼(https://github.com/unofficial-openjdk/openjdk/tags?after=jdk9-b09)。

       接下來以一段java線程代碼開始我們的分析:

         先分析上圖中第12行標紅的代碼,該行代碼是在調用Thread類的構造函數,打開相應的構造器一探究竟,如下圖所示:

翻譯:449-453行,分配一個新的Thread對象。此構造器和下圖中的構造函數具有相同效果,其中“gname”是一個新生成的名稱。自動生成的名稱以“Thread-n”的形式存在,n是一個整數。456-458行:當這個線程開始時,target對象的run方法將被調用。如果run方法體爲空,那這個類方法什麼也不做。

 

       可以看到461行調用了Thread內部的init()方法,具體代碼如下圖:

翻譯:用當前訪問控制上下文對線程進行初始化。

       查看341行代碼詳情,如下:

/**
     * Initializes a Thread.   // 初始化一個線程
     * 
     * @param g the Thread group    //參數g:線程組
     * @param target the object whose run() method gets called   //參數target:該對象的run()方法會被調用
     * @param name the name of the new Thread    //參數name:新線程的名稱
     * @param stackSize the desired stack size for the new thread, or
     *        zero to indicate that this parameter is to be ignored.   //參數stackSize:新線程期望的棧大小,或者爲0表明這個參數被忽略
     * @param acc the AccessControlContext to inherit, or    //參數acc:要繼承的訪問控制上下文,如果不對該參數傳值,就使用AccessController.getContext()對相應屬性賦值
     *            AccessController.getContext() if null
     */
    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc) {
		
		//對線程名字進行校驗,不允許爲空
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

		//對線程名進行賦值
        this.name = name.toCharArray();

		//爲保證後面g.checkAccess()中g不爲空,需要在此進行參數校驗,如果g==null,則需要對其賦值
        Thread parent = currentThread();
        SecurityManager security = System.getSecurityManager();
        if (g == null) {
            /* Determine if it's an applet or not */

			//如果security已經存在,那麼調用security.getThreadGroup()對參數g賦值
            if (security != null) {
                g = security.getThreadGroup();
            }

			//如通過security.getThreadGroup()進行賦值而參數g仍舊爲空,那就使用父線程的線程組進行賦值
            if (g == null) {
                g = parent.getThreadGroup();
            }
        }

		//判斷ThreadGroup(g)是否爲rootGroup,是的話,則需要校驗其修改線程組的權限
        g.checkAccess();

        //檢查是否有所需要的權限(SUBCLASS_IMPLEMENTATION_PERMISSION)
        if (security != null) {
            if (isCCLOverridden(getClass())) {
                security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
            }
        }

		//增加線程組中未開始線程的數量
        g.addUnstarted();

		//爲線程對象的各個屬性賦值
        this.group = g;
        this.daemon = parent.isDaemon();
        this.priority = parent.getPriority();
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;
        this.inheritedAccessControlContext =
                acc != null ? acc : AccessController.getContext();
        this.target = target;
        setPriority(priority);
        if (parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

        /* Set thread ID */
        tid = nextThreadID();
    }

        構造函數(new Thread(new Runnable...))的調用鏈路到此就結束了,過程中並沒有出現操作系統線程接口相關的調用,那Java線程到底是如何與操作系統內核線程對應起來的呢?答案就在下圖中第19行代碼,接下來讓我們跟蹤th.start()調用鏈路。

        start()方法的源碼以及解釋如下所示:

/**
     * Causes this thread to begin execution; the Java Virtual Machine
     * calls the <code>run</code> method of this thread.    //導致這個線程開始執行; java虛擬機調用本線程的run方法
     * <p>
     * The result is that two threads are running concurrently: the
     * current thread (which returns from the call to the
     * <code>start</code> method) and the other thread (which executes its
     * <code>run</code> method).                            //結果是兩個線程同時運行:當前線程(在調用start方法後返回),另一個線程(執行它的run方法)
     * <p>
     * It is never legal to start a thread more than once.  //對一個線程執行超過1次的start操作都是不允許的
     * In particular, a thread may not be restarted once it has completed
     * execution.                                           //尤其是,一旦線程執行完成它就不可能重新啓動
     *
     * @exception  IllegalThreadStateException  if the thread was already
     *               started.                               //如果線程已經啓動,調用此方法會拋出 IllegalThreadStateException
     * @see        #run()
     * @see        #stop()
     */
    public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *                                                  //VM創建/設置的主方法線程或“系統”組線程,不調用此方法。將來添加到此方法中的任何新功能可能也必須添加到VM。
         * A zero status value corresponds to state "NEW".  //狀態值爲0的話相當於線程狀態爲”NEW“
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
															//注意啓動線程時,該線程所屬線程組的threads列表應相應地增加,爲啓動線程列表的size也要遞減
        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */	//不做任何事情。如果start0方法拋出異常,該異常會被向上傳遞到調用棧
            }
        }
    }

        繼續跟蹤start0()方法,如下圖所示,發現此處是採用JNI調用。關於java代碼如何調用在系統本地採用其他語言實現的功能接口,可參考JNI傳送門,此處不再贅述。

        接下來我們找到start0()方法的native實現,找到對應的Thread.c文件,在openjdk源碼中的如下位置:

       由於後續的調用鏈路相當長且繁瑣,所以筆者通過Xmind樹結構的形式給出該方法後續的調用鏈路,對每一個文件所涉及的方法及源碼都有相應說明,詳情如下圖:

       通過對源碼的追蹤和分析,可以得出以下3個結論:第一,java線程在其構造器被調用時,並不會真正創建與系統內核線程與之映射,在執行start()方法的過程中,C++層面線程以及系統平臺層面的線程纔會被真正創建;第二,java線程與系統內核線程之間是一一對應的關係,具體證據請仔細閱讀上圖;第三,結合2.1節中“用戶級線程與內核控制線程的連接”板塊,我們知道Java用戶級線程與內核控制線程的連接模型爲一對一模型。

 

2.3計算機內存模型和Java內存模型

2.3.1計算機內存模型

https://www.cnblogs.com/adinosaur/p/6243605.html

https://blog.csdn.net/gupao123456/article/details/81221641

http://www.importnew.com/10589.html

 

https://blog.csdn.net/yhb1047818384/article/details/79604976

https://www.cnblogs.com/gtarcoder/p/5295281.html

http://g.oswego.edu/dl/jmm/cookbook.html

https://blog.csdn.net/gupao123456/article/details/81221641(java內存模型以及計算機內存模型)

2.3.2Java內存模型

http://www.importnew.com/10589.html(深入java內存模型)

http://www.importnew.com/29860.html(內存屏障和volatile語義)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

5 參考資料

《計算機操作系統》

 

 

 

 

 

 

 

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