併發編程之線程第二篇

3.12 五種狀態

這是從操做系統層面來描述的
在這裏插入圖片描述java

  • 【初始狀態】僅是在語音層面建立了線程對象,還未與操做系統線程關聯
  • 【可運行狀態】(就緒狀態)指該線程已經被建立(與操做系統線程關聯),能夠由CPU調度執行
  • 【運行狀態】指獲取了CPU時間片運行中的狀態
    (1)當CPU時間片用完,會從【運行狀態】轉換至【可運行狀態】,會致使線程的上下文切換
  • 【阻塞狀態】
    (1)若是調用了阻塞API,如BIO讀寫文件,這時該線程實際不會用到CPU,會致使線程上下文切換,進入【阻塞狀態】
    (2)等BIO操做完畢,會由操做系統喚醒阻塞的線程,轉換至【可運行狀態】
    (3)與【可運行狀態】的區別是,對【阻塞狀態】的線程來講只要它們一直不喚醒,調度器就一直不會考慮調度它們
  • 【終止狀態】表示線程已經執行完畢,生命週期已經結束,不會再轉換爲其它狀態

3.13 六種狀態

這是從Java API層面來描述的
根據Thread.State枚舉,分爲六種狀態
在這裏插入圖片描述編程

  • NEW 線程剛被建立,可是尚未調用start()方法
  • RUNNABLE 當調用了start()方法以後,注意,Java API層面的RUNNABLE狀態涵蓋了操做系統層面的【可運行狀態】、【運行狀態】和【阻塞狀態】(因爲BIO致使的線程阻塞,在Java裏沒法區分,任然認爲是可運行)
  • BLOCKED、WAITING、TIMED_WAITING都是Java API層面對【阻塞狀態】的細分。
  • TERMINATED當線程代碼運行結束

4.1 共享帶來的問題

Java的體現
兩個線程對初始值爲0的靜態變量一個作自增,一個作自減,各作5000次,結果是0嗎?
在這裏插入圖片描述
問題分析
y以上的結果多是正數、負數、零。爲何呢?由於Java中對靜態變量的自增,自減並非原子操做,要完全理解,必須從字節碼來進行分析
例如對於 i++而言(i爲靜態變量),實際會產生以下的JVM字節碼指令 :
getstatic i // 獲取靜態變量i的值
iconst_1 // 準備常量1
iadd // 自增
putstatic i // 將修改後的值存入靜態變量i
而對應 i-- 也是相似
getstatic i // 獲取靜態變量i的值
iconst_1 // 準備常量1
isub // 自減
putstatic i // 將修改後的值存入靜態變量i
而Java的內存模型以下,完成靜態變量的自增,自減須要在主存和工做內存中進行數據交換 :
在這裏插入圖片描述
若是是單線程以上8行代碼是順序執行(不會交錯)沒有問題 :
在這裏插入圖片描述
但多線程下這8行代碼可能交錯運行 :
出現負數的狀況 :
在這裏插入圖片描述
出現正數的狀況
在這裏插入圖片描述
臨界區Critical Section數組

  • 一個程序運行多個線程自己是沒有問題的
  • 問題出在多個線程訪問共享資源
    • 多個線程讀共享資源其實也沒有問題
    • 在多個線程對共享資源讀寫操做時發生指令交錯,就會出現問題
  • 一段代碼塊若是存在對共享資源的多線程讀寫操做,稱這段代碼塊爲臨界區
    例如,下面代碼中臨界區
    在這裏插入圖片描述
    競態條件 Race Condition
    多個線程在臨界區內執行,因爲代碼的執行序列不一樣而致使結果沒法預測,稱之爲發生了競態條件

4.2 synchronized解決方案

  • 應用之互斥
    爲了不臨界區的競態條件發生,有多種手段能夠達到目的。
  • 阻塞式的解決方案 : synchronized,Lock
  • 非阻塞式的解決方案 : 原子變量
    本次使用阻塞式的解決方案 : synchronized,來解決上述問題,即俗稱的【對象鎖】,它採用互斥的方式讓同一個時刻最多隻有一個線程能持有【對象鎖】,其它線程再想獲取這個【對象鎖】時就會阻塞住。這樣就能保證擁有鎖的線程能夠安全的執行臨界區內的代碼,不用擔憂線程上下文切換
    注意
    雖然java中互斥和同步均可以採用synchronized關鍵字來完成,但它們仍是有區別的 :
  • 互斥是保證臨界區的競態條件發生,同一時刻只能有一個線程執行臨界區代碼
  • 同步是因爲線程執行的前後、順序不一樣、須要一個線程等待其它線程運行到某個點
    synchronized
    語法
    在這裏插入圖片描述
    解決
    在這裏插入圖片描述
    在這裏插入圖片描述
    所謂的線程八鎖
    其實就是考察synchronized鎖住的是哪一個對象
    狀況1 :

