線程共享和協(xié)作(二):Synchronized、ThreadLocal、Volatile如何實(shí)現(xiàn)線程共享

共享對(duì)象

使用Java編寫線程安全的程序關(guān)鍵在于正確的使用共享對(duì)象,以及安全的對(duì)其進(jìn)行訪問管理。Java的內(nèi)置鎖可以保障線程安全,對(duì)于其他的應(yīng)用來說并發(fā)的安全性是使用內(nèi)置鎖保障了線程變量使用的邊界。談到線程的邊界問題,隨之而來的是Java內(nèi)存模型另外的一個(gè)重要的含義,可見性。Java對(duì)可見性提供的原生支持是volatile關(guān)鍵字。

Atomic

作用

對(duì)于原子操作類,Java的concurrent并發(fā)包中主要為我們提供了這么幾個(gè)常用的:AtomicInteger、AtomicLong、AtomicBoolean、AtomicReference<T>。
對(duì)于原子操作類,最大的特點(diǎn)是在多線程并發(fā)操作同一個(gè)資源的情況下,使用Lock-Free算法來替代鎖,這樣開銷小、速度快,對(duì)于原子操作類是采用原子操作指令實(shí)現(xiàn)的,從而可以保證操作的原子性。
通常情況下,在Java里面,++i或者--i不是線程安全的,這里面有三個(gè)獨(dú)立的操作:獲得變量當(dāng)前值,為該值+1/-1,然后寫回新的值。在沒有額外資源可以利用的情況下,只能使用加鎖才能保證讀-改-寫這三個(gè)操作是“原子性”的。
Java 5新增了AtomicInteger類,該類包含方法getAndIncrement()以及getAndDecrement(),這兩個(gè)方法實(shí)現(xiàn)了原子加以及原子減操作,但是比較不同的是這兩個(gè)操作沒有使用任何加鎖機(jī)制,屬于無鎖操作。
它會(huì)在這步操作都完成情況下才允許其它線程再對(duì)它進(jìn)行操作,而這個(gè)實(shí)現(xiàn)則是通過Lock-Free+原子操作指令來確定的
AtomicInteger類中:

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

public final int get() {  
    return value;  
}  

private volatile int value;

public final boolean compareAndSet(int expect, int update) {  
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);  
}  

可以看到是一個(gè)cas原子操作。
unsafe是java用來在CPU級(jí)別的操作CAS指令的類,對(duì)于程序員來說,此類是不可用。
由于是cpu級(jí)別的指令,其開銷比需要操作系統(tǒng)參與的鎖的開銷小。
對(duì)于多個(gè)線程進(jìn)入時(shí),會(huì)先比較現(xiàn)在的value 是否與expect相等,如果不相等,則進(jìn)入下一個(gè)循環(huán)。如果相等,則會(huì)更新成update值。
之后再進(jìn)入的線程則會(huì)死循環(huán)。這樣就保證了操作的原子性。
這樣一個(gè)方法中 即包含了原子性,又包含了可見性

而關(guān)于Lock-Free算法,則是一種新的策略替代鎖來保證資源在并發(fā)時(shí)的完整性的,Lock-Free的實(shí)現(xiàn)有三步:

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

用法

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

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

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

內(nèi)部實(shí)現(xiàn)

采用Lock-Free算法替代鎖+原子操作指令實(shí)現(xiàn)并發(fā)情況下資源的安全、完整、一致性

ABA問題(AtomicStampedReference的使用)

public class ABA {

    // 普通的原子類,存在ABA問題
    AtomicInteger a1 = new AtomicInteger(10);
    // 帶有時(shí)間戳的原子類,不存在ABA問題,第二個(gè)參數(shù)就是默認(rèn)時(shí)間戳,這里指定為0
    AtomicStampedReference<Integer> a2 = new AtomicStampedReference<Integer>(10, 0);

    public static void main(String[] args) {
        ABA a = new ABA();
        a.test();
    }

    public void test() {
        new Thread1().start();
        new Thread2().start();
        new Thread3().start();
        new Thread4().start();
    }

