Java多線程基礎(chǔ)與使用詳細(xì)篇(五)----volatile與單例模式

前言

繼續(xù)學(xué)習(xí)Java多線程基礎(chǔ)與使用詳細(xì)篇(四)----Java內(nèi)存模型下的知識。本篇會涉及volatile關(guān)鍵字以及單例模式采章。

1. volatile 是什么

(1).volatile是一種同步機制,比synchronized或者Lock相關(guān)類更輕量,因為使用volatile并不會發(fā)生上下文切換等開銷很大的行為瘦馍。
(2).如果一個變量別修飾成volatile,那么JVM就知道了這個變量可能會被并發(fā)修改。
(3).但是開銷小,相應(yīng)的能力也小逛薇,雖然說volatile是用來同步的保證線程安全的,但是volatile做不到synchronized那樣的原子保護, volatile僅在很有限的場景下才能發(fā)揮作用惠桃。

2.volatile 適用場景

2.1 不適用: a++

代碼演示:
與AtomicInteger 相比下,
打印出的值沒有到預(yù)期的結(jié)果惰爬,只有AtomicInteger 是預(yù)期的喊暖,
所以a ++的時候沒有起到原子保護

public class NoVolatile implements Runnable {

    volatile int a;
    AtomicInteger realA = new AtomicInteger();

    public static void main(String[] args) throws InterruptedException {
        Runnable r =  new NoVolatile();
        Thread thread1 = new Thread(r);
        Thread thread2 = new Thread(r);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(((NoVolatile) r).a);
        System.out.println(((NoVolatile) r).realA.get());
    }
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            a++;
            realA.incrementAndGet();
        }
    }
}
18909
20000
2.2 不適用場合2

在下面適用了 done = !done;,在前面運行的時候有起到原子保護作用一直是flase撕瞧,
但是在多次運行之后就是true了陵叽,因此這樣是不適應(yīng)的場景。

public class NoVolatile2 implements Runnable {

    volatile boolean done = false;
    AtomicInteger realA = new AtomicInteger();

    public static void main(String[] args) throws InterruptedException {
        Runnable r =  new NoVolatile2();
        Thread thread1 = new Thread(r);
        Thread thread2 = new Thread(r);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(((NoVolatile2) r).done);
        System.out.println(((NoVolatile2) r).realA.get());
    }
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            flipDone();
            realA.incrementAndGet();
        }
    }

    private void flipDone() {
        done = !done;
    }
}
2.3 適用場合1

適用 boolean flag,如果一個共享變量自始至終只被
各個線程賦值丛版,而沒有其他的操作巩掺,那么就可以用volatile來代替
synchronized或代替原子變量,因為賦值自身是有原子性的页畦,而volatile
又保證了可見性胖替,所以就足以保證線程安全

public class UseVolatile1 implements Runnable {

    volatile boolean done = false;
    AtomicInteger realA = new AtomicInteger();

    public static void main(String[] args) throws InterruptedException {
        Runnable r =  new UseVolatile1();
        Thread thread1 = new Thread(r);
        Thread thread2 = new Thread(r);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(((UseVolatile1) r).done);
        System.out.println(((UseVolatile1) r).realA.get());
    }
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            setDone();
            realA.incrementAndGet();
        }
    }

    private void setDone() {
        done = true;
    }
}
true
20000
2.4 適用場合2

作為刷新之前變量的觸發(fā)器

// 例如聲明一個 volatile 關(guān)鍵字
volatile boolean flag = false豫缨;
.....
//  Thread A
.....
flag = true  // 賦值為true

....
//  Thread B
if(!flag){      //此時已經(jīng)刷新了独令,被線程B完全的看到了
}    

3. volatile的作用: 可見性、禁止重排序

3.1. 可見性:讀一個volatile變量之前好芭,需要先使相應(yīng)的本地緩存失效燃箭,這樣就必須到主內(nèi)存讀取最新值,寫一個volatile 屬性會立即刷入到主內(nèi)存
3.2. 禁止指令重排序優(yōu)化:解決單例雙重鎖亂序的問題

4.volatile和synchronized的關(guān)系

volatile在這方面可以看做是輕量版的synchronized:如果一個共享變量
自始至終只被各個線程賦值舍败,而沒有其他的操作招狸,那么就可以用
volatile來代替synchronized或者代替原子變量,因為賦值自身是有原
子性的 ,而volatile又保證了可見性邻薯,所以就足以保證線程安全裙戏。

5. volatile 學(xué)習(xí)小結(jié)

