并發編程之ThreadLocal、Volatile、synchronized、Atomic關鍵字掃盲

DouSauer 8年前發布 | 14K 次閱讀 Java開發

來自: http://blog.csdn.net/u010687392/article/details/50549236


前言

對于ThreadLocal、Volatile、synchronized、Atomic這四個關鍵字,我想一提及到大家肯定都想到的是解決在多線程并發環境下資源的共享問題,但是要細說每一個的特點、區別、應用場景、內部實現等,卻可能模糊不清,說不出個所以然來,所以,本文就對這幾個關鍵字做一些作用、特點、實現上的講解。

1、Atomic

作用

對于原子操作類,Java的concurrent并發包中主要為我們提供了這么幾個常用的:AtomicInteger、AtomicLong、AtomicBoolean、AtomicReference<T>。
對于原子操作類,最大的特點是在多線程并發操作同一個資源的情況下,使用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的實現有三步:

1、循環(for(;;)、while)
2、CAS(CompareAndSet)
3、回退(return、break)

</blockquote>

用法

比如在多個線程操作一個count變量的情況下,則可以把count定義為AtomicInteger,如下:

public class Counter {
    private AtomicInteger count = new AtomicInteger();

public int getCount() {
    return count.get();
}

public void increment() {
    count.incrementAndGet();
}

}</pre>

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

內部實現

采用Lock-Free算法替代鎖+原子操作指令實現并發情況下資源的安全、完整、一致性

2、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;
}

}</pre>

這樣單例不管在哪個線程中創建的,所有線程都是共享這個單例的。

雖說這個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++;
}

}</pre>

而要想count能在并發環境中保持數據的一致性,則可以在increment()中加synchronized同步鎖修飾,改進后的為:

public class Counter {
    private volatile int count;

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

}</pre>

內部實現

匯編指令實現
可以看這篇詳細了解:Volatile實現原理

3、synchronized

作用

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

用法

    public synchronized void increment(){
            count++;
    }

public void increment(){
    synchronized (Counte.class){
        count++;
    }
}</pre> <h3 id="內部實現-2"><strong>內部實現</strong></h3>

重入鎖ReentrantLock+一個Condition,所以說是Lock的簡化版本,因為一個Lock往往可以對應多個Condition

4、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());
        }
    }

public static void main(String[] args){
    new Thread(new MyRunnable(1));
    new Thread(new MyRunnable(2));
    new Thread(new MyRunnable(3));
}

}</pre>

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

threadLocalValue:2
threadLocalValue:3
threadLocalValue:4

</blockquote>

對于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對資源的消耗稍微大點,但可以保證變量操作的原子性,保證變量的一致性,最佳實踐則是二者結合一起使用。

</blockquote>

1、對于synchronized的出現,是解決多線程資源共享的問題,同步機制采用了“以時間換空間”的方式:訪問串行化,對象共享化。同步機制是提供一份變量,讓所有線程都可以訪問。

2、對于Atomic的出現,是通過原子操作指令+Lock-Free完成,從而實現非阻塞式的并發問題。

3、對于Volatile,為多線程資源共享問題解決了部分需求,在非依賴自身的操作的情況下,對變量的改變將對任何線程可見。

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

</div>

 本文由用戶 DouSauer 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!