    class Thread1 extends Thread {
        @Override
        public void run() {
            a1.compareAndSet(10, 11);
            a1.compareAndSet(11, 10);
        }
    }
    class Thread2 extends Thread {
        @Override
        public void run() {
            try {
                Thread.sleep(200);  // 睡0.2秒,給線程1時(shí)間做ABA操作
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("AtomicInteger原子操作:" + a1.compareAndSet(10, 11));
        }
    }
    class Thread3 extends Thread {
        @Override
        public void run() {
            try {
                Thread.sleep(500);  // 睡0.5秒,保證線程4先執(zhí)行
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            int stamp = a2.getStamp();
            a2.compareAndSet(10, 11, stamp, stamp + 1);
            stamp = a2.getStamp();
            a2.compareAndSet(11, 10, stamp, stamp + 1);
        }
    }
    class Thread4 extends Thread {
        @Override
        public void run() {
            int stamp = a2.getStamp();
            try {
                Thread.sleep(1000);  // 睡一秒,給線程3時(shí)間做ABA操作
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("AtomicStampedReference原子操作:" + a2.compareAndSet(10, 11, stamp, stamp + 1));
        }
    }
}

Volatile

作用

Volatile可以看做是一個(gè)輕量級(jí)的synchronized,它可以在多線程并發(fā)的情況下保證變量的“可見性”,
什么是可見性?
就是在一個(gè)線程的工作內(nèi)存中修改了該變量的值,該變量的值立即能回顯到主內(nèi)存中,從而保證所有的線程看到這個(gè)變量的值是一致的,其二 volatile 禁止了指令重排,所以在處理同步問題上它大顯作用,而且它的開銷比synchronized小、使用成本更低。
雖然 volatile 變量具有可見性和禁止指令重排序,但是并不能說 volatile 變量能確保并發(fā)安全。

舉個(gè)栗子:在寫單例模式中,除了用靜態(tài)內(nèi)部類外,還有一種寫法也非常受歡迎,就是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;
    }
}

這樣單例不管在哪個(gè)線程中創(chuàng)建的,所有線程都是共享這個(gè)單例的。
雖說這個(gè)Volatile關(guān)鍵字可以解決多線程環(huán)境下的同步問題,不過這也是相對(duì)的,因?yàn)樗痪哂胁僮鞯脑有裕簿褪撬贿m合在對(duì)該變量的寫操作依賴于變量本身自己。舉個(gè)最簡單的栗子:在進(jìn)行計(jì)數(shù)操作時(shí)count++,實(shí)際是count=count+1;,count最終的值依賴于它本身的值。所以使用volatile修飾的變量在進(jìn)行這么一系列的操作的時(shí)候,就有并發(fā)的問題 .

volatile只能確保操作的是同一塊內(nèi)存,并不能保證操作的原子性。所以volatile一般用于聲明簡單類型變量,使得這些變量具有原子性,即一些簡單的賦值與返回操作將被確保不中斷。但是當(dāng)該變量的值由自身的上一個(gè)決定時(shí),volatile的作用就將失效,這是由volatile關(guān)鍵字的性質(zhì)所決定的。
所以在volatile時(shí)一定要謹(jǐn)慎,千萬不要以為用volatile修飾后該變量的所有操作都是原子操作,不再需要synchronized關(guān)鍵字了。

用法

因?yàn)関olatile不具有操作的原子性,所以如果用volatile修飾的變量在進(jìn)行依賴于它自身的操作時(shí),就有并發(fā)問題,如:count,像下面這樣寫在并發(fā)環(huán)境中是達(dá)不到任何效果的:

public class Counter {
    private volatile int count;

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

而要想count能在并發(fā)環(huán)境中保持?jǐn)?shù)據(jù)的一致性,則可以在increment()中加synchronized同步鎖修飾,改進(jìn)后的為:

public class Counter {
    private volatile/無 int count;

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

內(nèi)部實(shí)現(xiàn)

匯編指令實(shí)現(xiàn)

Synchronized

作用

synchronized關(guān)鍵字是Java利用鎖的機(jī)制自動(dòng)實(shí)現(xiàn)的,一般有同步方法和同步代碼塊兩種使用方式。Java中所有的對(duì)象都自動(dòng)含有單一的鎖(也稱為監(jiān)視器),當(dāng)在對(duì)象上調(diào)用其任意的synchronized方法時(shí),此對(duì)象被加鎖(一個(gè)任務(wù)可以多次獲得對(duì)象的鎖,計(jì)數(shù)會(huì)遞增),同時(shí)在線程從該方法返回之前,該對(duì)象內(nèi)其他所有要調(diào)用類中被標(biāo)記為synchronized的方法的線程都會(huì)被阻塞。當(dāng)然針對(duì)每個(gè)類也有一個(gè)鎖(作為類的Class對(duì)象的一部分),所以你懂的.
正因?yàn)樗谶@種阻塞的策略,所以它的性能不太好,但是由于操作上的優(yōu)勢,只需要簡單的聲明一下即可,而且被它聲明的代碼塊也是具有操作的原子性。
最后需要注意的是synchronized是同步機(jī)制中最安全的一種方式,其他的任何方式都是有風(fēng)險(xiǎn)的,當(dāng)然付出的代價(jià)也是最大的。