在這裏插入圖片描述
用圖來解釋
在這裏插入圖片描述
在這裏插入圖片描述
思考
synchronized實際是用對象鎖保證了臨界區內代碼的原子性,臨界區內的代碼對外是不可分割的,不會被線程切換所打斷。
爲了加深理解,請思考下面的問題安全

  • 若是把synchronized(obj)放在for循環的外面,如何理解?-- 原子性
  • 若是t1 synchronized(obj1) 而 t2 synchronized(obj2)會怎樣運做? – 鎖對象
  • 若是t1 synchronized(obj) 而t2 沒有加會怎麼樣?如何理解? – 鎖對象
    面向對象改進
    把須要保護的共享變量放入一個類
    在這裏插入圖片描述

4.4 變量的線程安全分析

成員變量和靜態變量是否線程安全?多線程

  • 若是它們沒有共享,則線程安全
  • 若是它們被共享了,根據它們的狀態是否可以改變,又分兩種狀況
    • 若是隻有讀操做,則線程安全
    • 若是有讀寫操做,則這段代碼是臨界區,須要考慮線程安全
      局部變量是否線程安全?
  • 局部變量是線程安全的
  • 但局部變量引用的對象則未必
    • 若是該對象沒有逃離方法的做用訪問,它是線程安全的
    • 若是該對象逃離方法的做用範圍,須要考慮線程安全
      局部變量線程安全分析
      在這裏插入圖片描述
      每一個線程調用test1()方法時局部變量i,會在每一個線程的棧幀內存中被建立多份,所以不存在共享
      在這裏插入圖片描述
      如圖
      在這裏插入圖片描述
      局部變量的引用稍有不一樣
      先看一個成員變量的例子
      在這裏插入圖片描述
      在這裏插入圖片描述
      其中一種狀況是,若是線程2還未add,線程1remove就會報錯 :
      在這裏插入圖片描述
      分析 :
  • 不管哪一個線程中的method2引用的都是同一個對象中的list成員變量
  • method3與method2分析相同
    在這裏插入圖片描述
    將list修改爲局部變量
    在這裏插入圖片描述
    分析 :
  • list是局部變量,每一個線程調用時會建立其不一樣實例,沒有共享
  • 而method2的蠶食是從method1中傳遞過來的,與method1中引用同一個對象
  • method3的參數分析與method2相同
    在這裏插入圖片描述
    方法訪問修飾符帶來的思考,若是把method2和method3的方法修改成public會不會代理線程安全問題?
  • 狀況1 :有其它線程調用method2和method3
  • 狀況2 :在狀況1的基礎上,爲ThreadSafe類添加子類,子類覆蓋method2或method3方法。
    ![在這裏插入圖片描述](https://img-blog.csdnimg.cn/2020020816151116.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3poYW8xMjk5MDAyNzg4,size_16,color_FFFFFF,t_7
    從這裏例子中能夠看出private或final提升【安全】的意義所在,請體會開閉原則中的【閉】
    常見線程安全類
  • String
  • Integer
  • StringBuffer
  • Random
  • Vector
  • HashTable
  • java.util.concurrent包下的類
    這裏說它們是線程安全的是指,多個線程調用它們同一個實例的某個方法時,是線程安全的。也能夠理解爲
  • 它們的每一個方法是原子的
  • 但注意它們多個方法的組合不是原子的,見後面分析
    線程安全類方法的組合
    分析下面代碼是否線程安全?
    在這裏插入圖片描述
    在這裏插入圖片描述
    不可變類線程安全性
    String、Integer等都是不可變類,由於其內部的狀態不能夠改變,所以它們的方法都是線程安全的。
    在這裏插入圖片描述
    在這裏插入圖片描述
    其中foo的行爲是不肯定的,可能致使不安全的發生,被稱之爲外星方法
    在這裏插入圖片描述
    請比較JDK中String類的實現,爲何是final修飾的?由於防止子類去實現,這樣會引發線程安全問題。也是符合開閉原則

4.6 Monitor概念

Java對象頭
以32位虛擬機爲例
普通對象
在這裏插入圖片描述
數組對象
在這裏插入圖片描述
其中Mark Word結構爲
在這裏插入圖片描述
Monitor
Monitor被翻譯爲監視器或管程
每一個Java對象均可以關聯一個Monitor對象,若是使用synchronized給對象上鎖(重量級)以後,該對象頭的Mark Word中就被設置指向Monitor對象的指針
Monitor結構以下
在這裏插入圖片描述併發

  • 剛開始Monitor中Owner爲null
  • 當Thread-2指向synchronized(obj)就會將Monitor的全部者Owner置爲Thread-2,Monitor中只能有一個Owner
  • 在Thread-2上鎖的過程當中,若是Thread-3,Thread-4,Thread-5也來執行synchronized(obj),就會進入EntryList BLOCKED
  • Thread-2執行完同步代碼塊的內容,而後喚醒EntryList中等待的線程來競爭鎖,競爭的時是非公平的
  • 圖中WaitSet中的Thread-0,Thread-1是以前得到過鎖,但條件不知足進入WAITING狀態的線程,後面講wait-notify時會分析
    注意 :
    • synchronized必須是進入同一個對象的monitor纔有上述的效果
    • 不加synchronized的對象不會關聯監視器,不聽從以上規則
      原理之synchronized
      在這裏插入圖片描述
      對應的字節碼
      在這裏插入圖片描述

1. 輕量級鎖

輕量級鎖的使用場景 :若是一個對象雖然有多線程訪問,但多線程訪問的時間是錯開的(也就是沒有競爭),那麼可使用輕量級鎖來優化
輕量級鎖來優化。
輕量級鎖對使用者是透明的,即語法任然是synchronized
假設有兩個方法同步塊,利用同一個對象加鎖
在這裏插入圖片描述dom

  • 建立鎖記錄(Lock Record)對象,每一個線程都的棧幀都會包含一個鎖記錄的結構,內部能夠存儲鎖定對象的Mark Word
    在這裏插入圖片描述
  • 讓鎖記錄中Object reference指向鎖對象,並嘗試用cas替換Object的Mark Word,將Mark Word的值存入鎖記錄
    在這裏插入圖片描述
  • 若是cas替換成功,對象頭中存儲了鎖記錄地址和狀態 00,表示由該線程給對象加鎖,這時圖示以下
    在這裏插入圖片描述
  • 若是cas失敗,有兩種狀況
    • 若是是其餘線程已經持有了該Object的輕量級鎖,這時表面有競爭,進入膨脹過程
    • 若是是本身執行了synchronized鎖重入,那麼再添加一條Lock Record做爲重入的計數
      在這裏插入圖片描述
  • 當退出synchronized代碼塊(解鎖時)若是有取值爲null的鎖記錄,表示有重入,這時重置鎖記錄,表示重入計數減一
    在這裏插入圖片描述
  • 當退出synchronized代碼塊(解鎖時)鎖記錄的值不爲null,這時使用cas將Mark Word的值恢復給對象頭
    • 成功,則解鎖成功
    • 失敗,說明輕量級鎖進行了鎖膨脹或已經升級爲重量級鎖,進入重量級鎖解鎖流程

2. 鎖膨脹

若是在嘗試加輕量級鎖的過程當中,CAS操做沒法成功,這時一種狀況就是有其餘線程爲此對象加上了輕量級鎖(有競爭),這時須要進行鎖膨脹,將輕量級鎖變爲重量級鎖。
在這裏插入圖片描述優化

  • 當Thread-1進行輕量級加鎖時,Thread-0已結對該對象加了輕量級鎖
    在這裏插入圖片描述
  • 這時Thread-1加輕量級鎖失敗,進入鎖膨脹流程
    • 即爲Object對象申請Monitor鎖,讓Object指向重量級鎖地址
    • 而後本身進入Monitor的EntryList BLOCKED
      在這裏插入圖片描述
  • 當Thread-0退出同步塊解鎖時,使用cas將Mark Word的值恢復給對象頭,失敗。這時會進入重量級解鎖流程,即 按照Monitor地址找到Monitor對象,設置Owner爲null,喚醒EntryList中BLOCKED線程。
相關文章
相關標籤/搜索