JUC筆記(4)

qq_43378019發表於2020-12-31

16. JMM

請你談談你對 Volatile 的理解:

Volatile是java虛擬機器提供 輕量級的同步機制

1、保證可見性

2、不保證原子性

3、禁止指令重排序

什麼是JMM

JMM : Java記憶體模型,不存在的東西,概念!約定!  
關於JMM的一些同步的約定: 

1、執行緒解鎖前,必須把共享變數立刻重新整理回主存

2、執行緒加鎖前,必須讀取主存中的最新值到工作記憶體中

3、加鎖和解鎖是同一把鎖!

執行緒  工作記憶體 、主記憶體

記憶體互動操作有8種,虛擬機器實現必須保證每一個操作都是原子的,不可在分的(對於double和long類 型的變數來說,load、store、read和write操作在某些平臺上允許例外)

  • lock   (鎖定):作用於主記憶體的變數,把一個變數標識為執行緒獨佔狀態
  • unlock (解鎖):作用於主記憶體的變數,它把一個處於鎖定狀態的變數釋放出來,釋放後的變數 才可以被其他執行緒鎖定
  • read  (讀取):作用於主記憶體變數,它把一個變數的值從主記憶體傳輸到執行緒的工作記憶體中,以便 隨後的load動作使用
  • load   (載入):作用於工作記憶體的變數,它把read操作從主存中變數放入工作記憶體中
  • use   (使用):作用於工作記憶體中的變數,它把工作記憶體中的變數傳輸給執行引擎,每當虛擬機器 遇到一個需要使用到變數的值,就會使用到這個指令
  • assign (賦值):作用於工作記憶體中的變數,它把一個從執行引擎中接受到的值放入工作記憶體的變 量副本中
  • store  (儲存):作用於主記憶體中的變數,它把一個從工作記憶體中一個變數的值傳送到主記憶體中, 以便後續的write使用
  • write  (寫入):作用於主記憶體中的變數,它把store操作從工作記憶體中得到的變數的值放入主內 存的變數中

JMM對這八種指令的使用,制定瞭如下規則:

  • 不允許read和load、store和write操作之一單獨出現。即使用了read必須load,使用了store必須 write
  • 不允許執行緒丟棄他近的assign操作,即工作變數的資料改變了之後,必須告知主存
  • 不允許一個執行緒將沒有assign的資料從工作記憶體同步回主記憶體
  • 一個新的變數必須在主記憶體中誕生,不允許工作記憶體直接使用一個未被初始化的變數。就是懟變數 實施use、store操作之前,必須經過assign和load操作
  • 一個變數同一時間只有一個執行緒能對其進行lock。多次lock後,必須執行相同次數的unlock才能解 鎖 如果對一個變數進行lock操作,會清空所有工作記憶體中此變數的值,在執行引擎使用這個變數前, 必須重新load或assign操作初始化變數的值
  • 如果一個變數沒有被lock,就不能對其進行unlock操作。也不能unlock一個被其他執行緒鎖住的變數
  • 對一個變數進行unlock操作之前,必須把此變數同步回主記憶體

問題: 程式不知道主記憶體的值已經被修改過了

17. Volatile

(1) 保證可見性

public class JMMDemo {
    // 不加 volatile 程式就會死迴圈!
    // 加 volatile 可以保證可見性
    private volatile static int num = 0;

    public static void main(String[] args) { // main

        new Thread(()->{ // 執行緒 1 對主記憶體的變化不知道的
            while (num==0){

            }
        }).start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        num = 1;
        System.out.println(num);
    }
}

num的修改被thread可見了

(2) 不保證原子性

原子性 : 不可分割
執行緒A在執行任務的時候,不能被打擾的,也不能被分割。要麼同時成功,要麼同時失敗。

// volatile 不保證原子性
public class VDemo02 {

    // volatile 不保證原子性
    // 原子類的 Integer
    private volatile static AtomicInteger num = new AtomicInteger();

