多線程(二)線程安全篇

上一篇講到多線程如何使用淑倾,多線程使用時(shí)特別應(yīng)該注意的是線程安全問(wèn)題,本篇將專(zhuān)門(mén)講述問(wèn)題原因和解決方案

為什么:

為什么出現(xiàn)多線程安全問(wèn)題

  • 多線程安全問(wèn)題原因要從jmm(java內(nèi)存模型砖织,非jvm模型)講起款侵。jmm簡(jiǎn)易模型如下圖,

jmm模型.jpg
  • 分為主內(nèi)存和線程工作內(nèi)存侧纯,多個(gè)線程使用共享變量時(shí)新锈,都是先從主內(nèi)存中拷貝到工作內(nèi)存,使用完成之后如果有寫(xiě)入操作則再寫(xiě)入主內(nèi)存眶熬。即線程A與線程B要使用共享變量c妹笆,都是從主內(nèi)存中拷貝一份副本到自己的工作內(nèi)存中,改后再將變更修改回主內(nèi)存娜氏,存在A線程修改變量C后拳缠,B線程不清楚C的修改,用的仍是C的副本贸弥,導(dǎo)致B完成后再次改變變量C窟坐,把A線程對(duì)變量的改動(dòng)覆蓋了∶嗥#或者B沒(méi)讀到A對(duì)C的修改哲鸳。這就存在數(shù)據(jù)不一致的事,A完成的任務(wù)又被B覆蓋了最岗。這就是多線程并發(fā)使用共享變量時(shí)的不安全問(wèn)題帕胆。

什么時(shí)候出現(xiàn)多線程安全問(wèn)題

  • 多線程并發(fā)訪問(wèn)共享資源引起,即多個(gè)線程同時(shí)讀寫(xiě)相同的資源或者共享變量

怎么辦:如何解決多線程安全的問(wèn)題

多線程安全的三個(gè)特性

  • 原子性:即在執(zhí)行一個(gè)或者多個(gè)操作的過(guò)程中般渡,要么一起成功要么一起失敗。典型的i++就是非原子操作,分三步驯用,取出i脸秽,i+1,再把結(jié)果賦給i。中途存在i在取出后再次賦值前蝴乔,存在被其他線程修改的可能性记餐。示例見(jiàn)下面demo,每次運(yùn)行結(jié)果都不同薇正,存在線程安全問(wèn)題片酝。
@Slf4j
public class ThreadNotSafeDemo {
    public static void main(String[] args) throws Exception{
        JobAdd jobAdd=new JobAdd();
        for(int i=0;i<10;i++){
            new Thread(jobAdd,"thread "+i).start();
        }
        Thread.sleep(1000);
        log.debug("thread is {} ,total is {}",Thread.currentThread().getName(),jobAdd.getTotal());
    }
}
@Slf4j
@Data
class JobAdd implements Runnable{
    private int total=0;
    @Override
    public void run() {
        for (int i=0;i<100;i++){
            try {
                // 必須加簡(jiǎn)單的sleep,否則可能當(dāng)前線程在下一個(gè)線程啟動(dòng)前就跑完了挖腰,演示不出效果
                Thread.sleep(10l);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            total++;
        }
        log.debug("thread is {} ,total is {}",Thread.currentThread().getName(),total);
    }
}

打印結(jié)果如下:
05:56:18.109 [thread 1] DEBUG com.dz.demo.multiThread.JobAdd - thread is thread 1 ,total is 912
05:56:18.119 [thread 4] DEBUG com.dz.demo.multiThread.JobAdd - thread is thread 4 ,total is 919
05:56:18.119 [thread 9] DEBUG com.dz.demo.multiThread.JobAdd - thread is thread 9 ,total is 915
05:56:18.119 [thread 8] DEBUG com.dz.demo.multiThread.JobAdd - thread is thread 8 ,total is 917
05:56:18.119 [thread 0] DEBUG com.dz.demo.multiThread.JobAdd - thread is thread 0 ,total is 919
05:56:18.132 [thread 5] DEBUG com.dz.demo.multiThread.JobAdd - thread is thread 5 ,total is 923
05:56:18.132 [thread 3] DEBUG com.dz.demo.multiThread.JobAdd - thread is thread 3 ,total is 924
05:56:18.132 [thread 6] DEBUG com.dz.demo.multiThread.JobAdd - thread is thread 6 ,total is 924
05:56:18.132 [thread 2] DEBUG com.dz.demo.multiThread.JobAdd - thread is thread 2 ,total is 925
05:56:18.132 [thread 7] DEBUG com.dz.demo.multiThread.JobAdd - thread is thread 7 ,total is 923
05:56:18.993 [main] DEBUG com.dz.demo.multiThread.ThreadNotSafeDemo - thread is main ,total is 925

