(轉)併發編程之ThreadLocal、Volatile、synchronized、Atomic關鍵字掃盲

以下內容來源於微信:

鏈接地址

前言

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

1、Atomic

作用

對於原子操作類,Java的concurrent併發包中主要爲我們提供了這麼幾個常用的:AtomicIntegerAtomicLongAtomicBooleanAtomicReference<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)

用法

比如在多個線程操作一個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。

內部實現

採用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;
    }
}

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

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

內部實現

彙編指令實現

3、synchronized

作用

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

用法

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

    public void increment(){
        synchronized (Counte.class){
            count++;
        }
    }

內部實現

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

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

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

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

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章