(1).volatile修飾符適用于以下場景:某個屬性被多個線程共享,其中
有一個線程修改了此屬性厕诡,其他線程可以立即得到修改后的值累榜,比如
Boolean flag ; 或者作為觸發(fā)器,實現(xiàn)輕量級同步
(2). volatile屬性的讀寫操作都是無鎖的木人,它不能替代synchronized
因為它沒有提供原子性和互斥性信柿。因為無鎖,不需要花費時間在獲取鎖和
釋放鎖上醒第,所以說它是低成本的渔嚷。
(3). volatile只能作用于屬性,我們用volatile修飾屬性稠曼,這樣compilers就不會對這個屬性做指令重排序形病。
(4).volatile提供了可見性,任何一個線程對其的修改將立馬對其他線程可見。volatile 屬性不會被線程緩存漠吻,始終從主存中讀取量瓜。
(5).volatile 提供了happens-before保證,對volatile變量v的寫入happens-before所有其他線程后續(xù)對v的讀操作途乃。
(6).volatile可以使得long和double的賦值是原子的绍傲,后面馬上會講long和double的原子性

6.能保證可見性的措施

(1).除了 volatile可以讓變量保證可見性外,synchronized耍共、lock烫饼、并發(fā)集合、
Thread.join()和Thread.start()等都可以保證可見性
(2). 具體看happens-before 原則的規(guī)定
(3). 升華:對 synchronized 可見性正確理解
synchronized不僅保證了原子性试读、還保證了可見性
synchronized不僅被保護的代碼安全杠纵,還近朱者赤
演示假設(shè)演示:

   // 假設(shè) 聲明  a b c
    int a  = 1 ;
    int b  = 3 钩骇;
    int c  = 2 比藻;
void change(){
      a  = 3;
      b  =  4;
sysnchronized(this){    // a,b 發(fā)生在sysnchronized 解鎖之前倘屹,第一個線程進入到sysnchronized內(nèi)后就進行解鎖。
       c = 5;
}
void prinft(){
    sysnchronized(this){
          int  a1 = a;    //  利用sysnchronized happen-before 原則
                              //  第二個線程進到這里的時候它就可看到之前所有的變化纽匙,包括a,b內(nèi)的值
     }
      int b2 = b;
      int c2 = c;
    }
}

7.原子性

什么是原子性
(1). 一系列的操作,要么全部執(zhí)行成功哄辣,要么全部不執(zhí)行赠尾,不會出現(xiàn)執(zhí)行一般的情況,是不可分割气嫁。
(2). 例如ATM里取錢這樣的例子
(3). i++ 不是原子性

image.png

假設(shè)從上面的圖可以看到兩個線程

//假設(shè)當(dāng)線程一的
i = 1時,
//執(zhí)行
i = i +1 寸宵,
//最終結(jié)果
i = 2,
但是在線程二的時候沒讀到或者沒有看到梯影,它是看到i=1巫员,所以就不是原子性的
7. 1用synchronized 實現(xiàn)原子性

由于上面 i ++ 導(dǎo)致不是原子性的問題,可以使用synchronized保證同時只有一個線程運行
,這樣就是實現(xiàn)了原子性操作甲棍。

      //例如
     synchronized (this){
     ......
         i = i +1 ;
  }
7.2. Java中的原子操作有哪些

(1). 除 long 和double 之外的基本類型(int, byte. boolean,short,char,float)的賦值操作
(2). 所有引用reference的賦值操作简识,不管是32位的機器還是64位的機器
(3). Java.concurrent.Atomic. 包中所有類的原子操作*

7.3. long和double的原子性

(1). 問題描述:官方文檔、對于64位的值的寫入,可以分為兩個32位的操作進行寫入七扰、
讀取錯誤奢赂、使用volatile解決
(2). 結(jié)論:在32位上的JVM上。long 和double 的操作不是原子的颈走,但是在64位的JVM是原子的
(3). 實際開發(fā)中:商用Java虛擬機中不會出現(xiàn)

7.4 原子操作+ 原子操作 膳灶!= 原子操作

(1). 簡單地把原子操作組合在一起,并不能保證整體依賴具有原子性
(2). 比如我去ATM機兩次取錢是兩次獨立的原子操作立由,但是期間有可能銀行卡被借給
別人轧钓,也就是被其它線程打斷并被修改。
(3). 全同步的HashMap也不完全安全

8. 單例模式
8.1. 單例模式的作用

為什么需要單例拆吆?
節(jié)省內(nèi)存和計算聋迎,保證結(jié)果正確,方便管理枣耀。

8.2 單例模式的適用場景
  1. 無狀態(tài)的工具類:比如日志工具類霉晕,不管是在哪里使用,我們需要的只是它幫我們記錄日志信息捞奕,除此之外牺堰,并不需要在它的實例對象上存儲任務(wù)狀態(tài),這時候我們就只需要一個實例對象即可颅围。
  2. 全局信息類:比如我們在一個類上記錄網(wǎng)站的訪問次數(shù)伟葫,我們不希望有的訪問被記錄在對象A上,有的卻記錄在對象B上院促,這時候我們就讓這個類成為單例筏养。