  • 可見(jiàn)性:可見(jiàn)性是指當(dāng)一個(gè)線程對(duì)共享變量進(jìn)行修改后雕沿,能立刻被其他正在使用該變量的線程感知,包含兩步:對(duì)變量修改后立馬同步回主內(nèi)存猴仑;使其他線程的該共享變量的副本值失效审轮,必須重新從主內(nèi)存中獲取。
  • 有序性:一般情況下辽俗,處理器為了提高運(yùn)行效率疾渣,在不影響本線程的前提下會(huì)對(duì)指令的執(zhí)行順序進(jìn)行重排序,代碼運(yùn)行順序可能與編寫(xiě)的順序不一致崖飘。但是對(duì)于多線程情況下榴捡,就容易出現(xiàn)問(wèn)題。當(dāng)前線程指令執(zhí)行先后對(duì)其他線程產(chǎn)生影響朱浴,這就是無(wú)序性吊圾。

要實(shí)現(xiàn)線程安全的幾個(gè)方案

  • 最簡(jiǎn)單的不使用或者慎重使用共享變量或者共享狀態(tài):不使用就不存在多線程并發(fā)訪問(wèn)共享變量安全問(wèn)題
  • 使用jdk已有的線程安全的api,包括以下幾種:java.util.concurrent.atomic包下的原子類(lèi)如AtomicBoolean赊琳、AtomicInteger街夭、AtomicIntegerArray、AtomicLong躏筏、DoubleAdder等板丽;可變字符串StringBuffer;java并發(fā)包下線程安全的集合趁尼,如ConcurrentHashMap埃碱、CopyOnWriteArrayList、CopyOnWriteArraySet酥泞、ConcurrentSkipListMap砚殿、BlockingQueue的實(shí)現(xiàn)類(lèi);一些老的線程安全集合如Hashtable芝囤,不過(guò)不推薦似炎,性能較低
  • 使用jdk自帶的同步關(guān)鍵字synchronized,它可以用在方法和代碼塊上辛萍。使用在方法上是取該對(duì)象的監(jiān)視器為同步對(duì)象。使用在代碼塊上則是取synchronized括號(hào)里的對(duì)象的監(jiān)視器為同步對(duì)象羡藐,如果使用靜態(tài)方法上贩毕,則是取該類(lèi)對(duì)象的監(jiān)視器為同步對(duì)象。synchronized同時(shí)是可重入的同步仆嗦,即在同一線程中可以在釋放鎖前多次獲取鎖辉阶。將上面例子改進(jìn)下為:
@Slf4j
public class SynchronizedUseDemo {
    public static void main(String[] args) throws Exception{
        SafeJobAdd jobAdd=new SafeJobAdd();
        for(int i=0;i<10;i++){
            new Thread(jobAdd,"thread "+i).start();
        }
        Thread.sleep(3000l);
        log.debug("thread is {} ,total is {}",Thread.currentThread().getName(),jobAdd.getTotal());
    }
}

@Slf4j
@Data
class SafeJobAdd implements Runnable{
    private int total=0;
    @Override
    public void run() {
        // 使用了同步關(guān)鍵字synchronized
        synchronized (this){
            for (int i=0;i<100;i++){
                try {
                    Thread.sleep(2l);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                total++;
            }
            log.debug("thread is {} ,total is {}",Thread.currentThread().getName(),total);
        }

    }
}
打印結(jié)果是:
06:36:35.800 [thread 0] DEBUG com.dz.demo.multiThread.SafeJobAdd - thread is thread 0 ,total is 100
06:36:36.042 [thread 9] DEBUG com.dz.demo.multiThread.SafeJobAdd - thread is thread 9 ,total is 200
06:36:36.290 [thread 8] DEBUG com.dz.demo.multiThread.SafeJobAdd - thread is thread 8 ,total is 300
06:36:36.539 [thread 7] DEBUG com.dz.demo.multiThread.SafeJobAdd - thread is thread 7 ,total is 400
06:36:36.787 [thread 6] DEBUG com.dz.demo.multiThread.SafeJobAdd - thread is thread 6 ,total is 500
06:36:37.040 [thread 5] DEBUG com.dz.demo.multiThread.SafeJobAdd - thread is thread 5 ,total is 600
06:36:37.292 [thread 4] DEBUG com.dz.demo.multiThread.SafeJobAdd - thread is thread 4 ,total is 700
06:36:37.541 [thread 3] DEBUG com.dz.demo.multiThread.SafeJobAdd - thread is thread 3 ,total is 800
06:36:37.793 [thread 2] DEBUG com.dz.demo.multiThread.SafeJobAdd - thread is thread 2 ,total is 900
06:36:38.043 [thread 1] DEBUG com.dz.demo.multiThread.SafeJobAdd - thread is thread 1 ,total is 1000
06:36:38.554 [main] DEBUG com.dz.demo.multiThread.SynchronizedUseDemo - thread is main ,total is 1000