用法

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

內(nèi)部實(shí)現(xiàn)

synchronized 關(guān)鍵字原理

ThreadLocal

作用

而ThreadLocal的設(shè)計(jì),并不是解決資源共享的問題,而是用來提供線程內(nèi)的局部變量,這樣每個(gè)線程都自己管理自己的局部變量,別的線程操作的數(shù)據(jù)不會(huì)對(duì)我產(chǎn)生影響,互不影響,所以不存在解決資源共享這么一說,如果是解決資源共享,那么其它線程操作的結(jié)果必然我需要獲取到,而ThreadLocal則是自己管理自己的,相當(dāng)于封裝在Thread內(nèi)部了,供線程自己管理,這樣做其實(shí)就是以空間換時(shí)間的方式(與synchronized相反),以耗費(fèi)內(nèi)存為代價(jià),單大大減少了線程同步(如synchronized)所帶來性能消耗以及減少了線程并發(fā)控制的復(fù)雜度。

用法

ThreadLocal實(shí)例通常來說都是private static類型的,用于關(guān)聯(lián)線程和線程的上下文
一般使用ThreadLocal,官方建議我們定義為private static ,至于為什么要定義成靜態(tài)的,這和內(nèi)存泄露有關(guān),后面再討論。
它有三個(gè)暴露的方法,set、get、remove。

public class TestThreadLocal {
    private static final ThreadLocal<Integer> value = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(new MyThread(i)).start();
        }
    }

    static class MyThread implements Runnable {
        private int index;

        public MyThread(int index) {
            this.index = index;
        }

        public void run() {
            System.out.println("線程" + index + "的初始value:" + value.get());
            for (int i = 0; i < 10; i++) {
                value.set(value.get() + i);
            }
            System.out.println("線程" + index + "的累加value:" + value.get());
        }
    }
}

運(yùn)行結(jié)果如下,這些ThreadLocal變量屬于線程內(nèi)部管理的,互不影響:

線程0的初始value:0
線程3的初始value:0
線程2的初始value:0
線程2的累加value:45
線程1的初始value:0
線程3的累加value:45
線程0的累加value:45
線程1的累加value:45
線程4的初始value:0
線程4的累加value:45

對(duì)于get方法,在ThreadLocal沒有set值得情況下,默認(rèn)返回null,所有如果要有一個(gè)初始值我們可以重寫initialValue()方法,在沒有set值得情況下調(diào)用get則返回初始值。

內(nèi)部實(shí)現(xiàn)

ThreadLocal內(nèi)部有一個(gè)靜態(tài)類ThreadLocalMap,使用到ThreadLocal的線程會(huì)與ThreadLocalMap綁定,維護(hù)著這個(gè)Map對(duì)象,而這個(gè)ThreadLocalMap的作用是映射當(dāng)前ThreadLocal對(duì)應(yīng)的值,它key為當(dāng)前ThreadLocal的弱引用:WeakReference

內(nèi)存泄露問題

對(duì)于ThreadLocal,一直涉及到內(nèi)存的泄露問題,即當(dāng)該線程不需要再操作某個(gè)ThreadLocal內(nèi)的值時(shí),應(yīng)該手動(dòng)的remove掉,為什么呢?我們來看看ThreadLocal與Thread的聯(lián)系圖:
此圖來自網(wǎng)絡(luò):

[圖片上傳失敗...(image-b3d685-1606987896368)]

其中虛線表示弱引用,從該圖可以看出,一個(gè)Thread維持著一個(gè)ThreadLocalMap對(duì)象,而該Map對(duì)象的key又由提供該value的ThreadLocal對(duì)象弱引用提供,所以這就有這種情況:
如果ThreadLocal不設(shè)為static的,由于Thread的生命周期不可預(yù)知,這就導(dǎo)致了當(dāng)系統(tǒng)gc時(shí)將會(huì)回收它,而ThreadLocal對(duì)象被回收了,此時(shí)它對(duì)應(yīng)key必定為null,這就導(dǎo)致了該key對(duì)應(yīng)得value拿不出來了,而value之前被Thread所引用,所以就存在key為null、value存在強(qiáng)引用導(dǎo)致這個(gè)Entry回收不了,從而導(dǎo)致內(nèi)存泄露。

