深入理解 Jvm 讀書筆記(三)

Jvm 併發相關

知識包括:

  • java內存模型jmm
    • 內存間操作符
    • volatile關鍵字
    • 先行發生原則
  • java與線程
  • java線程調度
    • thread的5種狀態
  • 線程安全和鎖優化
    • 線程安全的實現方法
  • 鎖優化介紹

高效併發

由於計算機的存儲設備和處理器的運算速度有幾個數量級的差距,所以加入一層讀寫速度竟可能能接近處理器運算速度的高速緩存(Cache)來作爲內存和處理器之間的緩衝: 將運算需要使用的數據複製到緩存中,讓運算能快速進行;當運算結束後再從緩存同步到內存中,這樣處理器 就無須等待緩慢的內存讀寫了;

同是帶來一個問題 : 緩存一致性(Cache Coherence): 每個處理器都有自己的高速緩存,而他們又共享同一主內存(Main Memory);
在這裏插入圖片描述

所以各個處理器訪問緩存時都要遵循一些協議,如MSI,MESI等; 內存模型:可以理解爲在特定的操作協議下,對特定的內存或高速緩存進行讀寫訪問的過程抽象; 不同架構的物理機器擁有不同的內存模式;

java 內存模型 java memory model JMM

  • 主內存與工作內存

    • 主要目標: 定義程序中各個變量的訪問規則,即在jvm中將變量存儲在內存和從內存中取出變量的底層細節; 此處的變量包括 實例字段,靜態字段和構成數組對象的元素等存在競爭關係的,不包括局部變量和方法參數,因爲後者是線程私有的;
    • jmm規定所有變量都存儲在主內存(Main Memory)中,每條線程還有自己的工作內存(Working Memory),線程的工作內存保存了被該線程使用到的變量的主內存副本拷貝,線程對變量的所有操作(讀取,賦值)都必須在工作內存中進行,而不能直接讀寫主內存的變量;不同的線程間也不能直接訪問對方工作內存中的變量,線程間變量值得傳遞都需要通過主內存來完成;
      在這裏插入圖片描述
  • 內存間交互操作

    • jmm定義8中操作完成,保證每一種操作都是原子的,不可在分的(對於double,long類型的變量,load,store,read,write操作在某些平臺允許有例外)
      • lock 鎖定,作用於主內存的變量,把一個變量標識爲一條線程獨佔的狀態;
      • unlock 解鎖,作用於主內存的變量,把一個處於鎖定狀態的變量釋放出來,釋放後的變量纔可以被其他線程鎖定;
      • read 讀取, 作用於主內存的變量,把一個變量的值從主內存傳輸到線程的工作內存中,以便隨後的load動作使用;
      • load 載入,作用於工作內存的變量,把read操作從主內存中得到的變量值放入工作內存的變量副本中;
      • use 使用,作用於工作內存的變量,把工作內存中一個變量的值傳遞給執行引擎,每當jvm遇到一個需要使用到變量的值的字節碼指令時將會執行這個操作;
      • assign 賦值,作用於工作內存的變量,把一個從執行引擎接受到的值賦給工作內存的變量,每當jvm遇到一個給變量賦值的字節碼指令時執行這個操作;
      • store 存儲,作用於工作內存的變量,把工作內存中一個變量的值傳送到主內存中,以便隨後的write操作使用;
      • write 寫入,作用於主內存的變量,把store操作從工作內存中得到的變量的值放入主內存的變量中;
    • 其中必須要滿足的規則:
      • 其中,read load ,store write 必須順序成對執行;
      • 不允許一個線程丟棄它的最近的assgin操作,變量在工作內存改變了之後必須把改變化同步會主內存;
      • 沒有發生assgin操作,不允許一個線程無原因的把數據同步回主內存;
      • 一個新的變量只能在主內存中誕生,即對一個變量實施use,store操作之前,必須先執行過assgin和load操作;
      • 一個變量在同一時刻只允許一條線程對其進行lock操作,但lock操作可以被同一線程重複執行多次,並執行同樣多的unlock才能解鎖;
      • 如果對一個變量執行lock操作,那將會清空工作內存中此變量的值,在執行引擎使用這個變量前,需要重新執行load或assgin操作初始化變量的值;
      • 如果一個變量事先沒有被lock操作鎖定,那就不允許對它執行unlock操作,不允許去unlock一個別其他線程鎖定住的變量;
      • 對一個變量執行unlock操作之前,必須先把此變量同步會主內存中(執行store,write操作)
  • volatile

    • jvm提供的最輕量級的同步機制;一個變量被定義爲volatile後,保證此變量對所有線程的可見性,指當一條線程修改了這個變量的值,新值對於其他線程來說是可以立即得知的;保證有序性,內存屏障禁止重排序;
    • volatile變量的第一個語義爲可見性,volatile變量值保證可見性,在不符合下列兩種規則的運算場景中,仍然要通過加鎖(synchronized 或java.util.concurrent中的原子類)來保證原子性;
      • 運算結果並不依賴變量的當前值,或者能夠確保只有單一的線程修改變量的值;
      • 變量不需要與其他的狀態變量共同參與不變約束;
    • 使用volatile變量的第二個語義是禁止指令重排序優化,即保證有序性;
      • 字節碼指令中多了一個lock,lock作用: 提供一個內存屏障(Memory Barrier 或Memory Fence,指令重排序時不能把後面的指令重排序到內存屏障之前的位置;) lock 使得本cpu的cache寫入內存,該寫入動作也會引起別的cpu或者別的內核無效化(Invalidate)其Cache,相當於對Cache中的變量做了一個jmm中的store和write操作;
    • i++的分析 併發混亂分析
      • getstatic 取字段值; iconst_1 將一個int型常量加載到操作數棧; iadd 加; putstatic 回值; return 記錄返回值; 操作不是原子性,getstatic 取得值可能是其他線程改變後的值,操作數棧的值就是過期的數據;
    • 對於long和double型變量的特殊規則
      • JMM 對於lock,unlock,read,load,use,assign,store,write 8個操作都具有原子性,對於64位的數據類型,定義一條相對寬鬆的規定:
        • 允許jvm將沒有被volatile修飾的64位數據的讀寫操作劃分爲兩次32位的操作進行;即允許jvm實現選擇可以不保證64位數據類型的load,store,read,write者4個操作,這就是long和double的非原子性協定;
        • 現象就是如果多個線程共享一個並未聲明爲volatile的long或double的變量,並且同時對它進行讀取和修改,可能某些線程會讀到一個既非原值,也不是其他線程修改的半個變量;
    • 併發三大特性: 原子性,可見性,有序性
      • 原子性 (Atomicity) : 基本數據類型的訪問讀寫是具備原子性的;jmm還提供了lock和unlock操作保證原子性,對應更高層次的字節碼指令monitorenter和monitorexit,這兩個字節碼指令反映到java代碼中就是同步塊(synchronize關鍵字),因此在synchronize塊中的操作也具有原子性;
      • 可見性 (Visibility) : 一個線程修改了共享變量的值,其他線程能夠立即得知這個修改; jmm是通過在變量修改後將新值同步回主內存,在變量讀取前從主內存刷新變量值這種依賴主內存作爲傳遞媒介的方式來實現可見性的; 無論是普通變量還是volatile變量都是如此,區別就是volatile的特殊規則保證了新值能立即同步到主內存,以及每次使用前立即從主內存刷新; 因此,可以說volatile保證了多線程操作時變量的可見性,而普通變量則不能保證這點;
        • 除volatile之外,java還有兩個關鍵字能實現可見性,即synchronized 和 final;
        • 同步塊的可見性是對一個變量執行unlock操作之前,必須先把此變量同步會主內存中(執行store,write操作)
        • final關鍵字可見性是被final修飾的字段在構造器中一旦初始化完成,並且構造器沒有把this的引用傳遞出去(this引用逃逸是一件很危險的事情,其他線程可能通過這個引用訪問到初始化了一半的對象),那在其他線程中就能看見final字段的值;
      • 有序性 (Ordering) :
        • java程序天然的有序性可總結爲 如果在本線程內觀察,所以的操作都是有序的;如果在一個線程中觀察另一個線程,所以的操作都是無序的; 對應於’線程內表現爲串行的語義’和’指令重排序,工作內存和主內存同步延遲’
        • java 語言提供了volatile 和synchronized 保證線程之間的操作的有序性; volatile本身通過內存屏障禁止指令重排序,sychronized由一個變量在同一時刻只允許一條線程對其進行lock操作;決定了持有同一個鎖的兩個同步塊只能串行的進入;
    • 先行發生原則 happens-before
      • 判斷數據是否存在競爭,線程是否安全的主要依據; jmm中定義的兩項操作之間的偏序關係;
      • 默認先行發生關係 (無任何同步手段保障的先行發生規則下):
        • 程序次序規則 Program Order Rule: 在一個線程內,按照程序代碼順序(控制流順序),書寫在前面的操作先行發生於書寫在後面的操作;
        • 管程鎖定規則 Monitor Lock Rule: 一個unlock操作先行發生於後面對同一個鎖的lock操作;
        • Volatile變量規則 : 一個volatile變量的寫操作先行發生於後面對這個變量的讀操作;
        • 線程啓動規則 Thread Start Rule : Thread對象的start方法先行發生於此線程的每一個動作;
        • 線程終止規則 Thread Termination Rule : 線程中的所有操作都先行發生於對此線程的終止檢測,可使用Thread.join()方法結束(阻塞當前線程,等待join的線程執行完畢),Thread.isAlive()的返回值等手段檢測線程已經終止執行;
        • 線程中斷規則 Thread Interuption Rule : 對線程interrupt的方法調用先行發生於被中斷線程的代碼檢測到中斷事件的發生;可使用Thread.interrupted()方法檢測到是否有中斷髮生;
        • 對象終結規則 Finalizer Rule : 一個對象的初始化完成(構造函數執行結束)先行發生於它的finalize方法的開始;
        • 傳遞性 Transitivity ;

