併發編程之ThreadLocal、Volatile、synchronized、Atomic關鍵字

前言

對於ThreadLocal、Volatile、synchronized、Atomic這四個關鍵字,我想一說起到你們確定都想到的是解決在多線程併發環境下資源的共享問題,可是要細說每個的特色、區別、應用場景、內部實現等,卻可能模糊不清,說不出個因此然來,因此,本文就對這幾個關鍵字作一些做用、特色、實現上的講解。算法

一、Atomic

做用:安全

對於原子操做類,Java的concurrent併發包中主要爲咱們提供了這麼幾個經常使用的: AtomicInteger、AtomicLong、AtomicBoolean、AtomicReference。
對於原子操做類,最大的特色是在多線程併發操做同一個資源的狀況下,使用 Lock-Free算法來替代鎖,這樣開銷小、速度快,對於原子操做類是採用原子操做指令實現的,從而能夠保證操做的原子性。

什麼是原子性?多線程

好比一個操做i++;實際上這是三個原子操做,先把i的值讀取、而後修改(+1)、最後寫入給i。因此使用Atomic原子類操做數,好比:i++;那麼它會在這步操做都完成狀況下才容許其它線程再對它進行操做,而這個實現則是經過Lock-Free+原子操做指令來肯定的.

如:AtomicInteger類中:併發

public final int incrementAndGet() {  
     for (;;) {  
         int current = get();  
         int next = current + 1;  
         if (compareAndSet(current, next))  
         return next;  
     }  
 }

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

  • 一、循環(for(;;)、while)
  • 二、CAS(CompareAndSet)
  • 三、回退(return、break)

用法
好比在多個線程操做一個count變量的狀況下,則能夠把count定義爲AtomicInteger,以下:性能

public class Counter {  

    private AtomicInteger count = new AtomicInteger(); 
    
    public int getCount() {   
        return count.get();    
    }    
    
    public void increment() {  
        count.incrementAndGet();   
    }
  }

在每一個線程中經過increment()來對count進行計數增長的操做,或者其它一些操做。這樣每一個線程訪問到的將是安全、完整的count。this

內部實現spa

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

二、Volatile

做用線程

Volatile能夠看作是一個輕量級的synchronized,它能夠在多線程併發的狀況下保證變量的「可見性」;

什麼是可見性?設計

就是在一個線程的工做內存中修改了該變量的值,該變量的值當即能回顯到主內存中,從而保證全部的線程看到這個變量的值是一致的。因此在處理同步問題上它大顯做用,並且它的開銷比synchronized小、使用成本更低。

舉個栗子:在寫單例模式中,除了用靜態內部類外,還有一種寫法也很是受歡迎,就是Volatile+DCL:

public class Singleton { 

 private static volatile Singleton instance;  
  
 private Singleton() {  
 }  
  
 public static Singleton getInstance() {  
     if (instance == null) {  
        synchronized (Singleton.class) {  
         if (instance == null) {  
            instance = new Singleton();  
         }  
       }  
     }  
    return instance;  
    }  
}

這樣單例無論在哪一個線程中建立的,全部線程都是共享這個單例的。

雖然說這個Volatile關鍵字能夠解決多線程環境下的同步問題,不過這也是相對的,由於它不具備操做的原子性,也就是它不適合在對該變量的寫操做依賴於變量自己本身。舉個最簡單的栗子:在進行計數操做時count++,實際是count=count+1;,count最終的值依賴於它自己的值。因此使用volatile修飾的變量在進行這麼一系列的操做的時候,就有併發的問題;

舉個栗子:

  • 由於它不具備操做的原子性,有可能1號線程在即將進行寫操做時count值爲4;而2號線程就剛好獲取了寫操做以前的值4,因此1號線程在完成它的寫操做後count值就爲5了,而在2號線程中count的值還爲4,即便2號線程已經完成了寫操做count仍是爲5,而咱們指望的是count最終爲6,因此這樣就有併發的問題。
  • 而若是count換成這樣:count=num+1;假設num是同步的,那麼這樣count就沒有併發的問題的,只要最終的值不依賴本身自己。

用法

由於volatile不具備操做的原子性,因此若是用volatile修飾的變量在進行依賴於它自身的操做時,就有併發問題,如:count,像下面這樣寫在併發環境中是達不到任何效果的:
public class Counter {  
 private volatile int count;  

 public int getCount(){  
 return count;  
 }  
 public void increment(){  
 count++;  
 }  
}

而要想count能在併發環境中保持數據的一致性,則能夠在increment()中加synchronized同步鎖修飾,改進後的爲:

public class Counter {  
 private volatile int count;  
  
 public int getCount(){  
 return count;  
 }  
 public synchronized void increment(){  
 count++;  
 }  
}

三、synchronized

做用