8.3. 單例模式的八種寫法
(1). 餓漢式(靜態(tài)常量) [可用]

優(yōu)點:這種寫法比較簡單,就是在類裝載的時候就完成實例化常拓。避免了線程同步問題渐溶。
缺點:在類裝載的時候就完成實例化,沒有達到Lazy Loading的效果弄抬。如果從始至終從未使用過這個實例茎辐,則會造成內(nèi)存的浪費。

public class Singleton1 {

    private final static Singleton1 INSTANCE = new Singleton1();

    private Singleton1() {

    }

    public static Singleton1 getInstance() {
        return INSTANCE;
    }

}
(2).餓漢式(靜態(tài)代碼塊)[可用]

這種方式和上面的方式其實類似掂恕,只不過將類實例化的過程放在了靜態(tài)代碼塊中拖陆,也就是類初始化的時候已經(jīng)加載了

public class Singleton2 {

    private final static Singleton2 INSTANCE;

    static {
        INSTANCE = new Singleton2();
    }

    private Singleton2() {
    }

    public static Singleton2 getInstance() {
        return INSTANCE;
    }
}
(3).懶漢式(線程不安全) [不可用]

這種寫法起到了Lazy Loading的效果懊亡,但是只能在單線程下使用依啰。如果在多線程下,會產(chǎn)生多個實例孔飒。

public class Singleton3 {

    private static Singleton3 instance;

    private Singleton3() {

    }

    public static Singleton3 getInstance() {
        if (instance == null) {
            instance = new Singleton3();
        }
        return instance;
    }
}
(4).懶漢式(線程安全,同步方法)[不推薦]

解決上面第三種實現(xiàn)方式的線程不安全問題桂对,做個線程同步就可以了鸠匀,
缺點:同步效果導(dǎo)致效率低。

public class Singleton4 {

    private static Singleton4 instance;

    private Singleton4() {

    }

    public synchronized static Singleton4 getInstance() {
        if (instance == null) {
            instance = new Singleton4();
        }
        return instance;
    }
}
(5).懶漢式(線程不安全宅此,同步代碼塊)[不推薦]

即便是修改成同步代碼塊父腕,效果也會跟上面一樣導(dǎo)致多個線程會產(chǎn)出多個實例青瀑。

public class Singleton5 {

    private static Singleton5 instance;

    private Singleton5() {

    }

    public static Singleton5 getInstance() {
        if (instance == null) {
            synchronized (Singleton5.class) {
                instance = new Singleton5();
            }
        }
        return instance;
    }
}
(6).雙重檢查[推薦用]

Double-Check是兩次if (singleton == null)檢查斥难,這樣就可以保證線程安全了。
這樣群扶,實例化代碼只用執(zhí)行一次镀裤,后面再次訪問時,判斷if (singleton == null)馁菜,直接return實例。
優(yōu)點:線程安全峭火;延遲加載;效率較高纺且。
使用 volatile 新建對象的好處:

  1. 新建對象實際上有3個步驟
  2. 重排序會帶來NPE
  3. 防止重排序
public class Singleton6 {

    private volatile static Singleton6 instance;

    private Singleton6() {

    }

    public static Singleton6 getInstance() {
        if (instance == null) {
            synchronized (Singleton6.class) {
                if (instance == null) {
                    instance = new Singleton6();
                }
            }
        }
        return instance;
    }
}
(7).靜態(tài)內(nèi)部類[推薦用]

靜態(tài)內(nèi)部類方式在Singleton7類被裝載時并不會立即實例化载碌,而是在需要實例化時,調(diào)用getInstance方法嫁艇,才會裝載SingletonInstance類步咪,從而完成Singleton7的實例化。
類的靜態(tài)屬性只會在第一次加載類的時候初始化点晴,所以在這里悯周,JVM幫助我們保證了線程的安全性禽翼,在類進行初始化時,別的線程是無法進入的仇矾。
優(yōu)點:更優(yōu)雅的方式解总、規(guī)范
1.保證懶加載
2.線程安全
3.效率特別高

public class Singleton7 {

    private Singleton7() {
    }

    private static class SingletonInstance {

        private static final Singleton7 INSTANCE = new Singleton7();
    }

    public static Singleton7 getInstance() {
        return SingletonInstance.INSTANCE;
    }
}
(8).枚舉單例(線程安全)[推薦]

優(yōu)點:
1.線程安全
2.只被裝載一次

public class Singleton8 {

    private Singleton8() {
    }

    private enum Singleton {
        INSTANCE;

        private final Singleton8 instance;

