synchronized關(guān)鍵字使用詳解

一、解決什么問(wèn)題

多線程訪問(wèn)同一對(duì)象的實(shí)例變量時(shí)饺蔑,避不開(kāi)線程安全的問(wèn)題锌介。如果處理不當(dāng),很可能會(huì)出現(xiàn)臟讀、死鎖等問(wèn)題孔祸。所以我們應(yīng)該合理地使用synchronized來(lái)保證線程安全隆敢。

什么場(chǎng)景下不用考慮線程安全問(wèn)題?

  • 變量是在方法內(nèi)部聲明的局部變量崔慧;

    public class Calculator {
        public int add(int num){
            // 局部變量沒(méi)有線程安全問(wèn)題
            int result = 0;
            result += num;
            return result;
        }
    }
    
    @Slf4j
    public class MyThread implements Runnable {
        private Calculator calculator;
        private int num;
    
        public MyThread(Calculator calculator, int num) {
            this.calculator = calculator;
            this.num = num;
        }
    
        @Override
        public void run() {
            int result = calculator.add(num);
            log.info("線程{}的計(jì)算結(jié)果為:{}", Thread.currentThread().getName(), result);
        }
    }
    
    @Slf4j
    public class Test001 {
        private static int result = 0;
    
        public static void main(String[] args) throws InterruptedException {
            // 實(shí)例化一個(gè)計(jì)算器對(duì)象
            Calculator calculator = new Calculator();
            // 兩個(gè)線程訪問(wèn)同一個(gè)計(jì)算器對(duì)象
            Thread thread1 = new Thread(new MyThread(calculator,100));
            Thread thread2 = new Thread(new MyThread(calculator,100));
    
            thread1.start();
            thread2.start();
        }
    }
    
  • 多個(gè)線程分別訪問(wèn)不同對(duì)象的實(shí)例變量拂蝎;

    @Slf4j
    public class Test001 {
        private static int result = 0;
    
        public static void main(String[] args) throws InterruptedException {
            // 實(shí)例化兩個(gè)計(jì)算器對(duì)象
            Calculator calculator1 = new Calculator();
            Calculator calculator2 = new Calculator();
            // 兩個(gè)線程分別訪問(wèn)各自的計(jì)算器對(duì)象
            Thread thread1 = new Thread(new MyThread(calculator1,100));
            Thread thread2 = new Thread(new MyThread(calculator2,100));
    
            thread1.start();
            thread2.start();
        }
    }
    

    此時(shí)每個(gè)線程對(duì)應(yīng)一個(gè)計(jì)算器對(duì)象,無(wú)論變量為局部變量還是實(shí)例變量温自,都不存在線程安全的問(wèn)題夹界。

  • 變量為線程獨(dú)享變量的時(shí)候;

    @Slf4j
    public class MyThread implements Runnable {
        // 線程獨(dú)享的變量
        private int count = 200;
    
        @Override
        public void run() {
            count--;
            log.info("當(dāng)前線程名稱(chēng):{},計(jì)數(shù)器為:{}", Thread.currentThread().getName(), count);
        }
    }
    

    或者使用ThreadLocal的時(shí)候,關(guān)于ThreadLocal的使用,參考另外一篇文章编振;

剩下的場(chǎng)景中瓢阴,多線程共享變量就必須要考慮線程安全的問(wèn)題了累贤。

二示损、對(duì)方法的使用

先來(lái)看下一個(gè)線程不安全的例子:

public class Calculator {
    private int result = 0;

    public int add(int num){
        result += num;
        return result;
    }
}
@Slf4j
public class MyThread implements Runnable {
    private Calculator calculator;
    private int num;

    public MyThread(Calculator calculator, int num) {
        this.calculator = calculator;
        this.num = num;
    }

    @Override
    public void run() {
        int result = calculator.add(num);
        log.info("線程{}的計(jì)算結(jié)果為:{}", Thread.currentThread().getName(), result);
    }
}
@Slf4j
public class Test001 {
    public static void main(String[] args) throws InterruptedException {
        Calculator calculator = new Calculator();
        Thread thread1 = new Thread(new MyThread(calculator,100));
        Thread thread2 = new Thread(new MyThread(calculator,100));

        thread1.start();
        thread2.start();
    }
}

此時(shí)thread1和thread2共享了Calculator中的result變量嘉汰,執(zhí)行后可能會(huì)得到如下線程不安全的結(jié)果,注意,這里是可能的結(jié)果村斟,并不是每次運(yùn)行都會(huì)得到這個(gè)結(jié)果逾滥。如果想要模擬線程不安全的結(jié)果,可以使用多線程調(diào)試技術(shù)《IDEA調(diào)試技巧進(jìn)階》

線程Thread-1的計(jì)算結(jié)果為:200
線程Thread-0的計(jì)算結(jié)果為:200

如何解決這個(gè)線程不安全問(wèn)題呢?很簡(jiǎn)單,在Calculator的add方法中加上synchronized關(guān)鍵字即可。