所以避免內(nèi)存泄露的方法,是對(duì)于ThreadLocal要設(shè)為static靜態(tài)的,
這樣的話ThreadLocal的生命周期就更長,由于一直存在ThreadLocal的強(qiáng)引用,所以ThreadLocal也就不會(huì)被回收,也就能保證任何時(shí)候都能根據(jù)ThreadLocal的弱引用訪問到Entry的value值,然后remove它,防止內(nèi)存泄露。除了這個(gè),還必須在線程不使用它的值是手動(dòng)remove掉該ThreadLocal的值,這樣Entry就能夠在系統(tǒng)gc的時(shí)候正?;厥?,而關(guān)于ThreadLocalMap的回收,會(huì)在當(dāng)前Thread銷毀之后進(jìn)行回收。

但需要注意的是,雖然ThreadLocal和Synchonized都用于解決多線程并發(fā)訪問,ThreadLocal與synchronized還是有本質(zhì)的區(qū)別。synchronized是利用鎖的機(jī)制,使變量或代碼塊在某一時(shí)該只能被一個(gè)線程訪問。而ThreadLocal為每一個(gè)線程都提供了變量的副本,使得每個(gè)線程在某一時(shí)間訪問到的并不是同一個(gè)對(duì)象,這樣就隔離了多個(gè)線程對(duì)數(shù)據(jù)的數(shù)據(jù)共享。而Synchronized卻正好相反,它用于在多個(gè)線程間通信時(shí)能夠獲得數(shù)據(jù)共享。即Synchronized用于線程間的數(shù)據(jù)共享,而ThreadLocal則用于線程間的數(shù)據(jù)隔離。所以ThreadLocal并不能代替synchronized,Synchronized的功能范圍更廣(同步機(jī)制)。

  • 補(bǔ)充
InheritableThreadLocal

ThreadLocal類固然很好,但是子線程并不能取到父線程的ThreadLocal類的變量,InheritableThreadLocal類就是解決這個(gè)問題的。

/**
 *TODO 驗(yàn)證線程變量間的隔離性
 */
public class Test3 {

    public static void main(String[] args) {
        try {
            for (int i = 0; i < 10; i++) {
                System.out.println("       在Main線程中取值=" + Tools.tl.get());
                Thread.sleep(100);
            }
            Thread.sleep(5000);
            ThreadA a = new ThreadA();
            a.start();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    /*static public class Tools {
        public static ThreadLocalExt tl = new ThreadLocalExt();
    }
    static public class ThreadLocalExt extends ThreadLocal {
        @Override
        protected Object initialValue() {
            return new Date().getTime();
        }
    }*/
static public class Tools {
        public static InheritableThreadLocalExt tl = new InheritableThreadLocalExt();
    }
    static public class InheritableThreadLocalExt extends InheritableThreadLocal {
        @Override
        protected Object initialValue() {
            return new Date().getTime();
        }

        @Override
        protected Object childValue(Object parentValue) {
            return parentValue + " 我在子線程加的~!";
        }
    }

    static public class ThreadA extends Thread {

        @Override
        public void run() {
            try {
                for (int i = 0; i < 10; i++) {
                    System.out.println("在ThreadA線程中取值=" + Tools.tl.get());
                    Thread.sleep(100);
                }
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }

    }
}

在使用InheritableThreadLocal類需要注意的一點(diǎn)是:如果子線程在取得值的同時(shí),主線程將InheritableThreadLocal中的值進(jìn)行更改,那么子線程取到的還是舊值。

總結(jié)

關(guān)于Volatile關(guān)鍵字具有可見性,但不具有操作的原子性,而synchronized比volatile對(duì)資源的消耗稍微大點(diǎn),但可以保證變量操作的原子性,保證變量的一致性,最佳實(shí)踐則是二者結(jié)合一起使用。

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

  2. 對(duì)于Atomic的出現(xiàn),是通過原子操作指令+Lock-Free完成,從而實(shí)現(xiàn)非阻塞式的并發(fā)問題。

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

  4. ThreadLocal的作用是提供線程內(nèi)的局部變量,這種變量在線程的生命周期內(nèi)起作用,減少同一個(gè)線程內(nèi)多個(gè)函數(shù)或者組件之間一些公共變量的傳遞的復(fù)雜度。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容