java與線程

線程的實現: 線程是最小的調度執行單位;每個已經執行start()且還未結束的java.lang.Thread類的實例就是代表了一個線程;

  • 使用內核線程實現
    • 內核線程(Kernel-level Thread KLT)直接由操作系統內核支持的線程,這種線程由內核來完成線程切換,內核通過操作調度器(Scheduler)對線程進行調度,並負責將線程的任務映射到各個處理器上;
    • 程序一般不會直接使用內核線程,而是使用內核線程的一種高級接口-輕量級進程(Light Weight Process,LWP),輕量級進程就是通常意義上的線程,每個輕量級進程都由一個內核線程支持;

在這裏插入圖片描述

  • 使用用戶線程實現

在這裏插入圖片描述

  • 使用用戶線程加輕量級進程混合實現

在這裏插入圖片描述

  • java線程的實現

目前的jdk版本中,操作系統支持怎樣的線程模型,很大程度上決定了jvm的線程是怎樣映射的;

java線程調度

線程調度是指系統爲線程分配處理器使用權的過程; 主要調度方法分爲: 協同式線程調度(Cooperative Threads-Scheduling) 和搶佔式線程調度 (Preemptive Threads-Scheduling);

  • 協同式的多線程系統 :線程的執行時間由線程本身來控制,線程把自己的工作執行完了後,主動通知系統切換到另外一個線程上;
  • 搶佔式的多線程系統 :每個線程將由系統來分配執行時間,線程的切換不由線程本身來決定;線程的執行時間可控,不會被一個線程導致整個進程阻塞;java使用的線程調度方式就是搶佔式調度;(Thread 10個線程優先級可給某些進程多分配一點時間)

