高併發 - 基礎

同步/異步、阻塞/非阻塞

同步/異步是 API 被調用者的通知方式。阻塞/非阻塞則是 API 調用者的等待方式(線程掛機/不掛起)。java

  • 同步非阻塞

Future方式,任務的完成要主線程本身判斷。
如NIO,後臺有多個任務在執行(非阻塞),主動循環查詢(同步)多個任務的完成狀態,只要有任何一個任務完成,就去處理它。這就是所謂的 「I/O 多路複用」。算法

同步非阻塞相比同步阻塞:
優勢:可以在等待任務完成的時間裏幹其餘活了(就是 「後臺」 能夠有多個任務在同時執行)。
缺點:任務完成的響應延遲增大了,由於每過一段時間纔去輪詢一次,而任務可能在兩次輪詢之間的任意時間完成。安全

  • 異步非阻塞

CompletableFuture方式,任務的完成的通知由其餘線程發出。
如AIO,應用程序發起調用,而不須要進行輪詢,進而處理下一個任務,只需在I/O完成後經過信號或是回調將數據傳遞給應用程序便可。多線程

異步非阻塞相比同步非阻塞:
不須要主動輪詢,減小CPU操做。併發


併發、並行

圖片描述

死鎖、飢餓、活鎖

  • 死鎖

線程A持有lock1,線程B持有lock2。當A試圖獲取lock2時,此時線程B也在試圖獲取lock1。此時兩者都在等待對方所持有鎖的釋放,而兩者卻又都沒釋放本身所持有的鎖,這時兩者便會一直阻塞下去。app

  • 飢餓

對於非公平隊列來講,線程有可能一直獲取不到對鎖的佔用。異步

  • 活鎖

因爲某些條件沒有知足,致使兩個線程一直互相「謙讓」對鎖的佔用,從而一直等下去。活鎖有可能自行解開,死鎖則不能。函數


什麼是線程安全

若是多個線程同時運行你的代碼,每個線程每次運行結果和單線程運行的結果是同樣的,就是線程安全的。oop

原子性、可見性、有序性

  • 原子性

即一個操做或者多個操做 要麼所有執行而且執行的過程不會被任何因素打斷,要麼就都不執行。性能

  • 可見性

可見性是指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其餘線程可以當即看獲得修改的值。

  • 有序性

即程序執行的順序按照代碼的前後順序執行。(在單線程中,編譯器對代碼的重排序沒有問題,但在多線程程序運行就可能有問題)

x = 10;         //語句1
y = x;         //語句2
x++;           //語句3
x = x + 1;     //語句4
上面4個語句只有語句1的操做具有原子性。也就是說,只有簡單的讀取、賦值(並且必須是將數字賦值給某個變量)纔是原子操做。
Java內存模型只保證了基本讀取和賦值是原子性操做,若是要實現更大範圍操做的原子性,能夠經過synchronized和Lock來實現。

重排序

編譯器可能會對程序操做作重排序(爲了讓CPU指令處理的流水線更加高效,減小空閒時間)。編譯器在重排序時,會遵照數據依賴性,不會改變存在數據依賴關係的兩個操做的執行順序。
注意,這裏所說的數據依賴性僅針對單個處理器中執行的指令序列和單個線程中執行的操做,不一樣處理器之間和不一樣線程之間的數據依賴性不被編譯器和處理器考慮。
因此重排序會使得多線程不安全。

關鍵字volatile

volatile修飾的變量不保留拷貝,直接訪問主內存中的變量,即保證可見性。
volatile前面的代碼確定在volatile以前,volatile後面的代碼確定在volatile以後,即保證有序性。
volatile修飾的變量缺乏原子性的保證。如volatile n=n+一、n++、n = m + 1 等,在多線程狀況下,該操做不是原子級別的;而n=false是原子的,因此volatile通常用於狀態標記。若是本身沒有把握,可使用synchronized、Lock、AtomicInteger來代替volatile。

關鍵字synchronized

synchronized與static synchronized 的區別:

  • synchronized是對類的當前方法的實例進行加鎖,類的兩個不一樣實例的synchronized方法能夠被兩個線程分別訪問。
  • static synchronized是類java.lang.Class對象鎖。由於當虛擬機加載一個類的時候,會會爲這個類實例化一個 java.lang.Class 對象。類的不一樣實例在執行該方法時共用一個鎖。

synchronized方法只能由synchronized的方法覆蓋:
繼承時子類的覆蓋方法必須定義成synchronized。

兩個線程不能同時訪問同一對象的不一樣synchronized方法:
由於synchronized鎖是基於對象的。但同一對象的普通方法和synchronized方法能同時被兩個線程分別訪問。

Happen-Before

程序順序原則:一個線程內保證語義的串行性
volatile規則:volatile變量的寫,先發生於讀,這保證了volatile變量的可見性
鎖規則:解鎖(unlock)必然發生在隨後的加鎖(lock)前
傳遞性:A先於B,B先於C,那麼A必然先於C
線程的start()方法先於它的每個動做
線程的全部操做先於線程的終結(Thread.join())
線程的中斷(interrupt())先於被中斷線程的代碼
對象的構造函數執行結束先於finalize()方法
這些原則保證了重排的語義是一致的。