此時(shí)使用多線程調(diào)試技術(shù)可以發(fā)現(xiàn)芙委,某個(gè)線程先進(jìn)入add方法后侧啼,另外一個(gè)線程壓根就沒(méi)有停止在斷點(diǎn)處哪审,運(yùn)行的結(jié)果是線程安全的舌狗。

關(guān)于加在方法上的synchronized需要注意如下事項(xiàng):

  • 鎖定的是對(duì)象實(shí)例恋日,而不是這個(gè)方法筷屡;

    因此扼倘,對(duì)于這個(gè)實(shí)例中其它的synchronized方法其實(shí)也是處于鎖定狀態(tài)的秉剑,其它線程都不能進(jìn)入岗仑,因?yàn)樵搶?duì)象的鎖還未被釋放既鞠;對(duì)于這個(gè)實(shí)例中其它的非synchronized方法,其它線程則是可以進(jìn)入的它碎。

    需要注意的是踊谋,要避免在非synchronized的方法中讀取會(huì)在synchronized方法中修改的變量害驹,這會(huì)造成臟讀,一定要這樣操作的話圣勒,那么請(qǐng)將非synchronized的方法改為synchronized。

  • synchronized鎖重入;

    一個(gè)已經(jīng)獲得對(duì)象鎖的線程夏跷,再次請(qǐng)求獲取該對(duì)象的鎖是沒(méi)問(wèn)題的佣蓉,這就叫做synchronized的鎖重入。具體表現(xiàn)就是基显,一個(gè)線程可以在synchronized的方法中調(diào)用該實(shí)例其它的synchronized方法舅桩,不會(huì)存在等待鎖的情況恢暖。

  • 出現(xiàn)異常時(shí)釋放鎖挨队;

    當(dāng)一個(gè)線程進(jìn)入某個(gè)實(shí)例的synchronized方法執(zhí)行出現(xiàn)異常時(shí)蔬充,是會(huì)自動(dòng)釋放該對(duì)象鎖的;

三如输、對(duì)語(yǔ)句塊的使用

先前synchronized都是在方法級(jí)別上使用的,當(dāng)方法邏輯很簡(jiǎn)單酪惭,耗時(shí)不多時(shí)纺铭,沒(méi)啥問(wèn)題锥累。但是如果方法邏輯是分步驟的姑蓝,且有些步驟并不需要同步執(zhí)行,那么方法級(jí)別的synchronized就顯得不太合適了蜻拨。我們應(yīng)該只鎖定那些需要同步執(zhí)行的代碼塊厘灼。

@Slf4j
public class Calculator {
    private int result = 0;

    public int add(int num) {
        // 不需要同步執(zhí)行
        if (num == 100) {
            log.info("不執(zhí)行計(jì)算,返回0萍悴!");
            return 0;
        } else {
            // 需要同步執(zhí)行
            synchronized (this) {
                result += num;
                return result;
            }
        }
    }
}

如此嗅蔬,對(duì)于執(zhí)行非同步代碼塊的線程盒延,并不受同步代碼塊中鎖的影響票罐,可以提高多線程的執(zhí)行效率。

關(guān)于加在代碼塊上的synchronized需要注意如下事項(xiàng):

  • 使用synchronized(this)玖翅,鎖定的是對(duì)象,所以其它線程都是無(wú)法直接進(jìn)入該對(duì)象的任何synchronized(this)代碼塊和synchronized方法的勘高,需要等待當(dāng)前線程釋放對(duì)象鎖建蹄;
  • 還可以使用synchronized(任意監(jiān)視對(duì)象)來(lái)實(shí)現(xiàn)同步鎖定代碼塊的效果焦人,但是相較于synchronized(this)有一些區(qū)別。
    • 因?yàn)榇藭r(shí)鎖定的不是對(duì)象前塔,而是這個(gè)“任意監(jiān)視對(duì)象”顶燕,多個(gè)線程只有在"任意監(jiān)視對(duì)象"為同一個(gè)對(duì)象的前提下,才可以同步執(zhí)行當(dāng)前對(duì)象中的任何synchronized(任意監(jiān)視對(duì)象)代碼塊憋肖。
    • 如果當(dāng)前對(duì)象中多個(gè)同步代碼塊"任意監(jiān)視對(duì)象"不是同一個(gè),或者synchronized(this)代碼塊和synchronized方法怎炊,那么多個(gè)線程都是不能同步執(zhí)行的。
    • 特別的歹垫,對(duì)于這個(gè)“任意監(jiān)視對(duì)象”中的synchronized(this)代碼塊和synchronized方法是可以同步執(zhí)行的。
  • 避免在非同步代碼塊中讀取會(huì)在同步代碼塊中修改的變量,容易造成臟讀;同樣的纱控,避免在非同一個(gè)監(jiān)視對(duì)象中讀取另外一個(gè)監(jiān)視對(duì)象中會(huì)修改的變量辆毡;