  • 使用jdk的自帶的鎖相關(guān)api;如ReentrantLock(可重入鎖)瘩扼、ReentrantReadWriteLock.ReadLock(可重入的讀寫(xiě)鎖之讀鎖)谆甜、ReentrantReadWriteLock.WriteLock(可重入鎖之寫(xiě)鎖)。這幾個(gè)鎖都是基于jdk的同步框架AbstractQueuedSynchronizer實(shí)現(xiàn)的集绰,具體可查看jdk源碼规辱。也可用AbstractQueuedSynchronizer實(shí)現(xiàn)自定義的同步鎖。在代碼塊中使用lock倒慧,注意在finnaly中釋放鎖按摘。實(shí)例如下:
@Slf4j
public class LockUseDemo {
    public static void main(String[] args) throws Exception{
        LockJobAdd jobAdd=new LockJobAdd();
        for(int i=0;i<10;i++){
            new Thread(jobAdd,"thread "+i).start();
        }
        Thread.sleep(3000l);
        log.debug("thread is {} ,total is {}",Thread.currentThread().getName(),jobAdd.getTotal());
    }
}

@Slf4j
@Data
class LockJobAdd implements Runnable{
    private int total=0;
    private ReentrantLock lock=new ReentrantLock();
    @Override
    public void run() {
            for (int i=0;i<100;i++){
                try {
                    Thread.sleep(2l);
                    lock.lock();
                    total++;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                finally {
                    lock.unlock();
                }
            }
            log.debug("thread is {} ,total is {}",Thread.currentThread().getName(),total);
    }
}
打印如下:
06:54:22.861 [thread 7] DEBUG com.dz.demo.multiThread.LockJobAdd - thread is thread 7 ,total is 994
06:54:22.862 [thread 4] DEBUG com.dz.demo.multiThread.LockJobAdd - thread is thread 4 ,total is 997
06:54:22.861 [thread 9] DEBUG com.dz.demo.multiThread.LockJobAdd - thread is thread 9 ,total is 994
06:54:22.861 [thread 8] DEBUG com.dz.demo.multiThread.LockJobAdd - thread is thread 8 ,total is 994
06:54:22.862 [thread 3] DEBUG com.dz.demo.multiThread.LockJobAdd - thread is thread 3 ,total is 999
06:54:22.861 [thread 5] DEBUG com.dz.demo.multiThread.LockJobAdd - thread is thread 5 ,total is 994
06:54:22.862 [thread 0] DEBUG com.dz.demo.multiThread.LockJobAdd - thread is thread 0 ,total is 997
06:54:22.861 [thread 1] DEBUG com.dz.demo.multiThread.LockJobAdd - thread is thread 1 ,total is 998
06:54:22.861 [thread 2] DEBUG com.dz.demo.multiThread.LockJobAdd - thread is thread 2 ,total is 994
06:54:22.862 [thread 6] DEBUG com.dz.demo.multiThread.LockJobAdd - thread is thread 6 ,total is 1000
06:54:25.633 [main] DEBUG com.dz.demo.multiThread.LockUseDemo - thread is main ,total is 1000
  • 使用volatile關(guān)鍵字修飾共享變量。volatile關(guān)鍵字并沒(méi)有實(shí)現(xiàn)lock或者synchronized關(guān)鍵字的完整的同步作用纫谅,只是保證了可見(jiàn)性與有序性炫贤。在寫(xiě)入是原子性操作或者寫(xiě)入時(shí)線程安全時(shí),用volatile關(guān)鍵字付秕,實(shí)現(xiàn)比synchronized性能更高一點(diǎn)的線程安全兰珍。這個(gè)文章講的比較詳細(xì)volatile,特此引用询吴。正確使用Volatile變量

