多線程之volatile、原子類、synchronized

在引入volatile、原子類、synchronized前,我們先來(lái)說(shuō)說(shuō)Java內(nèi)存模型的三大特性:可見(jiàn)性、原子性和有序性。
可見(jiàn)性:在多線程中,任一線程的修改對(duì)其它線程都是可見(jiàn)的。(確保可見(jiàn)性的方法有:volatile、synchronized 和 final)
原子性:指的是一個(gè)操作無(wú)法再進(jìn)行分割。(確保原子性的方法有:synchronized、原子類和加lock)
有序性:指代碼按程序員編寫(xiě)的順序串行執(zhí)行,然而在多線程中會(huì)出現(xiàn)指令重排。(確保有序性的方法有: volatile 和 synchronized)
下面,我們舉個(gè)例子對(duì)這多種方案進(jìn)行一一驗(yàn)證:

    private static final int THREADS_CONUT = 200;
    private static int count=0;
    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[THREADS_CONUT];
        for (int i = 0; i < THREADS_CONUT; i++) {
            threads[i] = new Thread(() -> {
                for (int i1 = 0; i1 < 100; i1++) {
                    count++;
                    //為了讓結(jié)果更加明顯,此處進(jìn)行休眠
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            threads[i].start();
        }
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println("執(zhí)行結(jié)果:"+count);
    }

這是個(gè)很常見(jiàn)的線程安全問(wèn)題的例子,總共有200個(gè)線程,然后計(jì)數(shù)100次,拋開(kāi)線程安全問(wèn)題的話,總數(shù)應(yīng)該是200*100=20000,然而某一次的運(yùn)行結(jié)果如下:

執(zhí)行結(jié)果:19687

注意到最后的循環(huán)判斷是:Thread.activeCount() > 2
這里為什么是大于2而不是1呢?按理來(lái)說(shuō)應(yīng)該最后只剩下一個(gè)主線程才對(duì)啊?如果我們把最后一部分進(jìn)行如下的更改:

  while (Thread.activeCount() > 1) {
      System.out.println("當(dāng)前線程數(shù):"+Thread.activeCount());
      Thread.currentThread().getThreadGroup().list();
      Thread.yield();
  }

你會(huì)發(fā)現(xiàn)程序進(jìn)入了死循環(huán)了,不停地循環(huán)打印出如下結(jié)果:

當(dāng)前線程數(shù):2
java.lang.ThreadGroup[name=main,maxpri=10]
Thread[main,5,main]
Thread[Monitor Ctrl-Break,5,main]

其實(shí),從上面的日志我們也可以看出來(lái)了,程序運(yùn)行到最后是包含兩個(gè)線程的:主線程和守護(hù)線程,所以程序才會(huì)陷入無(wú)限循環(huán)中。
再說(shuō)回線程安全的問(wèn)題,其實(shí)主要的原因在于:count++;這一步并不是原子操作,它可拆分為count=count+1,故在多線程中就會(huì)出現(xiàn)上面的線程安全問(wèn)題。
既然我們今天說(shuō)的是volatile、原子類、synchronized,那肯定要進(jìn)行逐一試驗(yàn)的咯:

1、volatile

首先將上述中的count變量用volatile關(guān)鍵字來(lái)進(jìn)行修飾,如下:

private static volatile int count=0;

某次的運(yùn)行結(jié)果如下:

執(zhí)行結(jié)果:19833

事實(shí)證明,volatile關(guān)鍵字并不能確保Java內(nèi)存的原子性,正如上面所說(shuō)的,volatile關(guān)鍵字能確保的是可見(jiàn)性和有序性,下面來(lái)說(shuō)說(shuō)volatile關(guān)鍵字是如何確保可見(jiàn)性和有序性的:
我們先來(lái)看看多線程中Java內(nèi)存模型是怎么樣的?借用網(wǎng)上的一張圖(侵刪):


多線程中的內(nèi)存模型

如上圖,我們知道每個(gè)線程都有自己獨(dú)立的工作內(nèi)存,對(duì)于普通變量,線程是不直接操作主內(nèi)存中的變量,操作的是工作內(nèi)存從主內(nèi)存copy過(guò)來(lái)的副本變量,而使用如果使用了volatile關(guān)鍵字標(biāo)記的變量,則是直接操作主內(nèi)存的變量,因此確保了可見(jiàn)性。
另一方面,使用volatile關(guān)鍵字修飾的變量,會(huì)禁止指令重排序優(yōu)化,禁止將后面的指令排到該變量前,相當(dāng)于建立了一個(gè)“內(nèi)存屏障”,以此來(lái)確保有序性。
性能方面,volatile 的讀性能消耗與普通變量幾乎相同,但是寫(xiě)操作稍慢,因?yàn)樗枰诒镜卮a中插入許多內(nèi)存屏障指令來(lái)保證處理器不發(fā)生亂序執(zhí)行。

2、原子類

Java 5.0 提供了 java.util.concurrent(簡(jiǎn)稱JUC)包,根據(jù)修改的數(shù)據(jù)類型,可以將JUC包中的原子操作類可以分為4類。

  1. 基本類型: AtomicInteger, AtomicLong, AtomicBoolean ;
  2. 數(shù)組類型: AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray ;
  3. 引用類型: AtomicReference, AtomicStampedRerence, AtomicMarkableReference ;
  4. 對(duì)象的屬性修改類型: AtomicIntegerFieldUpdater, AtomicLongFieldUpdater, AtomicReferenceFieldUpdater 。
    還是回到上面的例子,我們來(lái)看看原子類能否解決上面出現(xiàn)的線程安全問(wèn)題,將代碼更改為如下:
    private static final int THREADS_CONUT = 200;
    private  static AtomicInteger count = new AtomicInteger(0);
    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[THREADS_CONUT];
        for (int i = 0; i < THREADS_CONUT; i++) {
            threads[i] = new Thread(() -> {
                for (int i1 = 0; i1 < 100; i1++) {
                    count.incrementAndGet();
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            threads[i].start();
        }
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println("執(zhí)行結(jié)果:"+count);
    }

執(zhí)行結(jié)果:20000

事實(shí)證明,“原子類”這個(gè)名字可不是白叫的,果然能保證其原子性。
那么,原子類實(shí)現(xiàn)的原理又是什么呢?其實(shí),原子類是基于CAS實(shí)現(xiàn),而CAS是通過(guò)硬件命令保證了原子性,所以在性能方面有一定的優(yōu)勢(shì)。
CAS是一種“樂(lè)觀鎖”技術(shù),當(dāng)多個(gè)線程嘗試使用CAS同時(shí)更新同一個(gè)變量時(shí),只有其中一個(gè)線程能更新變量的值,而其它線程都失敗,失敗的線程并不會(huì)被掛起,而是被告知這次競(jìng)爭(zhēng)中失敗,并可以再次嘗試。
CAS有3個(gè)操作數(shù),內(nèi)存值V,舊的預(yù)期值A(chǔ),要修改的新值B。當(dāng)且僅當(dāng)預(yù)期值A(chǔ)和內(nèi)存值V相同時(shí),將內(nèi)存值V修改為B,否則返回V。
CAS存在著ABA問(wèn)題:在更新前的值是A,但在操作過(guò)程中被其他線程更新為B,又更新為 A。此時(shí),按照上面的規(guī)則,當(dāng)前線程判斷可執(zhí)行,但其實(shí)發(fā)生了不一致現(xiàn)象,需評(píng)估是否對(duì)程序存在影響(極少極少)。

3、synchronized

與“樂(lè)觀鎖”相對(duì)應(yīng)的是“悲觀鎖”,而synchronized就是一種悲觀鎖技術(shù)。
樂(lè)觀鎖相信的是在它修改之前,沒(méi)有其它線程去修改它。
悲觀鎖則認(rèn)為在它修改之前,一定會(huì)有其它線程去修改它,悲觀鎖效率很低。
同樣的,我們使用synchronized再次更改上面的例子:

    private static final int THREADS_CONUT = 200;
    private static int count=0;
    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[THREADS_CONUT];
        for (int i = 0; i < THREADS_CONUT; i++) {
            threads[i] = new Thread(() -> {
                for (int i1 = 0; i1 < 100; i1++) {
                    synchronized (threads){
                        count++;
                    }
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            threads[i].start();
        }
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(count);
    }

執(zhí)行結(jié)果:20000

正如最開(kāi)始說(shuō)的,使用synchronized可以保證Java內(nèi)存的三大特性,它是一種悲觀鎖,它只有在確保其它線程不會(huì)造成干擾的情況下執(zhí)行,會(huì)導(dǎo)致其它所有需要鎖的線程掛起,等待持有鎖的線程釋放鎖。
所以,如果如上面的例子的話,使用原子類會(huì)比synchronized在性能上有更大的優(yōu)勢(shì)!

最后編輯于
?著作權(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),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

  • 本文首發(fā)于我的個(gè)人博客:尾尾部落 本文是我刷了幾十篇一線互聯(lián)網(wǎng)校招java后端開(kāi)發(fā)崗位的面經(jīng)后總結(jié)的多線程相關(guān)題目...
    繁著閱讀 2,125評(píng)論 0 7
  • Java8張圖 11、字符串不變性 12、equals()方法、hashCode()方法的區(qū)別 13、...
    Miley_MOJIE閱讀 3,894評(píng)論 0 11
  • 從三月份找實(shí)習(xí)到現(xiàn)在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂(lè)視家的研發(fā)崗...
    時(shí)芥藍(lán)閱讀 42,789評(píng)論 11 349
  • 在這里 有溫婉優(yōu)雅的園長(zhǎng) 有滿腹才華的老師 有用心的保育老師 每一天 都是歡聲笑語(yǔ) 每一刻 都是滿滿幸福 每一秒 ...
    Catherineliao閱讀 448評(píng)論 0 4
  • 累世多艱 欲明志還需泊淡 浮生欠安 偏涼風(fēng)又動(dòng)孤簾 土爐火暖 煮清茗誰(shuí)藉云天 微身意寒 分濁酒夢(mèng)冷炊煙
    阡陌悠塵閱讀 371評(píng)論 2 17

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