併發程式設計學習筆記-2
前言:文中出現的示例程式碼地址為:gitee程式碼地址
5. 共享模型之記憶體
上一章講解的 Monitor 主要關注的是訪問共享變數時,保證臨界區程式碼的原子性。這一章我們進一步深入學習共享變數在多執行緒間的【可見性】問題與多條指令執行時的【有序性】問題
5.1 Java 記憶體模型
JMM 即 Java Memory Model,它從java層面定義了主存、工作記憶體抽象概念,底層對應著 CPU 暫存器、快取、硬體記憶體、CPU 指令優化等。JMM 體現在以下幾個方面
- 原子性 - 保證指令不會受到執行緒上下文切換的影響
- 可見性 - 保證指令不會受 cpu 快取的影響
- 有序性 - 保證指令不會受 cpu 指令並行優化的影響
5.2 可見性
退不出的迴圈
先來看一個現象,main 執行緒對 run 變數的修改對於 t 執行緒不可見,導致了 t 執行緒無法停止:Test1.java
public class Test1 { static boolean run = true; public static void main(String[] args) throws InterruptedException { Thread t = new Thread(()->{ while(run){ // .... // System.out.println(2323); 如果加上這個程式碼就會停下來 } }); t.start(); utils.sleep(1); System.out.println(3434); run = false; // 執行緒t不會如預想的停下來 } }
為什麼呢?分析一下:
- 初始狀態, t 執行緒剛開始從主記憶體讀取了 run 的值到工作記憶體。
- 因為t1執行緒頻繁地從主存中讀取run的值,jit即時編譯器會將run的值快取至自己工作記憶體中的快取記憶體中,減少對主存中run的訪問以提高效率
- 1 秒之後,main 執行緒修改了 run 的值,並同步至主存,而 t 是從自己工作記憶體中的快取記憶體中讀取這個變數 的值,結果永遠是舊值
解決方法
volatile(表示易變關鍵字的意思),它可以用來修飾成員變數和靜態成員變數,他可以避免執行緒從自己的工作快取中查詢變數的值,必須到主存中獲取它的值,執行緒操作 volatile 變數都是直接操作主存 Test1.java
使用synchronized關鍵字也有相同的效果!在Java記憶體模型中,synchronized規定,執行緒在加鎖時, 先清空工作記憶體→在主記憶體中拷貝最新變數的副本到工作記憶體 →執行完程式碼→將更改後的共享變數的值重新整理到主記憶體中→釋放互斥鎖。Test2.java
可見性 vs 原子性
前面例子體現的實際就是可見性,它保證的是在多個執行緒之間一個執行緒對 volatile 變數的修改對另一個執行緒可 見, 而不能保證原子性,僅用在一個寫執行緒,多個讀執行緒的情況。 上例從位元組碼理解是這樣的:
getstatic run // 執行緒 t 獲取 run true
getstatic run // 執行緒 t 獲取 run true
getstatic run // 執行緒 t 獲取 run true
getstatic run // 執行緒 t 獲取 run true
putstatic run // 執行緒 main 修改 run 為 false, 僅此一次
getstatic run // 執行緒 t 獲取 run false
比較一下之前我們將執行緒安全時舉的例子:兩個執行緒一個 i++ 一個 i-- ,只能保證看到最新值,不能解決指令交錯
// 假設i的初始值為0
getstatic i // 執行緒2-獲取靜態變數i的值 執行緒內i=0
getstatic i // 執行緒1-獲取靜態變數i的值 執行緒內i=0
iconst_1 // 執行緒1-準備常量1
iadd // 執行緒1-自增 執行緒內i=1
putstatic i // 執行緒1-將修改後的值存入靜態變數i 靜態變數i=1
iconst_1 // 執行緒2-準備常量1
isub // 執行緒2-自減 執行緒內i=-1
putstatic i // 執行緒2-將修改後的值存入靜態變數i 靜態變數i=-1
注意 :synchronized 語句塊既可以保證程式碼塊的原子性,也同時保證程式碼塊內變數的可見性。但缺點是 synchronized 是屬於重量級操作,效能相對更低。 如果在前面示例的死迴圈中加入 System.out.println() 會發現即使不加 volatile 修飾符,執行緒 t 也能正確看到 對 run 變數的修改了,想一想為什麼?因為println方法裡面有synchronized修飾。還有那個等煙的示例(Test34.java)為啥沒有出現可見性問題?和synchrozized是一個道理。
模式之兩階段終止
使用volatile關鍵字來實現兩階段終止模式,上程式碼:Test3.java
模式之 Balking
- 定義:Balking (猶豫)模式用在一個執行緒發現另一個執行緒或本執行緒已經做了某一件相同的事,那麼本執行緒就無需再做了,直接結束返回。有點類似於單例。
- 實現 Test4.java
5.3 有序性
詭異的結果
int num = 0;
// volatile 修飾的變數,可以禁用指令重排 volatile boolean ready = false; 可以防止變數之前的程式碼被重排序
boolean ready = false;
// 執行緒1 執行此方法
public void actor1(I_Result r) {
if(ready) {
r.r1 = num + num;
}
else {
r.r1 = 1;
}
}
// 執行緒2 執行此方法
public void actor2(I_Result r) {
num = 2;
ready = true;
}
分別執行上面兩個執行緒
I_Result 是一個物件,有一個屬性 r1 用來儲存結果,問可能的結果有幾種? 有同學這麼分析 情況1:執行緒1 先執行,這時 ready = false,所以進入 else 分支結果為 1 情況2:執行緒2 先執行 num = 2,但沒來得及執行 ready = true,執行緒1 執行,還是進入 else 分支,結果為1 情況3:執行緒2 執行到 ready = true,執行緒1 執行,這回進入 if 分支,結果為 4(因為 num 已經執行過了)
但我告訴你,結果還有可能是 0 ,信不信吧!這種情況下是:執行緒2 執行 ready = true,切換到執行緒1,進入 if 分支,相加為 0,再切回執行緒2 執行 num = 2。
這種現象叫做指令重排,是 JIT 編譯器在執行時的一些優化,這個現象需要通過大量測試才能復現,可以使用jcstress工具進行測試。上面僅是從程式碼層面體現出了有序性問題,下面在講到 double-checked locking 問題時還會從java位元組碼的層面瞭解有序性的問題。
重排序也需要遵守一定規則:
- 重排序操作不會對存在資料依賴關係的操作進行重排序。比如:a=1;b=a; 這個指令序列,由於第二個操作依賴於第一個操作,所以在編譯時和處理器執行時這兩個操作不會被重排序。
- 重排序是為了優化效能,但是不管怎麼重排序,單執行緒下程式的執行結果不能被改變。比如:a=1;b=2;c=a+b這三個操作,第一步(a=1)和第二步(b=2)由於不存在資料依賴關係,所以可能會發生重排序,但是c=a+b這個操作是不會被重排序的,因為需要保證最終的結果一定是c=a+b=3。
重排序在單執行緒模式下是一定會保證最終結果的正確性,但是在多執行緒環境下,問題就出來了。解決方法:volatile 修飾的變數,可以禁用指令重排
注意:使用synchronized並不能解決有序性問題,但是如果是該變數整個都在synchronized程式碼塊的保護範圍內,那麼變數就不會被多個執行緒同時操作,也不用考慮有序性問題!在這種情況下相當於解決了重排序問題!參考double-checked locking 問題裡的程式碼,第一個程式碼片段中的instance變數都在synchronized程式碼塊中,第二個程式碼片段中instance不全在synchronized中所以產生了問題。 視訊 P151
volatile 原理
volatile 的底層實現原理是記憶體屏障,Memory Barrier(Memory Fence)
- 對 volatile 變數的寫指令後會加入寫屏障
- 對 volatile 變數的讀指令前會加入讀屏障
如何保證可見性
- 寫屏障(sfence)保證在該屏障之前的,對共享變數的改動,都同步到主存當中
public void actor2(I_Result r) {
num = 2;
ready = true; // ready是被volatile修飾的 ,賦值帶寫屏障
// 寫屏障
}
-
而讀屏障(lfence)保證在該屏障之後,對共享變數的讀取,載入的是主存中最新資料
public void actor1(I_Result r) { // 讀屏障 // ready是被volatile修飾的 ,讀取值帶讀屏障 if(ready) { r.r1 = num + num; } else { r.r1 = 1; } }
如何保證有序性
-
寫屏障會確保指令重排序時,不會將寫屏障之前的程式碼排在寫屏障之後
public void actor2(I_Result r) { num = 2; ready = true; // ready是被volatile修飾的 , 賦值帶寫屏障 // 寫屏障 }
-
讀屏障會確保指令重排序時,不會將讀屏障之後的程式碼排在讀屏障之前
public void actor1(I_Result r) { // 讀屏障 // ready是被volatile修飾的 ,讀取值帶讀屏障 if(ready) { r.r1 = num + num; } else { r.r1 = 1; } }
還是那句話,不能解決指令交錯:
- 寫屏障僅僅是保證之後的讀能夠讀到最新的結果,但不能保證其它執行緒的讀跑到它前面去
- 而有序性的保證也只是保證了本執行緒內相關程式碼不被重排序
double-checked locking 問題
以著名的 double-checked locking 單例模式為例,這是volatile最常使用的地方。
//最開始的單例模式是這樣的
public final class Singleton {
private Singleton() { }
private static Singleton INSTANCE = null;
public static Singleton getInstance() {
// 首次訪問會同步,而之後的使用不用進入synchronized
synchronized(Singleton.class) {
if (INSTANCE == null) { // t1
INSTANCE = new Singleton();
}
}
return INSTANCE;
}
}
//但是上面的程式碼塊的效率是有問題的,因為即使已經產生了單例項之後,之後呼叫了getInstance()方法之後還是會加鎖,這會嚴重影響效能!因此就有了模式如下double-checked lockin:
public final class Singleton {
private Singleton() { }
private static Singleton INSTANCE = null;
public static Singleton getInstance() {
if(INSTANCE == null) { // t2
// 首次訪問會同步,而之後的使用沒有 synchronized
synchronized(Singleton.class) {
if (INSTANCE == null) { // t1
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
//但是上面的if(INSTANCE == null)判斷程式碼沒有在同步程式碼塊synchronized中,不能享有synchronized保證的原子性,可見性。所以
以上的實現特點是:
- 懶惰例項化
- 首次使用 getInstance() 才使用 synchronized 加鎖,後續使用時無需加鎖
- 有隱含的,但很關鍵的一點:第一個 if 使用了 INSTANCE 變數,是在同步塊之外
但在多執行緒環境下,上面的程式碼是有問題的,getInstance 方法對應的位元組碼為:
0: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
3: ifnonnull 37
// ldc是獲得類物件
6: ldc #3 // class cn/itcast/n5/Singleton
// 複製運算元棧棧頂的值放入棧頂, 將類物件的引用地址複製了一份
8: dup
// 運算元棧棧頂的值彈出,即將物件的引用地址存到區域性變量表中
// 將類物件的引用地址儲存了一份,是為了將來解鎖用
9: astore_0
10: monitorenter
11: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
14: ifnonnull 27
// 新建一個例項
17: new #3 // class cn/itcast/n5/Singleton
// 複製了一個例項的引用
20: dup
// 通過這個複製的引用呼叫它的構造方法
21: invokespecial #4 // Method "<init>":()V
// 最開始的這個引用用來進行賦值操作
24: putstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
27: aload_0
28: monitorexit
29: goto 37
32: astore_1
33: aload_0
34: monitorexit
35: aload_1
36: athrow
37: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
40: areturn
其中
- 17 表示建立物件,將物件引用入棧 // new Singleton
- 20 表示複製一份物件引用 // 複製了引用地址
- 21 表示利用一個物件引用,呼叫構造方法 // 根據複製的引用地址呼叫構造方法
- 24 表示利用一個物件引用,賦值給 static INSTANCE
也許 jvm 會優化為:先執行 24,再執行 21。如果兩個執行緒 t1,t2 按如下時間序列執行:
關鍵在於0: getstatic
這行程式碼在 monitor 控制之外,它就像之前舉例中不守規則的人,可以越過 monitor 讀取 INSTANCE 變數的值 這時 t1 還未完全將構造方法執行完畢,如果在構造方法中要執行很多初始化操作,那麼 t2 拿到的是將是一個未初 始化完畢的單例 對 INSTANCE 使用 volatile 修飾即可,可以禁用指令重排,但要注意在 JDK 5 以上的版本的 volatile 才會真正有效
double-checked locking 解決
加volatile就行了
public final class Singleton {
private Singleton() { }
private static volatile Singleton INSTANCE = null;
public static Singleton getInstance() {
// 例項沒建立,才會進入內部的 synchronized程式碼塊
if (INSTANCE == null) {
synchronized (Singleton.class) { // t2
// 也許有其它執行緒已經建立例項,所以再判斷一次
if (INSTANCE == null) { // t1
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
位元組碼上看不出來 volatile 指令的效果
// -------------------------------------> 加入對 INSTANCE 變數的讀屏障
0: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
3: ifnonnull 37
6: ldc #3 // class cn/itcast/n5/Singleton
8: dup
9: astore_0
10: monitorenter -----------------------> 保證原子性、可見性
11: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
14: ifnonnull 27
17: new #3 // class cn/itcast/n5/Singleton
20: dup
21: invokespecial #4 // Method "<init>":()V
24: putstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
// -------------------------------------> 加入對 INSTANCE 變數的寫屏障
27: aload_0
28: monitorexit ------------------------> 保證原子性、可見性
29: goto 37
32: astore_1
33: aload_0
34: monitorexit
35: aload_1
36: athrow
37: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
40: areturn
如上面的註釋內容所示,讀寫 volatile 變數操作(即getstatic操作和putstatic操作)時會加入記憶體屏障(Memory Barrier(Memory Fence)),保證下面兩點:
- 可見性
- 寫屏障(sfence)保證在該屏障之前的 t1 對共享變數的改動,都同步到主存當中
- 而讀屏障(lfence)保證在該屏障之後 t2 對共享變數的讀取,載入的是主存中最新資料
- 有序性
- 寫屏障會確保指令重排序時,不會將寫屏障之前的程式碼排在寫屏障之後
- 讀屏障會確保指令重排序時,不會將讀屏障之後的程式碼排在讀屏障之前
- 更底層是讀寫變數時使用 lock 指令來多核 CPU 之間的可見性與有序性
happens-before
下面說的變數都是指成員變數或靜態成員變數
-
執行緒解鎖 m 之前對變數的寫,對於接下來對 m 加鎖的其它執行緒對該變數的讀可見
-
static int x; static Object m = new Object(); new Thread(()->{ synchronized(m) { x = 10; } },"t1").start(); new Thread(()->{ synchronized(m) { System.out.println(x); } },"t2").start();
-
-
執行緒對 volatile 變數的寫,對接下來其它執行緒對該變數的讀可見
-
```java volatile static int x; new Thread(()->{ x = 10; },"t1").start(); new Thread(()->{ System.out.println(x); },"t2").start(); ```
-
-
執行緒 start 前對變數的寫,對該執行緒開始後對該變數的讀可見
-
```java static int x; x = 10; new Thread(()->{ System.out.println(x); },"t2").start(); ```
-
-
執行緒結束前對變數的寫,對其它執行緒得知它結束後的讀可見(比如其它執行緒呼叫 t1.isAlive() 或 t1.join()等待它結束)
-
```java static int x; Thread t1 = new Thread(()->{ x = 10; },"t1"); t1.start(); t1.join(); System.out.println(x); ```
-
-
執行緒 t1 打斷 t2(interrupt)前對變數的寫,對於其他執行緒得知 t2 被打斷後對變數的讀可見(通過 t2.interrupted 或 t2.isInterrupted)
-
static int x; public static void main(String[] args) { Thread t2 = new Thread(()->{ while(true) { if(Thread.currentThread().isInterrupted()) { System.out.println(x); break; } } },"t2"); t2.start(); new Thread(()->{ sleep(1); x = 10; t2.interrupt(); },"t1").start(); while(!t2.isInterrupted()) { Thread.yield(); } System.out.println(x); } ```
-
對變數預設值(0,false,null)的寫,對其它執行緒對該變數的讀可見
-
具有傳遞性,如果 x hb-> y 並且 y hb-> z 那麼有 x hb-> z ,配合 volatile 的防指令重排,有下面的例子
-
volatile static int x; static int y; new Thread(()->{ y = 10; x = 20; },"t1").start(); new Thread(()->{ // x=20 對 t2 可見, 同時 y=10 也對 t2 可見 System.out.println(x); },"t2").start();
-
總結
volatile主要用在一個執行緒改多個執行緒讀時的來保證可見性,和double-checked locking模式中保證synchronized程式碼塊外的共享變數的重排序問題
習題
balking 模式習題
希望 doInit() 方法僅被呼叫一次,下面的實現是否有問題,為什麼?
public class TestVolatile {
volatile boolean initialized = false;
void init() {
if (initialized) {
return;
}
doInit();
initialized = true;
}
private void doInit() {
}
}
執行緒安全單例習題
單例模式有很多實現方法,餓漢、懶漢、靜態內部類、列舉類,試分析每種實現下獲取單例物件(即呼叫 getInstance)時的執行緒安全,並思考註釋中的問題 餓漢式:類載入就會導致該單例項物件被建立 懶漢式:類載入不會導致該單例項物件被建立,而是首次使用該物件時才會建立
實現1: 餓漢式
// 問題1:為什麼加 final,防止子類繼承後更改
// 問題2:如果實現了序列化介面, 還要做什麼來防止反序列化破壞單例,如果進行反序列化的時候會生成新的物件,這樣跟單例模式生成的物件是不同的。要解決直接加上readResolve()方法就行了,如下所示
public final class Singleton implements Serializable {
// 問題3:為什麼設定為私有? 放棄其它類中使用new生成新的例項,是否能防止反射建立新的例項?不能。
private Singleton() {}
// 問題4:這樣初始化是否能保證單例物件建立時的執行緒安全?沒有,這是類變數,是jvm在類載入階段就進行了初始化,jvm保證了此操作的執行緒安全性
private static final Singleton INSTANCE = new Singleton();
// 問題5:為什麼提供靜態方法而不是直接將 INSTANCE 設定為 public, 說出你知道的理由。
//1.提供更好的封裝性;2.提供範型的支援
public static Singleton getInstance() {
return INSTANCE;
}
public Object readResolve() {
return INSTANCE;
}
}
實現2: 餓漢式
// 問題1:列舉單例是如何限制例項個數的:建立列舉類的時候就已經定義好了,每個列舉常量其實就是列舉類的一個靜態成員變數
// 問題2:列舉單例在建立時是否有併發問題:沒有,這是靜態成員變數
// 問題3:列舉單例能否被反射破壞單例:不能
// 問題4:列舉單例能否被反序列化破壞單例:列舉類預設實現了序列化介面,列舉類已經考慮到此問題,無需擔心破壞單例
// 問題5:列舉單例屬於懶漢式還是餓漢式:餓漢式
// 問題6:列舉單例如果希望加入一些單例建立時的初始化邏輯該如何做:加構造方法就行了
enum Singleton {
INSTANCE;
}
實現3:懶漢式
public final class Singleton {
private Singleton() { }
private static Singleton INSTANCE = null;
// 分析這裡的執行緒安全, 並說明有什麼缺點:synchronized載入靜態方法上,可以保證執行緒安全。缺點就是鎖的範圍過大
public static synchronized Singleton getInstance() {
if( INSTANCE != null ){
return INSTANCE;
}
INSTANCE = new Singleton();
return INSTANCE;
}
}
實現4:DCL 懶漢式
public final class Singleton {
private Singleton() { }
// 問題1:解釋為什麼要加 volatile ?為了防止重排序問題
private static volatile Singleton INSTANCE = null;
// 問題2:對比實現3, 說出這樣做的意義:提高了效率
public static Singleton getInstance() {
if (INSTANCE != null) {
return INSTANCE;
}
synchronized (Singleton.class) {
// 問題3:為什麼還要在這裡加為空判斷, 之前不是判斷過了嗎?這是為了第一次判斷時的併發問題。
if (INSTANCE != null) { // t2
return INSTANCE;
}
INSTANCE = new Singleton();
return INSTANCE;
}
}
}
實現5:
public final class Singleton {
private Singleton() { }
// 問題1:屬於懶漢式還是餓漢式:懶漢式,這是一個靜態內部類。類載入本身就是懶惰的,在沒有呼叫getInstance方法時是沒有執行LazyHolder內部類的類載入操作的。
private static class LazyHolder {
static final Singleton INSTANCE = new Singleton();
}
// 問題2:在建立時是否有併發問題,這是執行緒安全的,類載入時,jvm保證類載入操作的執行緒安全
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
5.4本章小結
本章重點講解了 JMM 中的
- 可見性 - 由 JVM 快取優化引起
- 有序性 - 由 JVM 指令重排序優化引起
- happens-before 規則
- 原理方面
- volatile
- 模式方面
- 兩階段終止模式的 volatile 改進
- 同步模式之 balking
6. 共享模型之無鎖
管程即monitor是阻塞式的悲觀鎖實現併發控制,這章我們將通過非阻塞式的樂觀鎖的來實現併發控制
6.1 問題提出
有如下需求,保證account.withdraw取款方法的執行緒安全 Test5.java
public class Test5 {
public static void main(String[] args) {
Account.demo(new AccountUnsafe(10000));
}
}
class AccountUnsafe implements Account {
private Integer balance;
public AccountUnsafe(Integer balance) {
this.balance = balance;
}
@Override
public Integer getBalance() {
return balance;
}
@Override
public void withdraw(Integer amount) {
// 通過這裡加鎖就可以實現執行緒安全,不加就會導致結果異常
synchronized (this){
balance -= amount;
}
}
}
interface Account {
// 獲取餘額
Integer getBalance();
// 取款
void withdraw(Integer amount);
/**
* 方法內會啟動 1000 個執行緒,每個執行緒做 -10 元 的操作
* 如果初始餘額為 10000 那麼正確的結果應當是 0
*/
static void demo(Account account) {
List<Thread> ts = new ArrayList<>();
long start = System.nanoTime();
for (int i = 0; i < 1000; i++) {
ts.add(new Thread(() -> {
account.withdraw(10);
}));
}
ts.forEach(Thread::start);
ts.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long end = System.nanoTime();
System.out.println(account.getBalance()
+ " cost: " + (end-start)/1000_000 + " ms");
}
}
解決思路-無鎖
上面的程式碼中可以使用synchronized加鎖操作來實現執行緒安全,但是synchronized加鎖操作太耗費資源,這裡我們使用無鎖來解決此問題: Test5.java
class AccountSafe implements Account{
AtomicInteger atomicInteger ;
public AccountSafe(Integer balance){
this.atomicInteger = new AtomicInteger(balance);
}
@Override
public Integer getBalance() {
return atomicInteger.get();
}
@Override
public void withdraw(Integer amount) {
// 核心程式碼
while (true){
int pre = getBalance();
int next = pre - amount;
if (atomicInteger.compareAndSet(pre,next)){
break;
}
}
// 可以簡化為下面的方法
// balance.addAndGet(-1 * amount);
}
}
6.2 CAS 與 volatile
cas
前面看到的AtomicInteger的解決方法,內部並沒有用鎖來保護共享變數的執行緒安全。那麼它是如何實現的呢?
@Override
public void withdraw(Integer amount) {
// 核心程式碼
// 需要不斷嘗試,直到成功為止
while (true){
// 比如拿到了舊值 1000
int pre = getBalance();
// 在這個基礎上 1000-10 = 990
int next = pre - amount;
/*
compareAndSet 正是做這個檢查,在 set 前,先比較 prev 與當前值
- 不一致了,next 作廢,返回 false 表示失敗
比如,別的執行緒已經做了減法,當前值已經被減成了 990
那麼本執行緒的這次 990 就作廢了,進入 while 下次迴圈重試
- 一致,以 next 設定為新值,返回 true 表示成功
*/
if (atomicInteger.compareAndSet(pre,next)){
break;
}
}
}
其中的關鍵是 compareAndSet,它的簡稱就是 CAS (也有 Compare And Swap 的說法),它必須是原子操作。
volatile
在上面程式碼中的AtomicInteger,儲存值的value屬性使用了volatile 。獲取共享變數時,為了保證該變數的可見性,需要使用 volatile 修飾。
它可以用來修飾成員變數和靜態成員變數,他可以避免執行緒從自己的工作快取中查詢變數的值,必須到主存中獲取 它的值,執行緒操作 volatile 變數都是直接操作主存。即一個執行緒對 volatile 變數的修改,對另一個執行緒可見。
再提一嘴 volatile 僅僅保證了共享變數的可見性,讓其它執行緒能夠看到最新值,但不能解決指令交錯問題(不能保證原 子性)
CAS 必須藉助 volatile 才能讀取到共享變數的最新值來實現【比較並交換】的效果
為什麼無鎖效率高
- 無鎖情況下,即使重試失敗,執行緒始終在高速執行,沒有停歇,而 synchronized 會讓執行緒在沒有獲得鎖的時候,發生上下文切換,進入阻塞。打個比喻:執行緒就好像高速跑道上的賽車,高速執行時,速度超快,一旦發生上下文切換,就好比賽車要減速、熄火,等被喚醒又得重新打火、啟動、加速... 恢復到高速執行,代價比較大
- 但無鎖情況下,因為執行緒要保持執行,需要額外 CPU 的支援,CPU 在這裡就好比高速跑道,沒有額外的跑道,執行緒想高速執行也無從談起,雖然不會進入阻塞,但由於沒有分到時間片,仍然會進入可執行狀態,還是會導致上下文切換。
CAS 的特點
結合 CAS 和 volatile 可以實現無鎖併發,適用於執行緒數少、多核 CPU 的場景下。
- CAS 是基於樂觀鎖的思想:最樂觀的估計,不怕別的執行緒來修改共享變數,就算改了也沒關係,我吃虧點再重試唄。
- synchronized 是基於悲觀鎖的思想:最悲觀的估計,得防著其它執行緒來修改共享變數,我上了鎖你們都別想改,我改完了解開鎖,你們才有機會。
- CAS 體現的是無鎖併發、無阻塞併發,請仔細體會這兩句話的意思
- 因為沒有使用 synchronized,所以執行緒不會陷入阻塞,這是效率提升的因素之一
- 但如果競爭激烈(寫操作多),可以想到重試必然頻繁發生,反而效率會受影響
6.3原子整數
java.util.concurrent.atomic併發包提供了一些併發工具類,這裡把它分成五類:
-
使用原子的方式更新基本型別
- AtomicInteger:整型原子類
- AtomicLong:長整型原子類
- AtomicBoolean :布林型原子類
上面三個類提供的方法幾乎相同,所以我們將以 AtomicInteger 為例子來介紹。
-
原子引用
-
原子陣列
-
欄位更新器
-
原子累加器
下面先討論原子整數類,以 AtomicInteger 為例討論它的api介面:通過觀察原始碼可以發現,AtomicInteger 內部都是通過cas的原理來實現的!!好像發現了新大陸! Test6.java
public static void main(String[] args) {
AtomicInteger i = new AtomicInteger(0);
// 獲取並自增(i = 0, 結果 i = 1, 返回 0),類似於 i++
System.out.println(i.getAndIncrement());
// 自增並獲取(i = 1, 結果 i = 2, 返回 2),類似於 ++i
System.out.println(i.incrementAndGet());
// 自減並獲取(i = 2, 結果 i = 1, 返回 1),類似於 --i
System.out.println(i.decrementAndGet());
// 獲取並自減(i = 1, 結果 i = 0, 返回 1),類似於 i--
System.out.println(i.getAndDecrement());
// 獲取並加值(i = 0, 結果 i = 5, 返回 0)
System.out.println(i.getAndAdd(5));
// 加值並獲取(i = 5, 結果 i = 0, 返回 0)
System.out.println(i.addAndGet(-5));
// 獲取並更新(i = 0, p 為 i 的當前值, 結果 i = -2, 返回 0)
// 函數語言程式設計介面,其中函式中的操作能保證原子,但函式需要無副作用
System.out.println(i.getAndUpdate(p -> p - 2));
// 更新並獲取(i = -2, p 為 i 的當前值, 結果 i = 0, 返回 0)
// 函數語言程式設計介面,其中函式中的操作能保證原子,但函式需要無副作用
System.out.println(i.updateAndGet(p -> p + 2));
// 獲取並計算(i = 0, p 為 i 的當前值, x 為引數1, 結果 i = 10, 返回 0)
// 函數語言程式設計介面,其中函式中的操作能保證原子,但函式需要無副作用
// getAndUpdate 如果在 lambda 中引用了外部的區域性變數,要保證該區域性變數是 final 的
// getAndAccumulate 可以通過 引數1 來引用外部的區域性變數,但因為其不在 lambda 中因此不必是 final
System.out.println(i.getAndAccumulate(10, (p, x) -> p + x));
// 計算並獲取(i = 10, p 為 i 的當前值, x 為引數1值, 結果 i = 0, 返回 0)
// 函數語言程式設計介面,其中函式中的操作能保證原子,但函式需要無副作用
System.out.println(i.accumulateAndGet(-10, (p, x) -> p + x));
}
6.4 原子引用
為什麼需要原子引用型別?保證引用型別的共享變數是執行緒安全的(確保這個原子引用沒有引用過別人)。
基本型別原子類只能更新一個變數,如果需要原子更新多個變數,需要使用引用型別原子類。
- AtomicReference:引用型別原子類
- AtomicStampedReference:原子更新帶有版本號的引用型別。該類將整數值與引用關聯起來,可用於解決原子的更新資料和資料的版本號,可以解決使用 CAS 進行原子更新時可能出現的 ABA 問題。
- AtomicMarkableReference :原子更新帶有標記的引用型別。該類將 boolean 標記與引用關聯起來,也可以解決使用 CAS 進行原子更新時可能出現的 ABA 問題。
使用原子引用實現BigDecimal存取款的執行緒安全:Test7.java
下面這個是不安全的實現過程:
class DecimalAccountUnsafe implements DecimalAccount {
BigDecimal balance;
public DecimalAccountUnsafe(BigDecimal balance) {
this.balance = balance;
}
@Override
public BigDecimal getBalance() {
return balance;
}
@Override
public void withdraw(BigDecimal amount) {
BigDecimal balance = this.getBalance();
this.balance = balance.subtract(amount);
}
}
解決程式碼如下:在AtomicReference類中,存在一個value型別的變數,儲存對BigDecimal物件的引用。
class DecimalAccountCas implements DecimalAccount{
//private BigDecimal balance;
private AtomicReference<BigDecimal> balance ;
public DecimalAccountCas(BigDecimal balance) {
this.balance = new AtomicReference<>(balance);
}
@Override
public BigDecimal getBalance() {
return balance.get();
}
@Override
public void withdraw(BigDecimal amount) {
while(true){
BigDecimal pre = balance.get();
// 注意:這裡的balance返回的是一個新的物件,即 pre!=next
BigDecimal next = pre.subtract(amount);
if (balance.compareAndSet(pre,next)){
break;
}
}
}
}
ABA 問題及解決
ABA 問題:Test8.java 如下程式所示,雖然再other方法中存在兩個執行緒對共享變數進行了修改,但是修改之後又變成了原值,main執行緒中對此是不可見得,這種操作這對業務程式碼並無影響。
static AtomicReference<String> ref = new AtomicReference<>("A");
public static void main(String[] args) throws InterruptedException {
log.debug("main start...");
// 獲取值 A
// 這個共享變數被它執行緒修改
String prev = ref.get();
other();
utils.sleep(1);
// 嘗試改為 C
log.debug("change A->C {}", ref.compareAndSet(prev, "C"));
}
private static void other() {
new Thread(() -> {
log.debug("change A->B {}", ref.compareAndSet(ref.get(), "B"));
}, "t1").start();
utils.sleep(1);
new Thread(() -> {
// 注意:如果這裡使用 log.debug("change B->A {}", ref.compareAndSet(ref.get(), new String("A")));
// 那麼此實驗中的 log.debug("change A->C {}", ref.compareAndSet(prev, "C"));
// 列印的就是false, 因為new String("A") 返回的物件的引用和"A"返回的物件的引用時不同的!
log.debug("change B->A {}", ref.compareAndSet(ref.get(), "A"));
}, "t2").start();
}
主執行緒僅能判斷出共享變數的值與最初值 A 是否相同,不能感知到這種從 A 改為 B 又改回 A 的情況,如果主執行緒希望:只要有其它執行緒【動過了】共享變數,那麼自己的 cas 就算失敗,這時,僅比較值是不夠的,需要再加一個版本號。使用AtomicStampedReference來解決。
AtomicStampedReference
解決ABA問題 Test9.java
static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A",0);
public static void main(String[] args) throws InterruptedException {
log.debug("main start...");
// 獲取值 A
int stamp = ref.getStamp();
log.info("{}",stamp);
String prev = ref.getReference();
other();
utils.sleep(1);
// 嘗試改為 C
log.debug("change A->C {}", ref.compareAndSet(prev, "C",stamp,stamp+1));
}
private static void other() {
new Thread(() -> {
int stamp = ref.getStamp();
log.info("{}",stamp);
log.debug("change A->B {}", ref.compareAndSet(ref.getReference(), "B",stamp,stamp+1));
}, "t1").start();
utils.sleep(1);
new Thread(() -> {
int stamp = ref.getStamp();
log.info("{}",stamp);
log.debug("change B->A {}", ref.compareAndSet(ref.getReference(), "A",stamp,stamp+1));
}, "t2").start();
}
AtomicMarkableReference
AtomicStampedReference 可以給原子引用加上版本號,追蹤原子引用整個的變化過程,如:A -> B -> A ->C,通過AtomicStampedReference,我們可以知道,引用變數中途被更改了幾次。但是有時候,並不關心引用變數更改了幾次,只是單純的關心是否更改過,所以就有了AtomicMarkableReference Test10.java
6.5 原子陣列
使用原子的方式更新數組裡的某個元素
- AtomicIntegerArray:整形陣列原子類
- AtomicLongArray:長整形陣列原子類
- AtomicReferenceArray :引用型別陣列原子類
上面三個類提供的方法幾乎相同,所以我們這裡以 AtomicIntegerArray 為例子來介紹。例項程式碼:Test11.java
我們將使用函數語言程式設計來實現,先看看一些函數語言程式設計的介面的javadoc文件
Represents a supplier of results.
表示supplier的結果。
There is no requirement that a new or distinct result be returned each time the supplier is invoked.
不要求每次呼叫供應商時都返回一個新的或不同的結果。
This is a functional interface whose functional method is get().
這是一個函式介面,其函式方法是get()。
public interface Supplier<T> {
/**
* Gets a result.
* @return a result
*/
T get();
}
Represents a function that accepts one argument and produces a result.
表示接受一個引數並生成結果的函式。
This is a functional interface whose functional method is apply(Object).
這是一個函式介面,其函式方法是apply(Object)。
public interface Function<T, R> {
/**
* Applies this function to the given argument.
* @param t the function argument
* @return the function result
*/
R apply(T t);
//....
}
Represents an operation that accepts two input arguments and returns no result. This is the two-arity specialization of Consumer. Unlike most other functional interfaces, BiConsumer is expected to operate via side-effects.
表示接受兩個輸入引數且不返回結果的操作。這就是Consumer的二元引數版本。與大多數其他功能性介面不同,BiConsumer期望執行帶有副作用的操作。
This is a functional interface whose functional method is accept(Object, Object).
這是一個函式介面,其函式方法是accept(Object,Object)。
public interface BiConsumer<T, U> {
void accept(T t, U u);
//....
}
Represents an operation that accepts a single input argument and returns no result. Unlike most other functional interfaces, Consumer is expected to operate via side-effects.
表示接受單個輸入引數但不返回結果的操作。與大多數其他功能介面不同,消費者期望執行帶有副作用的操作。
public interface Consumer<T> {
void accept(T t);
//....
}
6.6 欄位更新器
AtomicReferenceFieldUpdater // 域 欄位 ,AtomicIntegerFieldUpdater,AtomicLongFieldUpdater
注意:利用欄位更新器,可以針對物件的某個域(Field)進行原子操作,只能配合 volatile 修飾的欄位使用,否則會出現異常 Test12.java
Exception in thread "main" java.lang.IllegalArgumentException: Must be volatile type
6.7 原子累加器
累加器效能比較
LongAdder累加器的使用
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
demo(() -> new LongAdder(), adder -> adder.increment());
}
for (int i = 0; i < 5; i++) {
demo(() -> new AtomicLong(), adder -> adder.getAndIncrement());
}
}
private static <T> void demo(Supplier<T> adderSupplier, Consumer<T> action) {
T adder = adderSupplier.get();
long start = System.nanoTime();
List<Thread> ts = new ArrayList<>();
// 4 個執行緒,每人累加 50 萬
for (int i = 0; i < 40; i++) {
ts.add(new Thread(() -> {
for (int j = 0; j < 500000; j++) {
action.accept(adder);
}
}));
}
ts.forEach(t -> t.start());
ts.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long end = System.nanoTime();
System.out.println(adder + " cost:" + (end - start)/1000_000);
}
效能提升的原因很簡單,就是在有競爭時,設定多個累加單元(但不會超過cpu的核心數),Therad-0 累加 Cell[0],而 Thread-1 累加Cell[1]... 最後將結果彙總。這樣它們在累加時操作的不同的 Cell 變數,因此減少了 CAS 重試失敗,從而提高效能。
原始碼之 LongAdder
LongAdder 類有幾個關鍵域
// 累加單元陣列, 懶惰初始化
transient volatile Cell[] cells;
// 基礎值, 如果沒有競爭, 則用 cas 累加這個域
transient volatile long base;
// 在 cells 建立或擴容時, 置為 1, 表示加鎖
transient volatile int cellsBusy;
cas 鎖
使用cas實現一個自旋鎖
// 不要用於生產實踐!!!
public class LockCas {
private AtomicInteger state = new AtomicInteger(0);
public void lock() {
while (true) {
if (state.compareAndSet(0, 1)) {
break;
}
}
}
public void unlock() {
log.debug("unlock...");
state.set(0);
}
}
測試
LockCas lock = new LockCas();
new Thread(() -> {
log.debug("begin...");
lock.lock();
try {
log.debug("lock...");
sleep(1);
} finally {
lock.unlock();
}
}).start();
new Thread(() -> {
log.debug("begin...");
lock.lock();
try {
log.debug("lock...");
} finally {
lock.unlock();
}
}).start();
原理之偽共享
其中 Cell 即為累加單元
// 防止快取行偽共享
@sun.misc.Contended
static final class Cell {
volatile long value;
Cell(long x) { value = x; }
// 最重要的方法, 用來 cas 方式進行累加, prev 表示舊值, next 表示新值
final boolean cas(long prev, long next) {
return UNSAFE.compareAndSwapLong(this, valueOffset, prev, next);
}
// 省略不重要程式碼
}
下面討論@sun.misc.Contended註解的重要意義
得從快取說起,快取與記憶體的速度比較
因為 CPU 與 記憶體的速度差異很大,需要靠預讀資料至快取來提升效率。快取離cpu越近速度越快。 而快取以快取行為單位,每個快取行對應著一塊記憶體,一般是 64 byte(8 個 long),快取的加入會造成資料副本的產生,即同一份資料會快取在不同核心的快取行中,CPU 要保證資料的一致性,如果某個 CPU 核心更改了資料,其它 CPU 核心對應的整個快取行必須失效。
因為 Cell 是陣列形式,在記憶體中是連續儲存的,一個 Cell 為 24 位元組(16 位元組的物件頭和 8 位元組的 value),因 此快取行可以存下 2 個的 Cell 物件。這樣問題來了: Core-0 要修改 Cell[0],Core-1 要修改 Cell[1]
無論誰修改成功,都會導致對方 Core 的快取行失效,比如 Core-0 中 Cell[0]=6000, Cell[1]=8000 要累加 Cell[0]=6001, Cell[1]=8000 ,這時會讓 Core-1 的快取行失效,@sun.misc.Contended 用來解決這個問題,它的原理是在使用此註解的物件或欄位的前後各增加 128 位元組大小的padding,從而讓 CPU 將物件預讀至快取時佔用不同的快取行,這樣,不會造成對方快取行的失效
再來看看LongAdder類的累加increment()方法中又主要呼叫下面的方法
public void add(long x) {
// as 為累加單元陣列
// b 為基礎值
// x 為累加值
Cell[] as; long b, v; int m; Cell a;
// 進入 if 的兩個條件
// 1. as 有值, 表示已經發生過競爭, 進入 if
// 2. cas 給 base 累加時失敗了, 表示 base 發生了競爭, 進入 if
if ((as = cells) != null || !casBase(b = base, b + x)) {
// uncontended 表示 cell 沒有競爭
boolean uncontended = true;
if (
// as 還沒有建立
as == null || (m = as.length - 1) < 0 ||
// 當前執行緒對應的 cell 還沒有被建立,a為當執行緒的cell
(a = as[getProbe() & m]) == null ||
// 給當前執行緒的 cell 累加失敗 uncontended=false ( a 為當前執行緒的 cell )
!(uncontended = a.cas(v = a.value, v + x))
) {
// 進入 cell 陣列建立、cell 建立的流程
longAccumulate(x, null, uncontended);
}
}
}
add 方法分析
add 流程圖
final void longAccumulate(long x, LongBinaryOperator fn,
boolean wasUncontended) {
int h;
// 當前執行緒還沒有對應的 cell, 需要隨機生成一個 h 值用來將當前執行緒繫結到 cell
if ((h = getProbe()) == 0) {
// 初始化 probe
ThreadLocalRandom.current();
// h 對應新的 probe 值, 用來對應 cell
h = getProbe();
wasUncontended = true;
}
// collide 為 true 表示需要擴容
boolean collide = false;
for (;;) {
Cell[] as; Cell a; int n; long v;
// 已經有了 cells
if ((as = cells) != null && (n = as.length) > 0) {
// 但是還沒有當前執行緒對應的 cell
if ((a = as[(n - 1) & h]) == null) {
// 為 cellsBusy 加鎖, 建立 cell, cell 的初始累加值為 x
// 成功則 break, 否則繼續 continue 迴圈
if (cellsBusy == 0) { // Try to attach new Cell
Cell r = new Cell(x); // Optimistically create
if (cellsBusy == 0 && casCellsBusy()) {
boolean created = false;
try { // Recheck under lock
Cell[] rs; int m, j;
if ((rs = cells) != null &&
(m = rs.length) > 0 &&
// 判斷槽位確實是空的
rs[j = (m - 1) & h] == null) {
rs[j] = r;
created = true;
}
} finally {
cellsBusy = 0;
}
if (created)
break;
continue; // Slot is now non-empty
}
}
// 有競爭, 改變執行緒對應的 cell 來重試 cas
else if (!wasUncontended)
wasUncontended = true;
// cas 嘗試累加, fn 配合 LongAccumulator 不為 null, 配合 LongAdder 為 null
else if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
break;
// 如果 cells 長度已經超過了最大長度, 或者已經擴容, 改變執行緒對應的 cell 來重試 cas
else if (n >= NCPU || cells != as)
collide = false;
// 確保 collide 為 false 進入此分支, 就不會進入下面的 else if 進行擴容了
else if (!collide)
collide = true;
// 加鎖
else if (cellsBusy == 0 && casCellsBusy()) {
// 加鎖成功, 擴容
continue;
}
// 改變執行緒對應的 cell
h = advanceProbe(h);
}
// 還沒有 cells, cells==as是指沒有其它執行緒修改cells,as和cells引用相同的物件,使用casCellsBusy()嘗試給 cellsBusy 加鎖
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
// 加鎖成功, 初始化 cells, 最開始長度為 2, 並填充一個 cell
// 成功則 break;
boolean init = false;
try { // Initialize table
if (cells == as) {
Cell[] rs = new Cell[2];
rs[h & 1] = new Cell(x);
cells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
// 上兩種情況失敗, 嘗試給 base 使用casBase累加
else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
break;
}
}
上圖中的第一個else if 中程式碼的邏輯,這是cells未建立時的處理邏輯。
上圖中的if 中程式碼的邏輯,裡面包含執行緒對應的cell已經建立好和沒建立好的兩種情況。
執行緒對應的cell還沒建立好,則執行的是第一個紅框裡的程式碼,邏輯如下
執行緒對應的cell已經建立好,則執行的是第二個紅框裡的程式碼,邏輯如下
sum 方法分析
獲取最終結果通過 sum 方法,將各個累加單元的值加起來就得到了總的結果。
public long sum() {
Cell[] as = cells; Cell a;
long sum = base;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
6.8 Unsafe
概述
Unsafe 物件提供了非常底層的,操作記憶體、執行緒的方法,Unsafe 物件不能直接呼叫,只能通過反射獲得。LockSupport的park方法,cas相關的方法底層都是通過Unsafe類來實現的。Test14.java
static Unsafe unsafe;
static {
try {
// Unsafe 使用了單例模式,unsafe物件是類中的一個私有的變數
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
unsafe = (Unsafe) theUnsafe.get(null);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new Error(e);
}
}
static Unsafe getUnsafe() {
return unsafe;
}
Unsafe CAS 操作
- 使用Unsafe 進行cas操作:Test15.java
- 使用自定義的 AtomicData 實現之前執行緒安全的原子整數 ,並用之前的取款例項來進行驗證 Test16.java
6.9總結
- CAS 與 volatile
- juc包下API
- 原子整數
- 原子引用
- 原子陣列
- 欄位更新器
- 原子累加器
- Unsafe
- 原理方面
- LongAdder 原始碼
- 偽共享
7. 共享模型之不可變
7.1 日期轉換的問題
問題提出,下面的程式碼在執行時,由於 SimpleDateFormat 不是執行緒安全的,有很大機率出現java.lang.NumberFormatException
或者出現不正確的日期解析結果。
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
log.debug("{}", sdf.parse("1951-04-21"));
} catch (Exception e) {
log.error("{}", e);
}
}).start();
}
思路 - 不可變物件
如果一個物件在不能夠修改其內部狀態(屬性),那麼它就是執行緒安全的,因為不存在併發修改啊!這樣的物件在 Java 中有很多,例如在 Java 8 後,提供了一個新的日期格式化類DateTimeFormatter:
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
LocalDate date = dtf.parse("2018-10-01", LocalDate::from);
log.debug("{}", date);
}).start();
}
7.2 不可變設計
另一個大家更為熟悉的 String 類也是不可變的,以它為例,說明一下不可變類設計的要素
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
// ...
}
final 的使用
發現該類、類中所有屬性都是 final 的,屬性用 final 修飾保證了該屬性是隻讀的,不能修改,類用 final 修飾保證了該類中的方法不能被覆蓋,防止子類無意間破壞不可變性。
保護性拷貝
但有同學會說,使用字串時,也有一些跟修改相關的方法啊,比如 substring 等,那麼下面就看一看這些方法是 如何實現的,就以 substring 為例:
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
// 上面是一些校驗,下面才是真正的建立新的String物件
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
發現其內部是呼叫 String 的構造方法建立了一個新字串,再進入這個構造看看,是否對 final char[] value 做出 了修改:結果發現也沒有,構造新字串物件時,會生成新的 char[] value,對內容進行復制。這種通過建立副本物件來避免共享的手段稱之為【保護性拷貝(defensive copy)】
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count <= 0) {
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
if (offset <= value.length) {
this.value = "".value;
return;
}
}
// Note: offset or count might be near -1>>>1.
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
// 上面是一些安全性的校驗,下面是給String物件的value賦值,新建立了一個數組來儲存String物件的值
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
模式之享元
-
簡介定義英文名稱:Flyweight pattern. 當需要重用數量有限的同一類物件時
-
體現
-
在JDK中 Boolean,Byte,Short,Integer,Long,Character 等包裝類提供了 valueOf 方法,例如 Long 的valueOf 會快取 -128~127 之間的 Long 物件,在這個範圍之間會重用物件,大於這個範圍,才會新建 Long 物件:
public static Long valueOf(long l) { final int offset = 128; if (l >= -128 && l <= 127) { // will cache return LongCache.cache[(int)l + offset]; } return new Long(l); }
注意: > Byte, Short, Long 快取的範圍都是 -128~127 > Character 快取的範圍是 0~127 > Integer的預設範圍是 -128~127,最小值不能變,但最大值可以通過調整虛擬機器引數
> "-Djava.lang.Integer.IntegerCache.high
"來改變 > Boolean 快取了 TRUE 和 FALSE -
String 串池
-
BigDecimal BigInteger
-
-
diy:例如:一個線上商城應用,QPS 達到數千,如果每次都重新建立和關閉資料庫連線,效能會受到極大影響。 這時預先建立好一批連線,放入連線池。一次請求到達後,從連線池獲取連線,使用完畢後再還回連線池,這樣既節約了連線的建立和關閉時間,也實現了連線的重用,不至於讓龐大的連線數壓垮資料庫。 Test17.java
final的原理
-
設定 final 變數的原理
-
理解了 volatile 原理,再對比 final 的實現就比較簡單了
public class TestFinal {final int a=20;}
位元組碼
0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: bipush 20 7: putfield #2 // Field a:I <-- 寫屏障 10: return
-
final變數的賦值操作都必須在定義時或者構造器中進行初始化賦值,並發現 final 變數的賦值也會通過 putfield 指令來完成,同樣在這條指令之後也會加入寫屏障,保證在其它執行緒讀到它的值時不會出現為 0 的情況。
-
-
獲取 final 變數的原理:從位元組碼的層面去理解視訊。
7.3 本章小結
- 不可變類使用
- 不可變類設計
- 原理方面:final
- 模式方面
- 享元模式-> 設定執行緒池
問題
- 什麼時候將導致使用者態到核心態的轉變?在synchroniezed進行加鎖的時候。
- final是怎麼優化讀取速度的?複習完jvm再看就懂了。視訊