Java併發知識點快速複習手冊(下)

前言

本文快速回顧了常考的的知識點,用做面試複習,事半功倍。html

面試知識點複習手冊

已發佈知識點複習手冊java

參考

本文內容參考自CyC2018的Github倉庫:CS-Notesgit

github.com/CyC2018/CS-…程序員

有刪減,修改,補充額外增長內容github

知識共享署名-非商業性使用 4.0 國際許可協議

本做品採用知識共享署名-非商業性使用 4.0 國際許可協議進行許可。面試

文章目錄

  • 線程不安全示例
  • Java 內存模型
  • ThreadLocal/Volatile/Synchronized/Atomic橫向對比
  • 線程安全
  • 鎖優化
  • 多線程開發良好的實踐
  • 補充經典併發集合和同步集合參考
  • Java線程鎖

線程不安全示例

若是多個線程對同一個共享數據進行訪問而不採起同步操做的話,那麼操做的結果是不一致的。算法

如下代碼演示了 1000 個線程同時對 cnt 執行自增操做,操做結束以後它的值有可能小於 1000。數據庫

public class ThreadUnsafeExample {

    private int cnt = 0;

    public void add() {
        cnt++;
    }

    public int get() {
        return cnt;
    }
}
複製代碼
public static void main(String[] args) throws InterruptedException {
    final int threadSize = 1000;
    ThreadUnsafeExample example = new ThreadUnsafeExample();
    final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 0; i < threadSize; i++) {
        executorService.execute(() -> {
            example.add();
            countDownLatch.countDown();
        });
    }
    countDownLatch.await();
    executorService.shutdown();
    System.out.println(example.get());
}
複製代碼
997
複製代碼

Java 內存模型

Java 內存模型試圖屏蔽各類硬件和操做系統的內存訪問差別,以實現讓 Java 程序在各類平臺下都能達到一致的內存訪問效果。segmentfault

主內存與工做內存

處理器上的寄存器的讀寫的速度比內存快幾個數量級,爲了解決這種速度矛盾,在它們之間加入了高速緩存。設計模式

加入高速緩存帶來了一個新的問題:緩存一致性。若是多個緩存共享同一塊主內存區域,那麼多個緩存的數據可能會不一致,須要一些協議來解決這個問題。

在這裏插入圖片描述

全部的變量都存儲在主內存中,每一個線程還有本身的工做內存,工做內存存儲在高速緩存或者寄存器中,保存了該線程使用的變量的主內存副本拷貝。

線程只能直接操做工做內存中的變量,不一樣線程之間的變量值傳遞須要經過主內存來完成。

在這裏插入圖片描述

內存間交互操做

Java 內存模型定義了 8 個操做來完成主內存和工做內存的交互操做

在這裏插入圖片描述

  • read:把一個變量的值從主內存傳輸到工做內存中
  • load:在 read 以後執行,把 read 獲得的值放入工做內存的變量副本中
  • use:把工做內存中一個變量的值傳遞給執行引擎
  • assign:把一個從執行引擎接收到的值賦給工做內存的變量
  • store:把工做內存的一個變量的值傳送到主內存中
  • write:在 store 以後執行,把 store 獲得的值放入主內存的變量中
  • lock:做用於主內存的變量
  • unlock

內存模型三大特性

1. 原子性

Java 內存模型保證了 read、load、use、assign、store、write、lock 和 unlock 操做具備原子性

Java 內存模型保證了 read、load、use、assign、store、write、lock 和 unlock 操做具備原子性,例如對一個 int 類型的變量執行 assign 賦值操做,這個操做就是原子性的。可是 Java 內存模型容許虛擬機將沒有被 volatile 修飾的 64 位數據(long,double)的讀寫操做劃分爲兩次 32 位的操做來進行,即 load、store、read 和 write 操做能夠不具有原子性。

有一個錯誤認識就是,int 等原子性的類型在多線程環境中不會出現線程安全問題。前面的線程不安全示例代碼中,cnt 屬於 int 類型變量,1000 個線程對它進行自增操做以後,獲得的值爲 997 而不是 1000。

原子類

在這裏插入圖片描述

使用 AtomicInteger 重寫以前線程不安全的代碼以後獲得如下線程安全實現:

public class AtomicExample {
    private AtomicInteger cnt = new AtomicInteger();

    public void add() {
        cnt.incrementAndGet();
    }