synchronized叫作同步鎖,是Lock的一個簡化版本,因爲是簡化版本,那麼性能確定是不如Lock的,不過它操做起來方便,只須要在一個方法或把須要同步的代碼塊包裝在它內部,那麼這段代碼就是同步的了,全部線程對這塊區域的代碼訪問必須先持有鎖才能進入,不然則攔截在外面等待正在持有鎖的線程處理完畢再獲取鎖進入,正由於它基於這種阻塞的策略,因此它的性能不太好,可是因爲操做上的優點,只須要簡單的聲明一下便可,並且被它聲明的代碼塊也是具備操做的原子性。

用法

public synchronized void increment(){  
    count++;  
 }  
  
 public void increment(){  
     //同步代碼塊
     synchronized (Counte.class){  
         count++;  
     }  
 }

內部實現
重入鎖ReentrantLock+一個Condition,因此說是Lock的簡化版本,由於一個Lock每每能夠對應多個Condition;

四、ThreadLocal

做用

  • 關於ThreadLocal,這個類的出現並非用來解決在多線程併發環境下資源的共享問題的,它和其它三個關鍵字不同,其它三個關鍵字都是從線程外來保證變量的一致性,這樣使得多個線程訪問的變量具備一致性,能夠更好的體現出資源的共享。
  • 而ThreadLocal的設計,並非解決資源共享的問題,而是用來提供線程內的局部變量,這樣每一個線程都本身管理本身的局部變量,別的線程操做的數據不會對我產生影響,互不影響,因此不存在解決資源共享這麼一說,若是是解決資源共享,那麼其它線程操做的結果必然我須要獲取到,而ThreadLocal則是本身管理本身的,至關於封裝在Thread內部了,供線程本身管理。

用法

通常使用ThreadLocal,官方建議咱們定義爲 private static ,至於爲何要定義成靜態的,這和內存泄露有關,後面再講。
它有三個暴露的方法,set、get、remove。
public class ThreadLocalDemo {  
 
 private static ThreadLocal<String> threadLocal = new ThreadLocal<String>(){  
     @Override  
     protected String initialValue() {  
         return "hello";  
     }  
 };  
 
 static class MyRunnable implements Runnable{  
     private int num;  
     
     public MyRunnable(int num){  
         this.num = num;  
     }  
     @Override  
     public void run() {  
         threadLocal.set(String.valueOf(num));  
         System.out.println("threadLocalValue:"+threadLocal.get()); 
         //手動移除
         threadLocal.remove();
     }  
 }  
  
 public static void main(String[] args){  
     new Thread(new MyRunnable(1)).start();  
     new Thread(new MyRunnable(2)).start();  
     new Thread(new MyRunnable(3)).start();  
 }  
 
}

運行結果以下,這些ThreadLocal變量屬於線程內部管理的,互不影響:

threadLocalValue:1  
threadLocalValue:2  
threadLocalValue:3

對於get方法,在ThreadLocal沒有set值得狀況下,默認返回null,全部若是要有一個初始值咱們能夠重寫initialValue()方法,在沒有set值得狀況下調用get則返回初始值。

值得注意的一點:ThreadLocal在線程使用完畢後,咱們應該手動調用remove方法,移除它內部的值,這樣能夠防止內存泄露,固然還有設爲static。

內部實現

ThreadLocal內部有一個靜態類ThreadLocalMap,使用到ThreadLocal的線程會與ThreadLocalMap綁定,維護着這個Map對象,而這個ThreadLocalMap的做用是映射當前ThreadLocal對應的值,它key爲當前ThreadLocal的弱引用:WeakReference

內存泄露問題

對於ThreadLocal,一直涉及到內存的泄露問題,即當該線程不須要再操做某個ThreadLocal內的值時,應該手動的remove掉,爲何呢?咱們來看看ThreadLocal與Thread的聯繫圖:

其中虛線表示弱引用,從該圖能夠看出,一個Thread維持着一個ThreadLocalMap對象,而該Map對象的key又由提供該value的ThreadLocal對象弱引用提供,因此這就有這種狀況:

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

因此避免內存泄露的方法,是對於ThreadLocal要設爲static靜態的,除了這個,還必須在線程不使用它的值是手動remove掉該ThreadLocal的值,這樣Entry就可以在系統gc的時候正常回收,而關於ThreadLocalMap的回收,會在當前Thread銷燬以後進行回收。

總結

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

  • 一、對於synchronized的出現,是解決多線程資源共享的問題,同步機制採用了「以時間換空間」的方式:訪問串行化,對象共享化。同步機制是提供一份變量,讓全部線程均可以訪問。
  • 二、對於Atomic的出現,是經過原子操做指令+Lock-Free完成,從而實現非阻塞式的併發問題。
  • 三、對於Volatile,爲多線程資源共享問題解決了部分需求,在非依賴自身的操做的狀況下,對變量的改變將對任何線程可見。
  • 四、對於ThreadLocal的出現,並非解決多線程資源共享的問題,而是用來提供線程內的局部變量,省去參數傳遞這個沒必要要的麻煩,ThreadLocal採用了「以空間換時間」的方式:訪問並行化,對象獨享化。ThreadLocal是爲每個線程都提供了一份獨有的變量,各個線程互不影響。
相關文章
相關標籤/搜索