CAS(Compare and swap)

CAS算法:CAS(V, E, N)。V表示要更新的變量,E表示預期值,N表示新值。僅當V值等於E值時,纔會將V的值設爲N,若是V值和E值不一樣,則說明已經有其餘線程作了更新,則當前線程什麼都不作。當多個線程同時使用CAS操做一個變量時,只有一個會勝出,併成功更新,其他均會失敗。失敗的線程不會被掛起,僅是被告知失敗,而且容許再次嘗試,固然也容許失敗的線程放棄操做。

以AtomicInteger爲例:

private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
private volatile int value;    //保證線程間的數據是可見的

static {
    try {    //valueOffset就是value
        valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
}

public final int getAndSet(int newValue) {
    return unsafe.getAndSetInt(this, valueOffset, newValue);
}

public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

public final boolean compareAndSet(int expect, int update) {
    //對於this這個類上的偏移量爲valueOffset的變量值若是與指望值expect相同,那麼把這個變量的值設爲update。
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

以上compareAndSet方法相似如下:

if (value == expect) {
    Value = update;
    return true;
} else {
    return false;
}

那麼問題來了,若是value== expect以後,正要執行value= update時,切換了線程更改了值,則會形成了數據不一致。但這個擔憂是多餘的,由於CAS操做是原子的,中間不會有線程切換。
如何保證原子性,即一個步驟?
實際上compareAndSet()利用JNI(Java Native Interface)來執行CPU的CMPXCHG指令,從而保證比較、交換是一步操做,即原子性操做。


CAS缺點

  • ABA問題
static final AtomicReference<Integer> ref = new AtomicReference<Integer>(1);

    public final int incrementAndGet() {
        while (true) {
            int current = ref.get();
            int next = current + 1;    // 1        
        if (ref.compareAndSet(current, next)) {    // 2
                return next;
            }
        }
    }

在代碼1和代碼2之間,若其餘線程將value設置爲3,另外一個線程又將value設置1,則CAS進行檢查時會錯誤的認爲值沒有發生變化,可是實際上卻變化了。這就是A變成B又變成A,即ABA問題。
解決思路就是添加版本號。在變量和版本號綁定,每次變量更新的時候把版本號加一,那麼A-B-A 就會變成1A-2B-3A,當版本號相同時才作更新值的操做。

java.util.concurrent.atomic.AtomicStampedReference<V>能夠解決ABA問題,其內部類:

private static class Pair<T> {
    final T reference;
    final int stamp;
    ......
 }

AtomicStampedReference的compareAndSet方法會首先檢查當前reference是否==預期reference(內存地址比較),而且當前stamp是否等於預期stamp,若是都相等,則執行Unsafe.compareAndSwapObject方法。

public boolean compareAndSet(V   expectedReference,
                             V   newReference,
                             int expectedStamp,
                             int newStamp) {
    Pair<V> current = pair;
    return
        expectedReference == current.reference &&
        expectedStamp == current.stamp &&
        ((newReference == current.reference &&
          newStamp == current.stamp) ||
         casPair(current, Pair.of(newReference, newStamp)));    //Unsafe.compareAndSwapObject
}
  • 循環時間長開銷大

自旋CAS若是長時間不成功,會給CPU帶來很是大的執行開銷。若是JVM能支持處理器提供的PAUSE指令那麼效率會有必定的提高,PAUSE指令提高了自旋等待循環(spin-wait loop)的性能。

  • 只能保證一個共享變量的原子操做

對於多個共享變量操做,循環CAS就沒法保證操做的原子性,這個時候就能夠用鎖,或者把多個共享變量合併成一個共享變量來操做。JDK提供了AtomicReference類來保證引用對象之間的原子性,你能夠把多個變量放在一個對象裏來進行CAS操做。

普通變量的原子操做
java.util.concurrent.atomic.AtomicIntegerFieldUpdater<T>類的主要做用是讓普通變量也享受原子操做。
就好比本來有一個變量是int型,而且不少地方都應用了這個變量,可是在某個場景下,想讓int型變成AtomicInteger,可是若是直接改類型,就要改其餘地方的應用。AtomicIntegerFieldUpdater就是爲了解決這樣的問題產生的。

public static class V {
    volatile int score;

    public int getScore() {
        return score;
    }

    public void setScore(int score) {
        this.score = score;
    }
}

public final static AtomicIntegerFieldUpdater<V> vv = AtomicIntegerFieldUpdater.newUpdater(V.class, "score");

public static void main(String[] args) {
    final V stu = new V();
    vv.incrementAndGet(stu);
}

注:Updater只能修改它可見範圍內的變量。由於Updater使用反射獲得這個變量。變量必須是volatile類型的。因爲CAS操做會經過對象實例中的偏移量(堆內存的偏移量)直接進行賦值,所以,它不支持static字段(Unsafe.objectFieldOffset()不支持靜態變量)。

相關文章
相關標籤/搜索