狀態轉換

java定義5中線程狀態,在任意一個時間點,一個線程只能有且只有其中一種狀態;

  • 新建 New : 創建後尚未啓動的線程處於這種狀態;
  • 運行 Runnable : 包括了操作系統狀態中的Running和Ready,也就是處於此狀態的線程有可能正在執行,也有可能正在等待CPU爲他分配時間;
  • 無限期等待 Waiting : 處於這種狀態的線程不會被分配CPU執行時間,他們要等待被其他線程顯示的喚醒; 可將線程進入等待狀態方法:
    • 沒有設置Timeout參數的Object.wait()方法;
    • 沒有設置Timeout參數的Thread.join()方法;
    • LockSupport.park()方法;
  • 限期等待 Timed Waiting : 處於這種狀態的線程也不會被分配CPU執行時間,不過無需等待被其他線程顯示的喚醒,在一定的時間後它們會由系統自動喚醒; 可將線程進入限期等待方法:
    • Thread.sleep()方法;
    • 設置Timeout參數的Object.wait()方法;
    • 設置Timeout參數的Thread.join()方法;
    • LockSupport.parkNanos()方法;
    • LockSupport.parkUntil()方法;
  • 阻塞 Blocked : 線程被阻塞了,與等待狀態的區別爲: 阻塞狀態在等待着獲取到一個排他鎖,這個事件將在另外一個線程放棄這個鎖的時候發生; 而等待狀態則是等待一段時間,或者喚醒動作的發生; 在程序等待進入同步區域的時候,線程將進入這種狀態;
  • 終結 Terminated : 已終止線程的線程狀態,線程已經結束執行;