    public static void add(){
        // num++; // 不是一個原子性操作
        num.getAndIncrement(); // AtomicInteger + 1 方法, CAS
    }

    public static void main(String[] args) {

        //理論上num結果應該為 2 萬
        for (int i = 1; i <= 20; i++) {
            new Thread(()->{
                for (int j = 0; j < 1000 ; j++) {
                    add();
                }
            }).start();
        }

        while (Thread.activeCount()>2){ // main  gc
            Thread.yield();
        }

        System.out.println(Thread.currentThread().getName() + " " + num);

    }
}

但是如果不加 lock 和 synchronized ,怎麼樣保證原子性?

num++其實不是個原子性操作。

因此使用原子類,解決原子性問題,沒必要lock:

這些類的底層都直接和作業系統掛鉤!在記憶體中修改值!Unsafe類是一個很特殊的存在!

    /**
     * Atomically increments by one the current value.
     *
     * @return the previous value
     */
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

(3) 指令重排

什麼是 指令重排:你寫的程式,計算機並不是按照你寫的那樣去執行的。
原始碼-->編譯器優化的重排--> 指令並行也可能會重排--> 記憶體系統也會重排--->  執行
處理器在進行指令重排的時候,考慮:資料之間的依賴性!

int x = 1; // 1

int y = 2; // 2

x = x + 5; // 3

y = x * x; // 4
我們所期望的:1234  但是可能執行的時候回變成 2134  1324 可不可能是  4123!

可能造成影響的結果: a b x y 這四個值預設都是 0;
volatile可以避免指令重排: 記憶體屏障。CPU指令。

作用:
1、保證特定的操作的執行順序!

2、可以保證某些變數的記憶體可見性 (利用這些特性volatile實現了可見性)

Volatile 是可以保持 可見性。不能保證原子性,由於記憶體屏障,可以保證避免指令重排的現象產生! 

18. 徹底玩轉單例模式

記憶體屏障在單例模式使用最多

餓漢式

// 餓漢式單例
public class Hungry {

    // 可能會浪費空間
    private byte[] data1 = new byte[1024*1024];
    private byte[] data2 = new byte[1024*1024];
    private byte[] data3 = new byte[1024*1024];
    private byte[] data4 = new byte[1024*1024];

    private Hungry(){

    }

    private final static Hungry HUNGRY = new Hungry();

    public static Hungry getInstance(){
        return HUNGRY;
    }

}

DCL 懶漢式(鎖):

問題:為什麼加volatile?因為volatile可以避免指令重排

// 懶漢式單例
// 道高一尺,魔高一丈!
/**
lazyMan = new LazyMan();
 * 1. 分配記憶體空間
 * 2、執行構造方法,初始化物件
 * 3、把這個物件指向這個空間
 *
 * 123
 * 132 A
 *     B // 此時lazyMan還沒有完成構造
 */
public class LazyMan {

    private static boolean flag = false;

    private LazyMan(){
        synchronized (LazyMan.class){
            if (flag == false){
                flag = true;
            }else {
                throw new RuntimeException("不要試圖使用反射破壞異常");
            }
        }
    }

    private volatile static LazyMan lazyMan;

    // 雙重檢測鎖模式的 懶漢式單例  DCL懶漢式
    public static LazyMan getInstance(){
        if (lazyMan==null){
            synchronized (LazyMan.class){
                if (lazyMan==null){
                    lazyMan = new LazyMan(); // 不是一個原子性操作
                }
            }
        }
        return lazyMan;
    }
    // 反射!
    public static void main(String[] args) throws Exception {
//        LazyMan instance = LazyMan.getInstance();

        Field qinjiang = LazyMan.class.getDeclaredField("flag");
        flag.setAccessible(true);

        Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);//獲得空參構造器
        declaredConstructor.setAccessible(true);
        LazyMan instance = declaredConstructor.newInstance();