    public int get() {
        return cnt.get();
    }
}
複製代碼
public static void main(String[] args) throws InterruptedException {
    final int threadSize = 1000;
    AtomicExample example = new AtomicExample(); // 只修改這條語句
    final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 0; i < threadSize; i++) {
        executorService.execute(() -> {
            example.add();
            countDownLatch.countDown();
        });
    }
    countDownLatch.await();
    executorService.shutdown();
    System.out.println(example.get());
}
複製代碼
1000
複製代碼

synchronized

除了使用原子類以外,也可使用 synchronized 互斥鎖來保證操做的原子性。它對應的內存間交互操做爲:lock 和 unlock,在虛擬機實現上對應的字節碼指令爲 monitorenter 和 monitorexit。

public class AtomicSynchronizedExample {
    private int cnt = 0;

    public synchronized void add() {
        cnt++;
    }

    public synchronized int get() {
        return cnt;
    }
}
複製代碼
public static void main(String[] args) throws InterruptedException {
    final int threadSize = 1000;
    AtomicSynchronizedExample example = new AtomicSynchronizedExample();
    final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 0; i < threadSize; i++) {
        executorService.execute(() -> {
            example.add();
            countDownLatch.countDown();
        });
    }
    countDownLatch.await();
    executorService.shutdown();
    System.out.println(example.get());
}
複製代碼
1000
複製代碼

2. 可見性

可見性指當一個線程修改了共享變量的值,其它線程可以當即得知這個修改

Java 內存模型是經過在變量修改後將新值同步回主內存在變量讀取前從主內存刷新變量值來實現可見性的。

主要有有三種實現可見性的方式:

  • volatile:僅僅用來保證該變量對全部線程的可見性,但不保證原子性。
  • synchronized,對一個變量執行 unlock 操做以前,必須把變量值同步回主內存。
  • final,被 final 關鍵字修飾的字段在構造器中一旦初始化完成,而且沒有發生 this 逃逸(其它線程經過 this 引用訪問到初始化了一半的對象),那麼其它線程就能看見 final 字段的值。

3. 有序性

有序性是指:在本線程內觀察,全部操做都是有序的在一個線程觀察另外一個線程,全部操做都是無序的,無序是由於發生了指令重排序。

在 Java 內存模型中,容許編譯器和處理器對指令進行重排序,重排序過程不會影響到單線程程序的執行,卻會影響到多線程併發執行的正確性。

  • volatile 關鍵字經過添加內存屏障的方式來禁止指令重排,即重排序時不能把後面的指令放到內存屏障以前。

  • 能夠經過 synchronized 來保證有序性,它保證每一個時刻只有一個線程執行同步代碼,至關因而讓線程順序執行同步代碼。

happens-before

blog.csdn.net/qq_30137611…

happens-before是判斷數據是否存在競爭、線程是否安全的重要依據

定義:

若是操做A happens-before 於 操做B,那麼就能夠肯定,操做B執行完以後,j 的值必定爲 1;由於happens-before關係能夠向程序員保證:在操做B執行以前,操做A的執行後的影響[或者說結果](修改 i 的值)操做B是能夠觀察到的[或者說可見的]

這裏列舉幾個常見的Java「自然的」happens-before關係

程序順序規則: 一個線程中的每一個操做,happens-before於該線程中的任意後續操做(也就是說你寫的操做,若是是單線程執行,那麼前面的操做[程序邏輯上的前]就會happens-before於後面的操做) 這裏的影響指修改了 i 變量的值

監視器鎖規則: 對一個鎖的解鎖,happens-before 於隨後對這個鎖的加鎖

volatile變量規則: 對一個 volatile域的寫,happens-before於任意後續對這個volatile域的讀

④ 傳遞性:若是 A happens-before B,且 B happens-before C,那麼A happens-before C

在這裏插入圖片描述

總結

  • 保證原子性的操做:
    • read、load、assign、use、store和write(自身具備原子性)
    • 原子類
    • synchronized鎖
  • 保證可見性:
    • volatile
    • synchronized鎖
    • final
  • 保證有序性(重排序致使無序)的操做:
    • volatile
    • synchronized鎖

先行發生原則

上面提到了能夠用 volatile 和 synchronized 來保證有序性。除此以外,JVM 還規定了先行發生原則,讓一個操做無需控制就能先於另外一個操做完成。

主要有如下這些原則:

1. 單一線程原則

Single Thread rule

在一個線程內,在程序前面的操做先行發生於後面的操做。