總之,就是注意多個(gè)線程執(zhí)行同步代碼時(shí)甜害,關(guān)注鎖定的是否是同一個(gè)對(duì)象舶掖。

四、對(duì)類(lèi)的使用

在功能和效果上唾那,對(duì)靜態(tài)方法使用synchronized和對(duì)類(lèi)加鎖的代碼塊是一樣的访锻。

@Slf4j
public class Calculator {
    private static int result = 0;

    public static synchronized int add(int num) {
        result += num;
        return result;
    }
}

等同于

@Slf4j
public class Calculator {
    private int result = 0;

    public int add(int num) {
        synchronized (Calculator.class) {
            result += num;
            return result;
        }
    }
}

它們都是把當(dāng)前類(lèi)鎖定了褪尝,對(duì)于該類(lèi)的所有對(duì)象闹获,競(jìng)爭(zhēng)同一個(gè)類(lèi)鎖是同步的。

@Slf4j
public class Test001 {
    public static void main(String[] args) throws InterruptedException {
        // 兩個(gè)對(duì)象競(jìng)爭(zhēng)的是同一個(gè)類(lèi)鎖河哑,而不是各自的對(duì)象鎖
        Calculator calculator1 = new Calculator();
        Calculator calculator2 = new Calculator();
        Thread thread1 = new Thread(new MyThread(calculator1, 100));
        Thread thread2 = new Thread(new MyThread(calculator2,200));

        thread1.start();
        thread2.start();
    }
}

同時(shí)避诽,類(lèi)鎖并不影響對(duì)象鎖的運(yùn)行,它們不是同步的璃谨。

@Slf4j
public class Calculator {
    private int result = 0;

    public int add(int num) {
        // 類(lèi)鎖
        synchronized (Calculator.class) {
            result += num;
            return result;
        }
    }

    public int minus(int num) {
        // 對(duì)象鎖
        synchronized (this) {
            result -= num;
            return result;
        }
    }
}

五沙庐、其它

  • 持有字符串類(lèi)型的鎖容易因?yàn)镾tring常量池緩存導(dǎo)致多線程并發(fā)問(wèn)題。
  • 同步代碼塊獲取的鎖有可能是會(huì)改變的佳吞,多線程等待的只要同一個(gè)對(duì)象拱雏,那么就可以同步,不是同一個(gè)對(duì)象底扳,就不同步铸抑。
  • 在對(duì)象的屬性發(fā)生變化時(shí)并不代表對(duì)象發(fā)生變化,此時(shí)等待該對(duì)象鎖的所有線程是可以同步的衷模。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末鹊汛,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子阱冶,更是在濱河造成了極大的恐慌刁憋,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,907評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件木蹬,死亡現(xiàn)場(chǎng)離奇詭異至耻,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)镊叁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,987評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)有梆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人意系,你說(shuō)我怎么就攤上這事泥耀。” “怎么了蛔添?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,298評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵痰催,是天一觀的道長(zhǎng)兜辞。 經(jīng)常有香客問(wèn)我,道長(zhǎng)夸溶,這世上最難降的妖魔是什么逸吵? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,586評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮缝裁,結(jié)果婚禮上扫皱,老公的妹妹穿的比我還像新娘。我一直安慰自己捷绑,他們只是感情好韩脑,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,633評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著粹污,像睡著了一般段多。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上壮吩,一...
    開(kāi)封第一講書(shū)人閱讀 51,488評(píng)論 1 302
  • 那天进苍,我揣著相機(jī)與錄音,去河邊找鬼鸭叙。 笑死觉啊,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的沈贝。 我是一名探鬼主播杠人,決...
    沈念sama閱讀 40,275評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼缀程!你這毒婦竟也來(lái)了搜吧?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,176評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤杨凑,失蹤者是張志新(化名)和其女友劉穎滤奈,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體撩满,經(jīng)...
    沈念sama閱讀 45,619評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蜒程,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,819評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了伺帘。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片昭躺。...
    茶點(diǎn)故事閱讀 39,932評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖伪嫁,靈堂內(nèi)的尸體忽然破棺而出领炫,到底是詐尸還是另有隱情,我是刑警寧澤张咳,帶...
    沈念sama閱讀 35,655評(píng)論 5 346
  • 正文 年R本政府宣布帝洪,位于F島的核電站似舵,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏葱峡。R本人自食惡果不足惜砚哗,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,265評(píng)論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望砰奕。 院中可真熱鬧蛛芥,春花似錦、人聲如沸军援。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,871評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)盖溺。三九已至漓糙,卻和暖如春铣缠,著一層夾襖步出監(jiān)牢的瞬間烘嘱,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,994評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工蝗蛙, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蝇庭,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,095評(píng)論 3 370
  • 正文 我出身青樓捡硅,卻偏偏與公主長(zhǎng)得像哮内,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子壮韭,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,884評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容