        flag.set(instance,false);

        LazyMan instance2 = declaredConstructor.newInstance();

        System.out.println(instance);
        System.out.println(instance2);
    }
}

這樣雖然解決了問題,但是因為用到了synchronized,會導致很大的效能開銷,並且加鎖其實只需要在第一次初始化的時候用到,之後的呼叫都沒必要再進行加鎖。

雙重檢查鎖(double checked locking)是對上述問題的一種優化。先判斷物件是否已經被初始化,再決定要不要加鎖。

此外,反射可以破壞這種單例,因此可以在無參構造裡丟擲異常。

靜態內部類

// 靜態內部類
public class Holder {
    private Holder(){

    }
    public static Holder getInstace(){
        return InnerClass.HOLDER;
    }
    public static class InnerClass{
        private static final Holder HOLDER = new Holder();
    }
}

列舉

// enum 是一個什麼? 本身也是一個Class類
public enum EnumSingle {

    INSTANCE;

    public EnumSingle getInstance(){
        return INSTANCE;
    }
}

class Test{

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        EnumSingle instance1 = EnumSingle.INSTANCE;
        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
        declaredConstructor.setAccessible(true);
        EnumSingle instance2 = declaredConstructor.newInstance();

        // NoSuchMethodException: com.kuang.single.EnumSingle.<init>()
        System.out.println(instance1);
        System.out.println(instance2);
    }
}

反射不能破壞列舉的單例模式。java.lang.enum  可以看到列舉沒有無參構造器。

用有參構造器:

19. 深入理解CAS

什麼是CAS: compare and set  比較並交換

大廠你必須要深入研究底層!有所突破! 修內功,作業系統,計算機網路

valueOffset:記憶體地址偏移值

Unsafe類:

var1+var2=var5,則var5=var5+1

自旋鎖:不停旋轉,直到這個值能夠成功為止

CAS:

比較當前工作記憶體中的值和主記憶體中的值,如果這個值是期望的,那麼執行操作!如果不是就一直迴圈!

好處:自帶原子性

缺點:

  • 迴圈會消耗CPU資源
  • 一次性只能保證一個共享變數的原子性
  • ABA問題

ABA問題:(狸貓換太子)

public class CASDemo {
    // CAS  compareAndSet : 比較並交換!
        public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(2020);
        // 期望、更新 public final boolean compareAndSet(int expect, int update);
        // 如果我期望的值達到了,那麼就更新,否則,就不更新, CAS 是CPU的併發原語!        // ============== 搗亂的執行緒 ==================        System.out.println(atomicInteger.compareAndSet(2020, 2021));        System.out.println(atomicInteger.get());
        System.out.println(atomicInteger.compareAndSet(2021, 2020));
        System.out.println(atomicInteger.get());
        // ============== 期望的執行緒 =================
        System.out.println(atomicInteger.compareAndSet(2020, 6666));
        System.out.println(atomicInteger.get());
        }
}

這就是樂觀鎖,只要判斷鎖沒被動過,就修改值。

樂觀鎖假設資料一般情況下不會造成衝突,所以在資料進行提交更新的時候,才會正式對資料的衝突與否進行檢測,如果發現衝突了,則返回給使用者錯誤的資訊,讓使用者決定如何去做。樂觀鎖適用於讀操作多的場景,這樣可以提高程式的吞吐量。

樂觀鎖是相對悲觀鎖而言,也是為了避免資料庫幻讀、業務處理時間過長等原因引起資料處理錯誤的一種機制,但樂觀鎖不會刻意使用資料庫本身的鎖機制,而是依據資料本身來保證資料的正確性。樂觀鎖的實現:

  • CAS 實現:Java 中java.util.concurrent.atomic包下面的原子變數使用了樂觀鎖的一種 CAS 實現方式。
  • 版本號控制:一般是在資料表中加上一個資料版本號 version 欄位,表示資料被修改的次數。當資料被修改時,version 值會+1。當執行緒A要更新資料值時,在讀取資料的同時也會讀取 version 值,在提交更新時,若剛才讀取到的 version 值與當前資料庫中的 version 值相等時才更新,否則重試更新操作,直到更新成功。