2. 管程鎖定規則

Monitor Lock Rule

一個 unlock 操做先行發生於後面對同一個鎖的 lock 操做。

3. volatile 變量規則

Volatile Variable Rule

對一個 volatile 變量的寫操做先行發生於後面對這個變量的讀操做。

4. 線程啓動規則

Thread Start Rule

Thread 對象的 start() 方法調用先行發生於此線程的每個動做。

5. 線程加入規則

Thread Join Rule

Thread 對象的結束先行發生於 join() 方法返回。

6. 線程中斷規則

Thread Interruption Rule

對線程 interrupt() 方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生,能夠經過 interrupted() 方法檢測到是否有中斷髮生。

7. 對象終結規則

Finalizer Rule

一個對象的初始化完成(構造函數執行結束)先行發生於它的 finalize() 方法的開始。

8. 傳遞性

Transitivity

若是操做 A 先行發生於操做 B,操做 B 先行發生於操做 C,那麼操做 A 先行發生於操做 C。

ThreadLocal/Volatile/Synchronized/Atomic橫向對比

blog.csdn.net/u010687392/…

Atomic 原子性

內部實現

採用Lock-Free算法替代鎖,加上原子操做指令實現併發狀況下資源的安全、完整、一致性

而關於Lock-Free算法,則是一種新的策略替代鎖來保證資源在併發時的完整性的,Lock-Free的實現有三步:

一、循環(for(;;)、while) 
二、CAS(CompareAndSet) 
三、回退(returnbreak複製代碼

volatile 可見性 有序性

www.jianshu.com/p/195ae7c77…

經過關鍵字sychronize能夠防止多個線程進入同一段代碼,在某些特定場景中,volatile至關於一個輕量級的sychronize,由於不會引發線程的上下文切換

爲什麼具備可見性

  • 對於普通變量

    • 讀操做會優先讀取工做內存的數據,若是工做內存中不存在,則從主內存中拷貝一份數據到工做內存中
    • 寫操做只會修改工做內存的副本數據,這種狀況下,其它線程就沒法讀取變量的最新值
  • 對於volatile變量

    • 讀操做時JMM會把工做內存中對應的值設爲無效要求線程從主內存中讀取數據
    • 寫操做時JMM會把工做內存中對應的數據刷新到主內存中,這種狀況下,其它線程就能夠讀取變量的最新值。

爲什麼具備有序性(內存屏障)

內存屏障,又稱內存柵欄,是一個CPU指令。在程序運行時,爲了提升執行性能,編譯器和處理器會對指令進行重排序

JMM爲了保證在不一樣的編譯器和CPU上有相同的結果,經過插入特定類型的內存屏障來禁止特定類型的編譯器重排序和處理器重排序,插入一條內存屏障會告訴編譯器和CPU:無論什麼指令都不能和這條Memory Barrier指令重排序。

知足下面的條件才應該使用volatile修飾變量

通常來講,volatile大多用於標誌位上(判斷操做),

  • 修改變量時不依賴變量的當前值(由於volatile是不保證原子性的)
  • 該變量不會歸入到不變性條件中(該變量是可變的)
  • 在訪問變量的時候不須要加鎖(加鎖就不必使用volatile這種輕量級同步機制了)

synchronized 全能

可是因爲操做上的優點,只須要簡單的聲明一下便可,並且被它聲明的代碼塊也是具備操做的原子性。

ThreadLocal

ThreadLocal提供了線程的局部變量,每一個線程均可以經過set()和get()來對這個局部變量進行操做,但不會和其餘線程的局部變量進行衝突,實現了線程的數據隔離。

而ThreadLocal的設計,並非解決資源共享的問題,而是用來提供線程內的局部變量,這樣每一個線程都本身管理本身的局部變量,別的線程操做的數據不會對我產生影響,至關於封裝在Thread內部了,供線程本身管理。

用法

它有三個暴露的方法,set、get、remove。

內部實現

  • 每一個Thread維護着一個ThreadLocalMap的引用
  • ThreadLocalMap是ThreadLocal的內部類,用Entry來進行存儲
  • 調用ThreadLocal的set()方法時,實際上就是往ThreadLocalMap設置值,key是ThreadLocal對象,值是傳遞進來的對象
  • 調用ThreadLocal的get()方法時,實際上就是往ThreadLocalMap獲取值,key是ThreadLocal對象
  • ThreadLocal自己並不存儲值,它只是做爲一個key來讓線程從ThreadLocalMap獲取value

在這裏插入圖片描述

內存泄漏

若是ThreadLocal不設爲static的,因爲Thread的生命週期不可預知,這就致使了當系統gc時將會回收它,而ThreadLocal對象被回收了,此時它對應key一定爲null,這就致使了該key對應得value拿不出來了而value以前被Thread所引用,因此就存在key爲null、value存在強引用致使這個Entry回收不了,從而致使內存泄露。

避免內存泄露的方法:

  • ThreadLocal要設爲static靜態的
  • 必須手動remove掉該ThreadLocal的值,這樣Entry就可以在系統gc的時候正常回收,而關於ThreadLocalMap的回收,會在當前Thread銷燬以後進行回收。

使用場景

  • 管理數據庫的Connection

threadLocal可以實現當前線程的操做都是用同一個Connection,保證了事務!

  • 避免一些參數傳遞

總結

關於Volatile關鍵字具備可見性,但不具備操做的原子性,而synchronized比volatile對資源的消耗稍微大點,但能夠保證變量操做的原子性,保證變量的一致性,最佳實踐則是兩者結合一塊兒使用。

一、synchronized:解決多線程資源共享的問題,同步機制採用了「以時間換空間」的方式:訪問串行化,對象共享化。同步機制是提供一份變量,讓全部線程均可以訪問。

二、對於Atomic的出現,是經過原子操做指令+Lock-Free完成,從而實現非阻塞式的併發問題

三、對於Volatile,爲多線程資源共享問題解決了部分需求,在非依賴自身的操做的狀況下,對變量的改變將對任何線程可見。

四、對於ThreadLocal的出現,並非解決多線程資源共享的問題,而是用來提供線程內的局部變量,省去參數傳遞這個沒必要要的麻煩,ThreadLocal採用了「以空間換時間」的方式:訪問並行化,對象獨享化。ThreadLocal是爲每個線程都提供了一份獨有的變量,各個線程互不影響。

線程安全類

等待IO的方式:阻塞,非阻塞

得到通知的方式:異步,非異步

多個線程無論以何種方式訪問某個類,而且在主調代碼中不須要進行同步,都能表現正確的行爲。

線程安全有如下幾種實現方式:

不可變

不可變(Immutable)的對象必定是線程安全的,不須要再採起任何的線程安全保障措施。只要一個不可變的對象被正確地構建出來,永遠也不會看到它在多個線程之中處於不一致的狀態。多線程環境下,應當儘可能使對象成爲不可變,來知足線程安全。

不可變的類型:

  • final 關鍵字修飾的基本數據類型
  • String
  • 枚舉類型
  • Number 部分子類,如 Long 和 Double 等數值包裝類型,BigInteger 和 BigDecimal 等大數據類型。但同爲 Number 的原子類 AtomicInteger 和 AtomicLong 則是可變的。

對於集合類型,可使用 Collections.unmodifiableXXX() 方法來獲取一個不可變的集合。

public class ImmutableExample {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();
        Map<String, Integer> unmodifiableMap = Collections.unmodifiableMap(map);
        unmodifiableMap.put("a", 1);
    }
}
複製代碼
Exception in thread "main" java.lang.UnsupportedOperationException
    at java.util.Collections$UnmodifiableMap.put(Collections.java:1457)
    at ImmutableExample.main(ImmutableExample.java:9)
複製代碼

Collections.unmodifiableXXX() 先對原始的集合進行拷貝,須要對集合進行修改的方法都直接拋出異常。

public V put(K key, V value) {
    throw new UnsupportedOperationException();
}
複製代碼

互斥同步

synchronized 和 ReentrantLock。

非阻塞同步

互斥同步最主要的問題就是進行線程阻塞和喚醒所帶來的性能問題,所以這種同步也稱爲阻塞同步(Blocking Synchronization)。

從處理問題的方式上說,互斥同步屬於一種悲觀的併發策略,老是認爲只要不去作正確的同步措施(例如加鎖),那就確定會出現問題。

隨着硬件指令集的發展,咱們有了另一個選擇:基於衝突檢測的樂觀併發策略,通俗地說,就是先進行操做,若是沒有其餘線程爭用共享數據,那操做就成功了;若是共享數據有爭用,產生了衝突,那就再採起其餘的補償措施(最多見的補償措施就是不斷地重試,直到成功爲止),這種樂觀的併發策略的許多實現都不須要把線程掛起,所以這種同步操做稱爲非阻塞同步(Non-Blocking Synchronization)。