        //構(gòu)建枚舉的函數(shù)的時候已經(jīng)被創(chuàng)建了
        Singleton() {
            instance = new Singleton8();
        }

        public Singleton8 getInstance() {
            return instance;
        }
    }
    public static Singleton8 getInstance() {
        return Singleton.INSTANCE.getInstance();
    }
8.4.用那種單例的實現(xiàn)方案最好
    1. Joshua Bloch 大神在《Effective Java》中明確表達過的觀點:
      "使用"枚舉實現(xiàn)單例方法雖然還沒有廣泛采用刻盐,
      但是單元素的枚舉類型已經(jīng)成為實現(xiàn)Singleton最佳方法
  1. 寫法簡單
  2. 線程安全有保障
  3. 避免反序列化破壞單例
8.5.各種寫法的適用場合
  1. 最好的方法是利用枚舉劳翰,因為還可以防止反序列化重新創(chuàng)建新的對象
  2. 非線程同步的方法不能使用
  3. 如果程序一開始要嘉愛的資源太多,那么就應(yīng)該使用懶加載
  4. 餓漢式如果是對象的創(chuàng)建需要配置文件就不適用
  5. 懶加載雖然好乙墙,但是靜態(tài)內(nèi)部類這種方式會引入編程復(fù)雜性
8.6.什么是原子操作听想?Java中有哪些原子操作马胧?生成對象的過程是不是原子操作
  1. 新建一個空的Person 對象
  2. 把這個對象的地址指向p
  3. 執(zhí)行Person的構(gòu)造函數(shù)
9.總結(jié)

大致上就把Java 多線程的volatile與單例模式學(xué)習(xí)了解,這是用看某學(xué)習(xí)視頻總結(jié)而來的個人學(xué)習(xí)文章蛙粘。希望自己也能對Java多線基礎(chǔ)鞏固起來。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末穴肘,一起剝皮案震驚了整個濱河市梢褐,隨后出現(xiàn)的幾起案子赵讯,更是在濱河造成了極大的恐慌边翼,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,589評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件丈积,死亡現(xiàn)場離奇詭異江滨,居然都是意外死亡厌均,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,615評論 3 396
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來模她,“玉大人,你說我怎么就攤上這事侈净。” “怎么了元扔?”我有些...
    開封第一講書人閱讀 165,933評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長吻氧。 經(jīng)常有香客問我咏连,道長祟滴,這世上最難降的妖魔是什么歌溉? 我笑而不...
    開封第一講書人閱讀 58,976評論 1 295
  • 正文 為了忘掉前任痛垛,我火速辦了婚禮,結(jié)果婚禮上漫谷,老公的妹妹穿的比我還像新娘蹂析。我一直安慰自己,他們只是感情好电抚,可當(dāng)我...
    茶點故事閱讀 67,999評論 6 393
  • 文/花漫 我一把揭開白布俺祠。 她就那樣靜靜地躺著甥温,像睡著了一般。 火紅的嫁衣襯著肌膚如雪姻蚓。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,775評論 1 307
  • 那天捂龄,我揣著相機與錄音加叁,去河邊找鬼。 笑死展融,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的告希。 我是一名探鬼主播,決...
    沈念sama閱讀 40,474評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼喝噪,長吁一口氣:“原來是場噩夢啊……” “哼酝惧!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起晚唇,我...
    開封第一講書人閱讀 39,359評論 0 276
  • 序言:老撾萬榮一對情侶失蹤缺亮,失蹤者是張志新(化名)和其女友劉穎萌踱,沒想到半個月后号阿,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,854評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡园担,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,007評論 3 338
  • 正文 我和宋清朗相戀三年弯汰,在試婚紗的時候發(fā)現(xiàn)自己被綠了湖雹。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,146評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡鸽嫂,死狀恐怖据某,靈堂內(nèi)的尸體忽然破棺而出诗箍,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 35,826評論 5 346
  • 正文 年R本政府宣布橱夭,位于F島的核電站桑逝,受9級特大地震影響楞遏,放射性物質(zhì)發(fā)生泄漏寡喝。R本人自食惡果不足惜勒奇,卻給世界環(huán)境...
    茶點故事閱讀 41,484評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望格二。 院中可真熱鬧顶猜,春花似錦痘括、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,029評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽灶芝。三九已至,卻和暖如春夜涕,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背酸役。 一陣腳步聲響...
    開封第一講書人閱讀 33,153評論 1 272
  • 我被黑心中介騙來泰國打工贱呐, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留入桂,地道東北人。 一個月前我還...
    沈念sama閱讀 48,420評論 3 373
  • 正文 我出身青樓馁蒂,卻偏偏與公主長得像蜘腌,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子沮脖,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,107評論 2 356