悲觀鎖分為共享鎖和排他鎖。

https://www.jianshu.com/p/d2ac26ca6525

20. 原子引用AtomicReference

解決ABA 問題,引入原子引用! 對應的思想:樂觀鎖!

帶版本號的原子操作!

public class CASDemo {

    //AtomicStampedReference 注意,如果泛型是一個包裝類,注意物件的引用問題

    // 正常在業務操作,這裡面比較的都是一個個物件;Integer不能超過-128~127
    static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1,1);

    // CAS  compareAndSet : 比較並交換!
    public static void main(String[] args) {

        new Thread(()->{
            int stamp = atomicStampedReference.getStamp(); // 獲得版本號
            System.out.println("a1=>"+stamp);

            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Lock lock = new ReentrantLock(true);
            atomicStampedReference.compareAndSet(1, 2,
                    atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);

            System.out.println("a2=>"+atomicStampedReference.getStamp());

            System.out.println(atomicStampedReference.compareAndSet(2, 1, atomicStampedReference.getStamp(),atomicStampedReference.getStamp() + 1));//版本號+1
            System.out.println("a3=>"+atomicStampedReference.getStamp());

        },"a").start();

        // 樂觀鎖的原理相同!
        new Thread(()->{
            int stamp = atomicStampedReference.getStamp(); // 獲得版本號
            System.out.println("b1=>"+stamp);

            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(atomicStampedReference.compareAndSet(1, 6,
                    stamp, stamp + 1));

            System.out.println("b2=>"+atomicStampedReference.getStamp());

        },"b").start();

    }
}

Integer 使用了物件快取機制,預設範圍是 -128 ~ 127 ,推薦使用靜態工廠方法 valueOf 獲取物件例項,而不是 new,因為 valueOf 使用快取,而 new 一定會建立新的物件分配新的記憶體空間;

21. 各種鎖的理解

(1) 公平鎖、非公平鎖 

公平鎖: 非常公平, 不能夠插隊,必須先來後到!

非公平鎖:非常不公平,可以插隊 (預設都是非公平)效率高

public ReentrantLock(){
    sync = new NonfairSync(); 
}
public ReentrantLock(boolean fair) {    
    sync = fair ? new FairSync() : new NonfairSync(); 
}

(2) 可重入鎖(遞迴鎖)

所有鎖都是可重入鎖。

// Synchronized
public class Demo01 {
    public static void main(String[] args) {
        Phone phone = new Phone();

        new Thread(()->{
            phone.sms();
        },"A").start();


        new Thread(()->{
            phone.sms();
        },"B").start();
    }
}
class Phone{

    public synchronized void sms(){
        System.out.println(Thread.currentThread().getName() + "sms");
        call(); // 這裡也有鎖
    }
    public synchronized void call(){
        System.out.println(Thread.currentThread().getName() + "call");
    }
}

可以看到儘管呼叫了其他方法,可以理解為同一把鎖。

Lock鎖:

public class Demo02 {
    public static void main(String[] args) {
        Phone2 phone = new Phone2();

        new Thread(()->{
            phone.sms();
        },"A").start();

        new Thread(()->{
            phone.sms();
        },"B").start();
    }
}
class Phone2{
    Lock lock = new ReentrantLock();

    public void sms(){
        lock.lock(); // 細節問題:lock.lock(); lock.unlock(); // lock 鎖必須配對,否則就會死在裡面
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "sms");
            call(); // 這裡也有鎖
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
            lock.unlock();
        }
    }
    public void call(){

        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "call");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

(3) 自旋鎖(spinlock)