在這裏插入圖片描述


線程安全和鎖優化

  • 線程安全 : 當多個線程訪問同一個對象時,如果不用考慮這些線程在運行時環境下的調度和交替執行,也不需要進行額外的同步,或者在調用方進行任何其他的協調操作,調用這個對象的行爲都可以獲得正確的結果,那這個對象是線程安全的; 即代碼封裝了所有必要的正確性保障手段(互斥同步)

    • java操作共享數據分類:
      • 不可變 Immutable : 不可變對象一定是線程安全的; java中如果共享數據是一個基本數據類型,只要在定義時使用final關鍵字修飾可保證它是不可變的; 如果共享數據是一個對象,需要保證對象的行爲不會對其狀態產生影響才行; 如java.lang.String類 AtomicLong等;
      • 絕對線程安全
      • 相對線程安全 : 通常意義上的線程安全;
      • 線程兼容
      • 線程對立 無論調用端是否採取同步措施,都無法在多線程環境中併發使用的代碼;
  • 線程安全的實現方法

    • 互斥同步 Mutual Exclusion & Synchronization (阻塞同步): 同步是指在多個線程併發訪問共享數據時,保證共享數據在同一個時刻只被一個線程使用(或者是一些,使用信號量的時候);互斥是實現同步的一種手段 ,臨界區(Critical Section),互斥量(Mutex),信號量(Semaphore)都是主要的互斥實現方式;
      • java中 最基本的互斥同步手段就是sychronized 關鍵字, sychronized經過編譯後,會在同步塊的前後分別形成monitorenter和monitorexit這兩個字節碼指令,這兩個字節碼都需要一個reference類型的參數來指明要鎖定和解鎖的對象;
      • 如果sychronized明確指定了對象參數,那就是這個對象的reference,如果沒有明確指令,那就是根據sychronized修飾的是實例方法還是類方法,取對應的對象實例或者Class對象作爲鎖對象;
      • jvm規範 sychronized同步塊對同一條線程來說是可重入的,不會出現自己把自己鎖死的問題; 同步塊在已進入的線程執行完之前,會阻塞後面的其他線程的進入; 而java的線程是映射在操作系統的原生線程之上的,如果要阻塞或者喚醒一個線程,都需要操作系統來幫忙完成,需要從用戶態轉換到內核態中,因此狀態轉換需要耗費很多的處理器時間; 所以 sychronized 是java中的一個重量級的操作;
      • 除了sychronized之外,還可以使用java.util.concurrent(JUC)包中的重入鎖(ReentrantLock)來實現同步; 增加一些高級功能: 等待可中斷,可實現公平鎖,鎖可綁定多個條件;
        • 等待可中斷, 當持有鎖的線程長期不釋放鎖的時候,正在等待的線程可以選擇放棄等待,改爲處理其他事情;
        • 公平鎖, 多個線程在等待同一個鎖時必須按照申請鎖的時間順序來依次獲得鎖; 非公平鎖不保證這一點,在鎖釋放時,任何一個等待鎖的線程都有機會獲得鎖;
        • 鎖綁定多個條件,ReentrantLock可以同時綁定多個Condition對象,而在Sychronized中,鎖對象的wait()和notify()或notifyAll()方法可以實現一個隱含的條件,如果要和多於一個的條件關聯的時候,就不得不得額外的添加一個鎖,而ReentrantLock無需這樣做;
    • 非阻塞同步 Non-Blocking Synchronization : 互斥同步的主要問題是進行線程阻塞和喚醒鎖帶來的性能問題,也被成爲阻塞同步;互斥同步屬於一種悲觀的併發策略;隨着硬件指令集的發展,還有另外一個選擇: 基於衝突檢測的樂觀併發策略;
      • 衝突檢測需要靠硬件實現,常用的指令有:
        • 測試並設置 Test and Set
        • 獲取並增加 Fetch and Increment
        • 交換 Swap
        • 比較並交換 Compare and Swap (CAS)
        • 加載鏈接/條件存儲 Load Linked /Store Conditional (LL/SC)
      • CAS 指令 有3個操作數,分別爲內存位置(Java中可理解爲變量的內存地址,用V表示),舊的預期值(用A表示),和新值(用B表示); CAS指令執行時,當且僅當V符合舊預期值A時,處理器用新值B更新V的值,否則它就不執行更新,但是無論是否更新了V的值,都會返回V的舊值,上述的處理過程是一個原子操作;
        • sun.misc.Unsafe類裏的compareAndSwapInt()和compareAndSwapLong()等幾個方法包裝提供,Unsafe類不是提供給用戶程序調用的類(Unsafe.getUnsafe()的代碼中限制了只有啓動類加載器(Bootstrap Classloader)加載的Class才能訪問它),因此,不採用反射手段,只能通過其他的api去使用它,如JUC的整數原子類的compareAndSet()使用Unsafe類的CAS操作;
        • CAS存在ABA問題,就是A先改爲B,在改爲A,CAS操作認爲它沒有被改變過;可使用傳統的互斥同步;
    • 無同步方法 不可變保證線程安全
      • 可重入代碼 Reentrant Code : 純代碼Pure Code,可以在代碼執行的任何時刻中斷它,轉而執行另外一段代碼(包括遞歸調用它本身),而在控制權返回後,原來的程序不會出現任何錯誤; 如果一個方法,它的返回結果是可以預測的,只要輸入了相同的數據,就都能返回相同的結果,那它就滿足可重入性的要求,當然也是線程安全的;
      • 線程本地存儲 Thread Local Storage : 如果一段代碼中所需要的數據必須與其他代碼共享,如果能保證這些共享數據的代碼在同一個線程中執行,就可以把共享數據的可見範圍限制在同一個線程之內,這樣無需同步也能保證線程間不出現數據爭用的問題;
        • 可使用java.lang.ThreadLocal類來實現線程本地存儲的功能,代表爲某個線程獨享,每個Thread對象中都有一個ThreadLocalMap對象,這個對象存儲了一組以ThreadLocal.threadLocalHashCode爲鍵,以本地線程變量爲值的K-V值對;

	ReentrantLock 的用法

 	public ArrayBlockingQueue(int capacity, boolean fair) {
        if (capacity <= 0)
            throw new IllegalArgumentException();
        this.items = new Object[capacity];
        lock = new ReentrantLock(fair);
        notEmpty = lock.newCondition();
        notFull =  lock.newCondition();
    }
	...
	public boolean offer(E e) {
        Objects.requireNonNull(e);
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            if (count == items.length)
                return false;
            else {
                enqueue(e);
                return true;
            }
        } finally {
            lock.unlock();
        }
    }