  • 分布式環(huán)境下的共享資源的安全問(wèn)題不屬于多線程概念內(nèi)的掠河,是多實(shí)例多線程共同使用共享資源引起的,但是也是存在類(lèi)似的問(wèn)題猛计。一般的解決方案唠摹,是用鎖的辦法來(lái)實(shí)現(xiàn),使用數(shù)據(jù)庫(kù)實(shí)現(xiàn)樂(lè)觀鎖比較版本號(hào)奉瘤,或者使用redis 的setnx操作或者使用zookeeper實(shí)現(xiàn)勾拉。具體使用將在后面單獨(dú)講一篇。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末盗温,一起剝皮案震驚了整個(gè)濱河市藕赞,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌卖局,老刑警劉巖斧蜕,帶你破解...
    沈念sama閱讀 222,865評(píng)論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異砚偶,居然都是意外死亡批销,警方通過(guò)查閱死者的電腦和手機(jī)洒闸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,296評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)风钻,“玉大人顷蟀,你說(shuō)我怎么就攤上這事酒请÷饧迹” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 169,631評(píng)論 0 364
  • 文/不壞的土叔 我叫張陵羞反,是天一觀的道長(zhǎng)布朦。 經(jīng)常有香客問(wèn)我,道長(zhǎng)昼窗,這世上最難降的妖魔是什么是趴? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 60,199評(píng)論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮澄惊,結(jié)果婚禮上唆途,老公的妹妹穿的比我還像新娘。我一直安慰自己掸驱,他們只是感情好肛搬,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,196評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著毕贼,像睡著了一般温赔。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上鬼癣,一...
    開(kāi)封第一講書(shū)人閱讀 52,793評(píng)論 1 314
  • 那天陶贼,我揣著相機(jī)與錄音,去河邊找鬼待秃。 笑死拜秧,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的章郁。 我是一名探鬼主播枉氮,決...
    沈念sama閱讀 41,221評(píng)論 3 423
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼驱犹!你這毒婦竟也來(lái)了嘲恍?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 40,174評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤雄驹,失蹤者是張志新(化名)和其女友劉穎佃牛,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體医舆,經(jīng)...
    沈念sama閱讀 46,699評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡俘侠,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,770評(píng)論 3 343
  • 正文 我和宋清朗相戀三年象缀,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片爷速。...
    茶點(diǎn)故事閱讀 40,918評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡央星,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出惫东,到底是詐尸還是另有隱情莉给,我是刑警寧澤,帶...
    沈念sama閱讀 36,573評(píng)論 5 351
  • 正文 年R本政府宣布廉沮,位于F島的核電站颓遏,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏滞时。R本人自食惡果不足惜叁幢,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,255評(píng)論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望坪稽。 院中可真熱鬧曼玩,春花似錦、人聲如沸窒百。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,749評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)贝咙。三九已至样悟,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間庭猩,已是汗流浹背窟她。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,862評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蔼水,地道東北人震糖。 一個(gè)月前我還...
    沈念sama閱讀 49,364評(píng)論 3 379
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像趴腋,于是被迫代替她去往敵國(guó)和親吊说。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,926評(píng)論 2 361