樂觀鎖須要操做衝突檢測這兩個步驟具有原子性,這裏就不能再使用互斥同步來保證了,只能靠硬件來完成。

1. CAS

硬件支持的原子性操做最典型的是:比較並交換(Compare-and-Swap,CAS)。

CAS 指令須要有 3 個操做數,分別是:

  • 內存位置(在 Java 中能夠簡單理解爲變量的內存地址,用 V 表示)
  • 舊的預期值(用 A 表示)
  • 新值(用 B 表示)。

當且僅當預期值A和內存值V相同時,將內存值V修改成B,不然什麼都不作。

可是不管是否更新了 V 的值,都會返回 V 的舊值,上述的處理過程是一個原子操做。

當多個線程嘗試使用CAS同時更新同一個變量時,只有其中一個線程能更新變量的值(A和內存值V相同時,將內存值V修改成B),而其它線程都失敗,失敗的線程並不會被掛起,而是被告知此次競爭中失敗,並能夠再次嘗試(不然什麼都不作)

J.U.C 包裏面的整數原子類 AtomicInteger,其中的 compareAndSet() 和 getAndIncrement() 等方法都使用了 Unsafe 類的 CAS 操做。

2. AtomicInteger

J.U.C 包裏面的整數原子類 AtomicInteger 的方法調用了 Unsafe 類的 CAS 操做。

如下代碼使用了 AtomicInteger 執行了自增的操做。

private AtomicInteger cnt = new AtomicInteger();

public void add() {
    cnt.incrementAndGet();
}
複製代碼

如下代碼是 incrementAndGet() 的源碼,它調用了 Unsafe 的 getAndAddInt() 。

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

如下代碼是 getAndAddInt() 源碼,var1 指示對象內存地址,var2 指示該字段相對對象內存地址的偏移,var4 指示操做須要加的數值,這裏爲 1。經過 getIntVolatile(var1, var2) 獲得舊的預期值,經過調用 compareAndSwapInt() 來進行 CAS 比較,若是該字段內存地址中的值等於 var5,那麼就更新內存地址爲 var1+var2 的變量爲 var5+var4。

能夠看到 getAndAddInt() 在一個循環中進行,發生衝突的作法是不斷的進行重試。

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}
複製代碼

3. ABA

若是一個變量初次讀取的時候是 A 值,它的值被改爲了 B,後來又被改回爲 A,那 CAS 操做就會誤認爲它歷來沒有被改變過。

J.U.C 包提供了一個帶有標記的原子引用類「AtomicStampedReference」來解決這個問題,它能夠經過控制變量值的版原本保證 CAS 的正確性。大部分狀況下 ABA 問題不會影響程序併發的正確性,若是須要解決 ABA 問題,改用傳統的互斥同步可能會比原子類更高效。

無同步方案

要保證線程安全,並非必定就要進行同步。若是一個方法原本就不涉及共享數據,那它天然就無須任何同步措施去保證正確性。

1. 棧封閉

多個線程訪問同一個方法的局部變量時,不會出現線程安全問題,由於局部變量存儲在虛擬機棧中,屬於線程私有的。

public class StackClosedExample {
    public void add100() {
        int cnt = 0;
        for (int i = 0; i < 100; i++) {
            cnt++;
        }
        System.out.println(cnt);
    }
}
複製代碼
public static void main(String[] args) {
    StackClosedExample example = new StackClosedExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> example.add100());
    executorService.execute(() -> example.add100());
    executorService.shutdown();
}
複製代碼
100
100
複製代碼

2. 線程本地存儲(Thread Local Storage)

若是一段代碼中所須要的數據必須與其餘代碼共享,那就看看這些共享數據的代碼是否能保證在同一個線程中執行。若是能保證,咱們就能夠把共享數據的可見範圍限制在同一個線程以內,這樣,無須同步也能保證線程之間不出現數據爭用的問題。

符合這種特色的應用並很多見,大部分使用消費隊列的架構模式(如「生產者-消費者」模式)都會將產品的消費過程儘可能在一個線程中消費完。其中最重要的一個應用實例就是經典 Web 交互模型中的「一個請求對應一個服務器線程」(Thread-per-Request)的處理方式,這種處理方式的普遍應用使得不少 Web 服務端應用均可以使用線程本地存儲來解決線程安全問題。