鎖優化

  • 自旋鎖和自適應自旋

    • 因爲互斥同步需要阻塞,掛起線程和恢復線程都需要轉入內核態完成,而某些共享數據的鎖定狀態只會持續很短的一段時間;如果物理機器有一個以上的處理器,能讓兩個或以上的線程同時並行執行,讓後面請求鎖的線程不放棄處理器的執行時間,讓線程執行一個忙循環(自旋),這就是自旋鎖;
    • 默認的自旋次數爲10次(循環),自適應的自旋時間不在固定,由上一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定;
  • 鎖消除 : jvm 在運行時,對一些代碼上要求同步,但是被檢測到不可能存在共享數據競爭的鎖進行消除;

  • 鎖粗化 : 循環體反覆加鎖,加鎖同步的範圍擴展到整個操作序列的外部;

  • 輕量級鎖 : 爲了沒有多線程競爭的前提下,減少傳統的重量級鎖使用操作系統互斥量產生的性能消耗;

    • hotspot jvm 的對象頭(Object Header)分爲兩部分信息,第一部分存儲對象自身的運行時數據,如hashcode,GC age等mark word; 這部分是實現輕量級鎖和偏向鎖的關鍵;另外一部分存儲的是指向方法區對象類型數據的指針,如果是數組對象還有用於存儲數組長度; 如果有兩條以上的線程爭用同一個鎖,輕量級鎖要膨脹爲重量級鎖;

在這裏插入圖片描述

  • 偏向鎖 : 消除數據在無競爭情況下的同步原語,提高程序的運行性能;如果輕量級鎖時無競爭的情況下使用CAS去消除同步使用的互斥量;偏向鎖就是無競爭情況下把整個同步都消除掉CAS都不做了;
    • 鎖會偏向於第一個獲得它的線程,如果在接下來的執行過程中,該鎖沒有被其他的線程獲取,則持有偏向鎖的線程將永遠不需要在進行同步;如果有其他線程嘗試獲取這個鎖,偏向模式宣告結束;

在這裏插入圖片描述


發佈了46 篇原創文章 · 獲贊 3 · 訪問量 5087
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章