【多執行緒與高併發 2】volatile 篇

和樹不困發表於2020-12-22

多執行緒與高併發 2

volatile 篇


偏向鎖 -> 迴圈鎖 -> 重量級鎖

synchronized(只能使用Object)

當執行緒≥2時,自旋鎖;當自旋次數>10時,重量級鎖。


Lock( )CAS使用自旋

synchronized( )使用鎖升級


volatile // 可變的,易變的

保證執行緒可見性,禁止指令重排序。

保證執行緒可見性:一個類的值給兩個類同時呼叫,裡面的變數改變後無法輕易發現(執行緒之間不可見)。

volatile可以讓一個執行緒發生改變之後,另一個執行緒可以馬上知道。

// 原理:CPU的快取一致性協議。

禁止指令重排序:CPU迸發執行指令,所以會對指令重新排序,加了volatile來保證重排序。

單例:保證在JVM記憶體永遠只有某一個類的一個例項(比如:許可權管理者)。


  • 餓漢式:(定義類的時候就例項化方法)
public class Mgr01{
  private static final Mgr01 INSTANCE = new Mgr01();
  private Mgr01(){};
  public static Mgr01 getInstance(){return INSTANCE;}
  public void m() {System.out.println("m");}
  public static void main(String[] args){
    Mgr01 m1 = Mgr01.getInstance();
    Mgr01 m2 = Mgr01.getInstance();
    System.out.println(m1 == m2);
  }
}
  • 懶漢式:什麼時候呼叫方法什麼時候初始化
public class Lazy{
    private Lazy(){}
    //預設不會例項化,什麼時候用什麼時候new
        private static Lazy lazy=null;
        public static synchronized Lazy getInstance(){
            if(lazy==null){
                lazy=new Lazy();
        	}
        	return lazy;
    	}
}
餓漢式懶漢式
安全
節省記憶體
  • 懶漢餓漢合併:

類的定義:

public class Mgr01{
  private /*volatile*/ static Mgr0x INSTANCE;
  private Mgr0x(){};
  public static Mgr01 getInstance(){
  	//以下所有程式碼寫的都是這一個方法
  }
}

以下所有方法寫的都是上面的 getInstance()方法。

以上方法沒有加volatile,最後會寫上。


  • 直接判斷null
// 先判斷是否為空 然後再那啥:
public static Mgr03 getInstance(){
    if(INSTANCE == null){
        try{
            Thread.sleep(1);
        }catch(InterruotedException e){
            e.printStackTrace();
        }
        INSTANCE = new Mgr03();
    }
    return INSTANCE;
}

  ↑   ↑   ↑   ↑    ↑ 這是一種錯誤的書寫方式,自己抿;

  • 先鎖再null
public static synchronized Mgr04 getInstance(){
    if(INSTANCE == null){
        try{
            Thread.sleep(1);
        }catch(InterruotedException e){
            e.printStackTrace();
        }
        INSTANCE = new Mgr04();
    }
    return INSTANCE;
}

↑ ↑ ↑ ↑ ↑ 修改正確,但是違背了  能不加鎖就不加鎖  原則。

  • 鎖細化:
public static Mgr05 getInstance(){
    if(INSTANCE == null){
        synchronized (Mgr05.class){
            try{
                Thread.sleep(1);
            }catch(InterruotedException e){
                e.printStackTrace();
            }
            INSTANCE = new Mgr05();
        }
    }
    return INSTANCE;
}

↑ ↑ ↑ ↑ ↑ 這也是一種錯誤的書寫方式(重複初始化);

  • 雙重檢查:
public static Mgr05 getInstance(){
    if(INSTANCE == null){
        synchronized (Mgr05.class){
            if(INSTANCE == null){
                try{
                    Thread.sleep(1);
                }catch(InterruotedException e){
                    e.printStackTrace();
                }
                INSTANCE = new Mgr05();
            }
        }
    }
    return INSTANCE;
}

↑ ↑ ↑ ↑ ↑ 修改正確…而且鎖不載入外面,效率增高~~

  • 關於volatile(主要是   指令重排序   )超高超高迸發的情況可能會發生:
// new物件的三步
INSTANCE = new Mgr06();

1. 申請記憶體並給初始值(int = 0,String = null;)
2. 修改值
3. 將值給物件

volatile 防止第二步第三步會顛倒;

  • 一個求結果是 100000 的小程式
public class T{
    volatile int count = 0; // 加上vilatile
    synchronized void m(){  // 加上 synchronized
        for(int i=0;i<10000;i++){count++;}
    }


    public static void main(String[] args){
        T t = new T();
        List<Thread> threads = new ArraysList<~>();

        for(int i=0;i<10;i++){
            threads.add(new Thread(t::m,"threads-"+i));
        }

        threads.forEach((o)->o.start());

        threads.forEach((o)->{
          try{
              o.join();
          } catch(InterruptedException e){
              e.printStackTrace();
          }
        });
            System.out.println(t.count);
    }
}

只有加上了 synchronized & volatile 才能執行出正確結果,其中 synchronized 用來保證原子性


鎖優化


  • 鎖力度變小(爭用不是很激烈的話)

如果有一群要爭用的程式碼,那麼可以將方法上的 synchronized 寫到 count++ 上;

  • 鎖力度變大(爭用很激烈很頻繁的話)

假如一個方法裡面 總共 20 行程式碼,加了19行,那不如直接用一個大的鎖。


鎖的物件被呼叫


public class = T{
    Object o = new Object();// 錯誤修改點
    
    synchronized(0){
        sout("123");
    }

    public void zbc(){
        T t = new T();
        t.o = "a";
    }

↑   ↑   ↑  以上程式碼錯誤!以下為修改 ↓   ↓   ↓

final Object o = new Object();

有些類在建立的時候直接加了鎖


Atomic 開頭的 ( AtomicInteger count = new AtomicInteger( ); // 讓count進行原子性加減)


CAS ( Compare And Set ) 無鎖優化 樂觀鎖

在請求的時候就樂觀的認為 程式碼裡的值就是我的期望值


cas (V ,Expected,NewValue){
    if (V == Expected){
        V = NewValue;
	}else{
        tryAgain or fail;
    }
}

↑ ↑ ↑  以上是在CPU 原語上的支援,不能被打斷。


  • ABA 問題(與前女友複合之後,其實她已經經歷了n個男人;)

有個物件 object == 1;想使用cas把它變成2:

cas(object,1,2);//沒有執行緒進行操作,可以進行更改

如果在更改的時候有一個執行緒給 object 改成了2,然後又改成了 1 ;在基礎型別(如:int)沒有影響,但是 Object 物件有影響;

解決方法:做 cas 的時候加個版本號:version

解決方法:使用 AutomicStampedReference ( unsafe 什麼時候呼叫什麼時候返回這個值 )


相關文章