可使用 java.lang.ThreadLocal 類來實現線程本地存儲功能。

對於如下代碼,thread1 中設置 threadLocal 爲 1,而 thread2 設置 threadLocal 爲 2。過了一段時間以後,thread1 讀取 threadLocal 依然是 1,不受 thread2 的影響。

public class ThreadLocalExample {
    public static void main(String[] args) {
        ThreadLocal threadLocal = new ThreadLocal();
        Thread thread1 = new Thread(() -> {
            threadLocal.set(1);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(threadLocal.get());
            threadLocal.remove();
        });
        Thread thread2 = new Thread(() -> {
            threadLocal.set(2);
            threadLocal.remove();
        });
        thread1.start();
        thread2.start();
    }
}
複製代碼
1
複製代碼

爲了理解 ThreadLocal,先看如下代碼:

public class ThreadLocalExample1 {
    public static void main(String[] args) {
        ThreadLocal threadLocal1 = new ThreadLocal();
        ThreadLocal threadLocal2 = new ThreadLocal();
        Thread thread1 = new Thread(() -> {
            threadLocal1.set(1);
            threadLocal2.set(1);
        });
        Thread thread2 = new Thread(() -> {
            threadLocal1.set(2);
            threadLocal2.set(2);
        });
        thread1.start();
        thread2.start();
    }
}
複製代碼

每一個 Thread 都有一個 ThreadLocal.ThreadLocalMap 對象。

/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
複製代碼

當調用一個 ThreadLocal 的 set(T value) 方法時,先獲得當前線程的 ThreadLocalMap 對象,而後將 ThreadLocal->value 鍵值對插入到該 Map 中。

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}
複製代碼

get() 方法相似。

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}
複製代碼

ThreadLocal 從理論上講並非用來解決多線程併發問題的,由於根本不存在多線程競爭。

在一些場景 (尤爲是使用線程池) 下,因爲 ThreadLocal.ThreadLocalMap 的底層數據結構致使 ThreadLocal 有內存泄漏的狀況,應該儘量在每次使用 ThreadLocal 後手動調用 remove(),以免出現 ThreadLocal 經典的內存泄漏甚至是形成自身業務混亂的風險。

3. 可重入代碼(Reentrant Code)

這種代碼也叫作純代碼(Pure Code),能夠在代碼執行的任什麼時候刻中斷它,轉而去執行另一段代碼(包括遞歸調用它自己),而在控制權返回後,原來的程序不會出現任何錯誤。

可重入代碼有一些共同的特徵,例如不依賴存儲在堆上的數據和公用的系統資源、用到的狀態量都由參數中傳入、不調用非可重入的方法等。

鎖優化

這裏的鎖優化主要是指虛擬機對synchronized的優化。

鎖競爭是kernal mode下的,會通過user mode(用戶態)到kernal mode(內核態) 的切換,是比較花時間的。

自旋鎖

自旋鎖的思想是讓一個線程在請求一個共享數據的鎖時執行忙循環(自旋)一段時間,若是在這段時間內能得到鎖,就能夠避免進入阻塞狀態。

它只適用於共享數據的鎖定狀態很短的場景

自旋次數的默認值是 10 次,用戶可使用虛擬機參數 -XX:PreBlockSpin 來更改。

在 JDK 1.6 中引入了自適應的自旋鎖。自適應意味着自旋的次數再也不固定了,而是由前一次在同一個鎖上的自旋次數鎖的擁有者的狀態來決定。

鎖消除

鎖消除是指對於被檢測出不可能存在競爭的共享數據的鎖進行消除。檢測到某段代碼是線程安全的(言外之意:無鎖也是安全的),JVM會安全地原有的鎖消除掉!

逃逸分析:若是堆上的共享數據不可能逃逸出去被其它線程訪問到,那麼就能夠把它們當成私有數據對待,也就能夠將它們上的鎖進行消除。

對於一些看起來沒有加鎖的代碼,其實隱式的加了不少鎖。例以下面的字符串拼接代碼就隱式加了鎖:

public static String concatString(String s1, String s2, String s3) {
    return s1 + s2 + s3;
}
複製代碼

String 是一個不可變的類,Javac 編譯器會對 String 的拼接自動優化。在 JDK 1.5 以前,會轉化爲 StringBuffer 對象的連續 append() 操做,在 JDK 1.5 及之後的版本中,會轉化爲 StringBuilder 對象的連續 append() 操做,即上面的代碼可能會變成下面的樣子:

public static String concatString(String s1, String s2, String s3) {
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    sb.append(s3);
    return sb.toString();
}
複製代碼

虛擬機觀察變量 sb,很快就會發現它的動態做用域被限制在 concatString() 方法內部。也就是說,sb 的全部引用永遠不會「逃逸」到 concatString() 方法以外,其餘線程沒法訪問到它。所以,雖然這裏有鎖,可是能夠被安全地消除掉。

鎖粗化

若是一系列的連續操做都對同一個對象反覆加鎖和解鎖,頻繁的加鎖操做就會致使性能損耗。

上一節的示例代碼中連續的 append() 方法就屬於這類狀況。若是虛擬機探測到由這樣的一串零碎的操做都對同一個對象加鎖,將會把加鎖的範圍擴展(粗化)到整個操做序列的外部。對於上一節的示例代碼就是擴展到第一個 append() 操做以前直至最後一個 append() 操做以後,這樣只須要加鎖一次就能夠了。

可是若是一系列的連續操做都對同一個對象反覆加鎖和解鎖,甚至加鎖操做是出如今循環體中的,頻繁地進行互斥同步操做也會致使沒必要要的性能損耗。

偏向鎖

總結:在無競爭環境下,把整個同步都消除,CAS也不作。

偏向鎖的思想是偏向於讓第一個獲取鎖對象的線程,這個線程在以後獲取該鎖就再也不須要進行同步操做,甚至連 CAS 操做也再也不須要。

可使用 -XX:+UseBiasedLocking=true 開啓偏向鎖,不過在 JDK 1.6 中它是默認開啓的。

當鎖對象第一次被線程得到的時候,進入偏向狀態,標記爲 1 01。同時使用 CAS 操做將線程 ID 記錄到 Mark Word 中,若是 CAS 操做成功,這個線程之後每次進入這個鎖相關的同步塊就不須要再進行任何同步操做。

當有另一個線程去嘗試獲取這個鎖對象時,偏向狀態就宣告結束,此時撤銷偏向(Revoke Bias)後恢復到未鎖定狀態或者輕量級鎖狀態。

輕量級鎖

輕量級鎖是相對於傳統的重量級鎖而言,它使用 CAS 操做來避免重量級鎖使用互斥量的開銷。對於絕大部分的鎖,在整個同步週期內都是不存在競爭的,所以也就不須要都使用互斥量進行同步,能夠先採用 CAS 操做進行同步,若是 CAS 失敗了再改用互斥量進行同步。(樂觀鎖)

JDK 1.6 引入了偏向鎖輕量級鎖,從而讓鎖擁有了四個狀態:無鎖狀態(unlocked)、偏向鎖狀態(biasble)、輕量級鎖狀態(lightweight locked)和重量級鎖狀態(inflated)。

若是 CAS 操做失敗了,虛擬機首先會檢查對象的 Mark Word 是否指向當前線程的虛擬機棧,若是是的話說明當前線程已經擁有了這個鎖對象,那就能夠直接進入同步塊繼續執行,不然說明這個鎖對象已經被其餘線程線程搶佔了。若是有兩條以上的線程爭用同一個鎖,那輕量級鎖就再也不有效,要膨脹爲重量級鎖。

但若是存在鎖競爭,除了互斥量的開銷外,還額外發生了CAS操做,所以在有競爭的狀況下,輕量級鎖會比傳統的重量級鎖更慢。

簡單來講:若是發現同步週期內都是不存在競爭,JVM會使用CAS操做來替代操做系統互斥量。這個優化就被叫作輕量級鎖。

多線程開發良好的實踐

  • 縮小同步範圍,例如對於 synchronized,應該儘可能使用同步塊而不是同步方法。

  • 多用同步類少用 wait() 和 notify(),多用CountDownLatch, Semaphore, CyclicBarrier 和 Exchanger 這些同步類。他們簡化了編碼操做,而用 wait() 和 notify() 很難實現對複雜控制流的控制。其次,這些類是由最好的企業編寫和維護,在後續的 JDK 中它們還會不斷優化和完善,使用這些更高等級的同步工具你的程序能夠不費吹灰之力得到優化。

  • 多用併發集合少用同步集合。

  • 使用本地變量ThreadLocal和不可變類來保證線程安全。

  • 使用線程池而不是直接建立 Thread 對象,這是由於建立線程代價很高,線程池能夠有效地利用有限的線程來啓動任務。

  • 使用 BlockingQueue 實現生產者消費者問題。