不斷嘗試,直到成功

我們來自定義一個鎖測試

/**
 * 自旋鎖
 */
public class SpinlockDemo {

    // int 0
    // Thread null
    AtomicReference<Thread> atomicReference = new AtomicReference<>();
    // 加鎖
    public void myLock(){
        Thread thread = Thread.currentThread();
       
        // 自旋鎖
        while (!atomicReference.compareAndSet(null,thread)){
            //如果執行緒是空的,就把它丟進去無限迴圈
        }
 System.out.println(Thread.currentThread().getName() + "==> mylock");
    }
    // 解鎖
    public void myUnLock(){
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName() + "==> myUnlock");
        atomicReference.compareAndSet(thread,null);
    }
}

測試

public class TestSpinLock {
    public static void main(String[] args) throws InterruptedException {
//        ReentrantLock reentrantLock = new ReentrantLock();
//        reentrantLock.lock();
//        reentrantLock.unlock();

        // 底層使用的自旋鎖CAS
        SpinlockDemo lock = new SpinlockDemo();
        new Thread(()-> {
            lock.myLock();

            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.myUnLock();
            }

        },"T1").start();

        TimeUnit.SECONDS.sleep(1);

        new Thread(()-> {
            lock.myLock();

            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.myUnLock();
            }

        },"T2").start();

    }
}

 

t1先拿到鎖,t2卡在自旋前面,然後t1解鎖,t2才有資格拿到鎖並解鎖。

(4) 死鎖

死鎖是什麼: 兩個人互相搶奪資源

產生死鎖的原因主要是:

(1) 因為系統資源不足。
(2) 程式執行推進的順序不合適。
(3) 資源分配不當等。
如果系統資源充足,程式的資源請求都能夠得到滿足,死鎖出現的可能性就很低,否則就會因爭奪有限的資源而陷入死鎖。其次,程式執行推進順序與速度不同,也可能產生死鎖。

產生死鎖的四個必要條件:

死鎖的四要素:互斥使用資源,佔用等待資源,不可搶佔資源,迴圈等待資源

(1) 互斥條件:一個資源每次只能被一個程式使用。
(2) 請求與保持條件:一個程式因請求資源而阻塞時,對已獲得的資源保持不放。
(3) 不剝奪條件:程式已獲得的資源,在末使用完之前,不能強行剝奪。
(4) 迴圈等待條件:若干程式之間形成一種頭尾相接的迴圈等待資源關係。
這四個條件是死鎖的必要條件,只要系統發生死鎖,這些條件必然成立,而只要上述條件之一不滿足,就不會發生死鎖。

死鎖測試,怎麼排除死鎖

關於synchronized () 括號中應該傳什麼物件?https://blog.csdn.net/qq_35993946/article/details/86359250

public class DeadLockDemo {
    public static void main(String[] args) {

        String lockA = "lockA";
        String lockB = "lockB";//兩個鎖的物件

        new Thread(new MyThread(lockA, lockB), "T1").start();//A想拿B
        new Thread(new MyThread(lockB, lockA), "T2").start();

    }
}

class MyThread implements Runnable{

    private String lockA;
    private String lockB;

    public MyThread(String lockA, String lockB) {
        this.lockA = lockA;
        this.lockB = lockB;
    }
    @Override
    public void run() {
        synchronized (lockA){
            System.out.println(Thread.currentThread().getName() + "lock:"+lockA+"=>get"+lockB);

            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            synchronized (lockB){
                System.out.println(Thread.currentThread().getName() + "lock:"+lockB+"=>get"+lockA);
            }

        }
    }
}

解決問題:

1)使用 jps -l 定位程式號

獲得當前活著的一些程式

2)使用 jstack 程式號 找到死鎖問題:

jstack 11444

談談面試:工作中!如何排查問題!

  • 日誌
  • 堆疊 (區分度)

 

 

 

 

 

 

 

 

 

相關文章