補充經典併發集合和同步集合參考

www.cnblogs.com/suneryong/p…

無論是同步集合仍是併發集合他們都支持線程安全,他們之間主要的區別體如今性能和可擴展性,還有他們如何實現的線程安全。同步HashMap, Hashtable, HashSet, Vector, ArrayList 相比他們併發的實現(好比:ConcurrentHashMap, CopyOnWriteArrayList, CopyOnWriteHashSet)會慢得多。形成如此慢的主要緣由是鎖, 同步集合會把整個Map或List鎖起來,而併發集合不會。併發集合實現線程安全是經過使用先進的和成熟的技術像鎖剝離。好比ConcurrentHashMap 會把整個Map 劃分紅幾個片斷,只對相關的幾個片斷上鎖,同時容許多線程訪問其餘未上鎖的片斷。

java.util.concurrent包中包含的併發集合類以下:

ConcurrentHashMap

CopyOnWriteArrayList

CopyOnWriteArraySet
複製代碼

對象的發佈與逸出

  • 發佈(publish) 使對象可以在當前做用域以外的代碼中使用
  • 逸出(escape) 當某個不該該發佈的對象被髮布了

常見逸出的有下面幾種方式:

  • 靜態域逸出
  • public修飾的get方法
  • 方法參數傳遞
  • 隱式的this

具體解釋見:segmentfault.com/a/119000001…

安全發佈對象有幾種常見的方式:

  • 在靜態域中直接初始化 : public static Person = new Person();
    • 靜態初始化由JVM在類的初始化階段就執行了,JVM內部存在着同步機制,導致這種方式咱們能夠安全發佈對象
  • 對應的引用保存到volatile或者AtomicReferance引用中
    • 保證了該對象的引用的可見性和原子性
  • 由final修飾
    • 該對象是不可變的
  • 由鎖來保護
    • 發佈和使用的時候都須要加鎖

Java線程鎖

segmentfault.com/a/119000001…

避免死鎖的方法

固定鎖順序避免死鎖

上面transferMoney()發生死鎖的緣由是由於加鎖順序不一致而出現的~

  • 若是全部線程以固定的順序來得到鎖,那麼程序中就不會出現鎖順序死鎖問題!

例子中,改造爲獲得對應的hash值來固定加鎖的順序,這樣咱們就不會發生死鎖的問題了!

開放調用避免死鎖

若是在調用某個方法時不須要持有鎖,那麼這種調用被稱爲開放調用!

使用定時鎖

使用顯式Lock鎖,在獲取鎖時使用tryLock()方法。當等待超過期限的時候,tryLock()不會一直等待,而是返回錯誤信息。

關注我

我是蠻三刀把刀,目前爲後臺開發工程師。主要關注後臺開發,網絡安全,Python爬蟲等技術。

來微信和我聊聊:yangzd1102

Github:github.com/qqxx6661

原創博客主要內容

  • 筆試面試複習知識點手冊
  • Leetcode算法題解析(前150題)
  • 劍指offer算法題解析
  • Python爬蟲相關技術分析和實戰
  • 後臺開發相關技術分析和實戰

同步更新如下博客

1. Csdn

blog.csdn.net/qqxx6661

擁有專欄:Leetcode題解(Java/Python)、Python爬蟲開發

2. 知乎

www.zhihu.com/people/yang…

擁有專欄:碼農面試助攻手冊

3. 掘金

juejin.im/user/5b4801…

4. 簡書

www.jianshu.com/u/b5f225ca2…

我的項目:電商價格監控網站

本人長期維護的我的項目,徹底免費,請你們多多支持。

實現功能

  • 京東商品監控:設置商品ID和預期價格,當商品價格【低於】設定的預期價格後自動發送郵件提醒用戶。(一小時之內)
  • 京東品類商品監控:用戶訂閱特定品類後,該類降價幅度大於7折的【自營商品】會被選出併發送郵件提醒用戶。
  • 品類商品瀏覽,商品歷史價格曲線,商品歷史最高最低價
  • 持續更新中...

網站地址

pricemonitor.online/

我的公衆號:Rude3Knife

我的公衆號:Rude3Knife

若是文章對你有幫助,不妨收藏起來並轉發給您的朋友們~

相關文章
相關標籤/搜索