如何用一句話介紹synchronize的內(nèi)涵

內(nèi)涵與表象

關(guān)于synchronize才漆,一個非常通俗易懂牛曹,很容易記住的解釋是:

Java語言的關(guān)鍵字,當(dāng)它用來修飾一個方法或者一個代碼塊的時候醇滥,能夠保證在同一時刻最多只有一個線程執(zhí)行該段代碼黎比。

這個解釋很好,它非常直觀的告訴我們使用synchronize會帶來什么效果腺办。
然而焰手,也正因為如此糟描,這個解釋太過停留在了表面怀喉,就像給一款洗衣機(jī)做廣告,廣告中說這款自動式洗衣機(jī)可以一鍵洗衣一樣船响,如果只是這樣說躬拢,那根本無法展示這臺洗衣機(jī)有什么與眾不同的地方,因為市面上可以一鍵式操作的洗衣機(jī)太多了见间,必須向客戶拋出問題聊闯,這款洗衣機(jī)是如何一鍵式完成整個洗衣流程的、為什么這款洗衣機(jī)洗的比別人干凈米诉,然后貼上各種高科技高逼格的圖片菱蔬、播放各種酷炫的動畫視頻,這樣,客戶才了解了這款洗衣機(jī)的內(nèi)涵拴泌,才有可能對這款洗衣機(jī)動心魏身。

回到synchronize,開頭的解釋告訴我們蚪腐,synchronize可以“保證在同一時刻最多只有一個線程執(zhí)行該段代碼”箭昵,那么,我們就不得不去想:

  • synchronize是如何“保證在同一時刻最多只有一個線程執(zhí)行該段代碼”的回季?
  • “保證在同一時刻最多只有一個線程執(zhí)行該段代碼”家制,這又會帶來什么意義?

太長不看版:

Java中的synchronize泡一,通過使用內(nèi)置鎖颤殴,來實現(xiàn)對變量的同步操作,進(jìn)而實現(xiàn)了對變量操作的原子性和其他線程對變量的可見性瘾杭,從而確保了并發(fā)情況下的線程安全诅病。

基本用法

首先還是要刷一把代碼,我會用一個簡單的例子演示如何使用synchronize粥烁,并對其進(jìn)行測試贤笆。如果你已經(jīng)了解了synchronize的用法,可以快速略讀這一小節(jié)讨阻。

假設(shè)我們要給一個處理器加入計數(shù)器芥永,每次調(diào)用時給計數(shù)器加一,為方便擴(kuò)展钝吮,我們定義了如下接口(本文的示例代碼埋涧,可到Github下載):
CountingProcessor:

public interface CountingProcessor {
    void process();
    long getCount();
}

不使用同步機(jī)制,我們寫出了第一個版本:
UnThreadSafeCountingProcessor:

public class UnThreadSafeCountingProcessor implements CountingProcessor {

    private long count = 0;

    public void process() {
        doProcess();
        count ++;
    }

    public long getCount() {
        return count;
    }

    private void doProcess() {
    }
}

這個版本自然是線程不安全的奇瘦,原因就是之前在《如何寫出線程不安全的代碼》里提到的棘催,count++是一個“讀取-修改-寫入”三個動作的操作序列。要想驗證這個類是線程不安全的耳标,非常簡單醇坝,寫個測試類測一下就知道了(用例寫的比較粗糙,后面再來談?wù)勅绾螠y試并發(fā)程序):
SynchronizeProcessTest:

public class SynchronizeProcessTest {

    public static final int LOOP_TIME = 1000 * 10000;

    @Test
    public void test_UnThreadSafeCountingProcessor() {
        CountingProcessor countingProcessor = new UnThreadSafeCountingProcessor();
        runTask(countingProcessor);
    }

    private void runTask(CountingProcessor processor) {
        Thread thread1 = new Thread(new ProcessTask(processor, LOOP_TIME), "thread-1");
        Thread thread2 = new Thread(new ProcessTask(processor, LOOP_TIME), "thread-2");
        thread1.start();
        thread2.start();
        // wait unit all the threads have finished
        while(thread1.isAlive() || thread2.isAlive()) {}
    }
}

其中的ProcessTask如下所示:

public class ProcessTask implements Runnable {

    private static Logger logger = LoggerFactory.getLogger(ProcessTask.class);

    private CountingProcessor countingProcessor;
    private long loopTime;

    public ProcessTask(CountingProcessor countingProcessor, long loopTime) {
        this.countingProcessor = countingProcessor;
        this.loopTime = loopTime;
    }

    public void run() {
        int i = 0;
        while (i < loopTime) {
            countingProcessor.process();
            i ++;
        }
        logger.info("Finally, the count is {}", countingProcessor.getCount());
    }
}

在ProcessTask里次坡,我們不斷循環(huán)執(zhí)行process()方法呼猪,讓計數(shù)器不斷遞增。然后在測試類中砸琅,我們創(chuàng)建了兩個線程宋距,分別指定ProcessTask的循環(huán)次數(shù)為一千萬次,最后查看日志打印症脂,如果程序時線程安全的谚赎,那么當(dāng)最后一個線程結(jié)束時淫僻,打印的計數(shù)器應(yīng)該是兩千萬,接著我們運行測試用例:

從運行結(jié)果可以看出來壶唤,在經(jīng)歷了兩千萬次調(diào)用后嘁傀,count的值是10469363,少計算了快一半视粮。

要讓我們這個計數(shù)器變得線程安全细办,有很多種方法,這里只介紹使用synchronize的兩種方法蕾殴,第一種笑撞,我們可以給整個函數(shù)加上synchronize修飾符:
SynchronizeMethodCountingProcessor:

    ...  
    public synchronized void process() {
        doProcess();
        count++;
    }
    ...  

這樣子固然可以解決問題,但是我們其實沒必要對整個函數(shù)都進(jìn)行同步钓觉,這樣會影響程序的吞吐量茴肥,我們只需要在計數(shù)器加一的過程進(jìn)行同步就好了,由此我們寫出第二種synchronize的版本荡灾,也就是synchronize代碼塊:
SynchronizeBlockCountingProcessor:

    ...
    public void process() {
        doProcess();
        synchronized (this) {
            count ++;
        }
    }
    ...

同樣瓤狐,我們給這兩個類增加兩個測試用例,借助前面良好的程序設(shè)計批幌,我們這兩個用例得以寫的非常簡潔:
SynchronizeProcessTest:

    ...

    @Test
    public void test_SynchronizeMethodCountingProcessor() {
        CountingProcessor countingProcessor = new SynchronizeMethodCountingProcessor();
        runTask(countingProcessor);
    }

    @Test
    public void test_SynchronizeBlockCountingProcessor() {
        CountingProcessor countingProcessor = new SynchronizeBlockCountingProcessor();
        runTask(countingProcessor);
    }

    ...

執(zhí)行用例:

可以看到础锐,使用synchronize改造后的版本,最后count都等于兩千萬荧缘,說明它們是線程安全的皆警。

原子性

上面的例子,展示了synchronize的一個作用:確保了操作的原子性截粗。
原先count++是三個動作信姓,其他線程可以在這三個操作之間對count變量進(jìn)行修改,而在使用了synchronize之后绸罗,這三個動作就變成一個不可拆分意推、一氣呵成的動作,不必?fù)?dān)心在這個操作的過程中會有其他線程進(jìn)行干擾珊蟀,這就是原子性菊值。
原子操作是線程安全的,這其實也是我們經(jīng)常使用synchronize來實現(xiàn)線程安全的原因系洛。

可見性

上面我們提到了synchronize的第一個作用俊性,確保原子性略步,這其實是從使用synchronize的線程的角度來講的描扯,而如果我們從其他線程的角度來看,那么synchronize則是實現(xiàn)了可見性趟薄。
可見性的意思是變量的修改可以被其他線程觀察到绽诚,在上面計數(shù)器的例子中,由于一次只有一個線程可以執(zhí)行count++,搶不到鎖的線程恩够,必須等搶到鎖的線程更新完count之后卒落,才可以去執(zhí)行count++,而這個時候蜂桶,count也已經(jīng)完成了更新儡毕,新的鎖持有者,可以看到更新后的count扑媚,而不至于拿著舊的count值去進(jìn)行計算腰湾,這就是可見性。

提起可見性疆股,我們就不得不提到volatile關(guān)鍵字费坊,volatile實現(xiàn)了比synchronize更輕量級的同步機(jī)制,或者說旬痹,加鎖機(jī)制既確保了可見性附井,有確保了原子性,而volatile只能保證可見性两残。

Locking can guarantee both visibility and atomicity; volatile variables can only guarantee visibility. —— 《Java并發(fā)編程實踐》

關(guān)于volatile關(guān)鍵字永毅,我們后面再單獨研究,這里就不深入探討了人弓。

下面卷雕,讓我們來探討開頭提的問題,synchronize是如何“保證在同一時刻最多只有一個線程執(zhí)行該段代碼”的票从?

內(nèi)置鎖

關(guān)于synchronize漫雕,我們經(jīng)常使用的隱喻就是鎖,首先進(jìn)入的線程峰鄙,拿到了鎖的唯一一把鑰匙浸间,至于其他線程,就只能阻塞(Blocked)吟榴;等到線程走出synchronize之后魁蒜,會把鎖釋放掉,也就是把鑰匙扔出去吩翻,下一個拿到鑰匙的線程兜看,就可以結(jié)束阻塞狀態(tài),繼續(xù)運行狭瞎。
但是鎖從哪來呢细移?隨隨便便抓起一個東西就可以作為鎖么?
還真是這樣熊锭,Java中每一個對象都有一個與之關(guān)聯(lián)的鎖弧轧,稱為內(nèi)置鎖

Every object has an intrinsic lock associated with it. —— The Java? Tutorials

當(dāng)我們使用synchronize修飾非靜態(tài)方法時雪侥,用的是調(diào)用該方法的實例的內(nèi)置鎖,也就是this;
當(dāng)我們使用synchronize修飾靜態(tài)方法時精绎,用的是調(diào)用該方法的所在的類對象的內(nèi)置鎖;
更多時候速缨,我們使用的是synchronize代碼塊,我們經(jīng)常用的是synchronize(this)代乃,也就是把對象實例作為鎖旬牲。

同一時間進(jìn)入同一個鎖的線程只有一個,如果我們希望有多個線程可以同時進(jìn)入多個加了鎖的方法搁吓,那只靠一個this鎖肯定是不夠的引谜,那怎么辦?一點都不擔(dān)心擎浴,還記得上面說的嗎员咽,Java中每個對象都是鎖,想用的時候new一個Object就好了:

public class MsLunch {
    private long c1 = 0;
    private long c2 = 0;
    private Object lock1 = new Object();
    private Object lock2 = new Object();

    public void inc1() {
        synchronized(lock1) {
            c1++;
        }
    }

    public void inc2() {
        synchronized(lock2) {
            c2++;
        }
    }
}

Java中只能使用對象作為鎖嗎贮预,當(dāng)然不是的贝室,我們還可以自己打造一把鎖,也就是顯示鎖仿吞,比如這樣:

      Lock lock = ...;
      if (lock.tryLock()) {
          try {
              // manipulate protected state
          } finally {
              lock.unlock();
          }
      } else {
          // perform alternative actions
      }

至于顯示鎖具體怎么用和它的原理滑频,以及Java中其他奇奇怪怪的鎖,我們也不在這里細(xì)究唤冈,后面再和大家一塊探討峡迷。

重入

最后再來看看這個代碼有什么問題:

public class Widget {
    public synchronized void doSomething() {
        ...
    }
}

public class LoggingWidget extends Widget {
    public synchronized void doSomething() {
        System.out.println(toString() + ": calling doSomething");
        super.doSomething();
    }
}

分析:
前面提到,synchronized修飾非靜態(tài)方法時你虹,用的是調(diào)用該方法的對象實例作為鎖绘搞,所以上面的代碼中,調(diào)用LoggingWidget的doSomething時傅物,拿到了實例的鎖的鑰匙夯辖,接著再去調(diào)用父類的doSomething方法吞歼,父類的方法同樣被synchronized修飾淆储,此時鑰匙已經(jīng)被拿走了而且還沒釋放严肪,所以阻塞回溺,而阻塞導(dǎo)致LoggingWidget的doSomething方法無法執(zhí)行完成,因而鎖一直不會被釋放抡谐,所以铁追,死鎖了姻成?也祠?昙楚?

當(dāng)然不是,上面的理解錯在了弄錯了鎖的持有者齿坷,鎖的持有者是“線程”桂肌,而不是“調(diào)用”,線程在進(jìn)入LoggingWidget的doSomething方法時永淌,已經(jīng)拿到this對象內(nèi)置鎖的鑰匙了崎场,下次再碰到同一把鎖,自然是用同一把鑰匙去打開它就可以了遂蛀。這就是內(nèi)置鎖的可重入性(Reentrancy)谭跨。

既然鎖是可重入的,那么也就意味著李滴,JVM不能簡單的在線程執(zhí)行完synchronized方法或者synchronized代碼塊時就釋放鎖螃宙,因為線程可能同時“重入”了很多道鎖,事實上所坯,JVM是借助鎖上的計數(shù)器來判斷是否可以釋放鎖的:

Reentrancy is implemented by associating with each lock an acquisition count and an owning thread. When the count is zero, the lock is considered unheld. When a thread acquires a previously unheld lock, the JVM records the owner and sets the acquisition count to one. If that same thread acquires the lock again, the count is incremented, and when the owning thread exits the synchronized block, the count is decremented. When the count reaches zero, the lock is released. —— 《Java并發(fā)編程實踐》

如果將含有synchronized代碼塊的代碼編譯出來的class文件谆扎,使用javap進(jìn)行反匯編,你可以看到會有兩條指令:
monitorenter和monitorexit芹助,這兩條指令做的也就是上面說的那些事堂湖,有興趣的同學(xué)可以研究一下。

總結(jié)

這篇文章主要對Java中的synchronized做了一些研究状土,總結(jié)一下:

  1. Java中每個對象都有一個內(nèi)置鎖无蜂。
  2. 與內(nèi)置鎖相對的是顯示鎖,使用顯示鎖需要手動創(chuàng)建Lock對象蒙谓,而內(nèi)置鎖則是所有對象自帶的斥季。
  3. synchronized使用對象自帶的內(nèi)置鎖來進(jìn)行加鎖,從而保證在同一時刻最多只有一個線程執(zhí)行代碼累驮。
  4. 所有的加鎖行為酣倾,都可以帶來兩個保障——原子性可見性。其中谤专,原子性是相對鎖所在的線程的角度而言灶挟,而可見性則是相對其他線程而言。
  5. 鎖的持有者是“線程”毒租,而不是“調(diào)用”稚铣,這也是鎖的為什么是可重入的原因。

如何向一個新手介紹synchronized的表象墅垮?

Java語言的關(guān)鍵字惕医,當(dāng)它用來修飾一個方法或者一個代碼塊的時候,能夠保證在同一時刻最多只有一個線程執(zhí)行該段代碼算色。

如何在一個老司機(jī)面前裝逼格抬伺?

Java中的synchronize,通過使用內(nèi)置鎖灾梦,來實現(xiàn)對變量的同步操作峡钓,進(jìn)而實現(xiàn)了對變量操作的原子性和其他線程對變量的可見性妓笙,從而確保了并發(fā)情況下的線程安全。

后記

難道synchronize就是這樣了能岩?自然不是寞宫,只要你繼續(xù)研究,肯定還會提出很多問題拉鹃。我先提一個:

  • 搶不到鎖而進(jìn)入阻塞狀態(tài)的線程辈赋,怎么知道鎖什么時候會被釋放?

要想弄清楚synchronize的原理膏燕,最直截了當(dāng)?shù)姆绞阶匀皇强丛创a钥屈,當(dāng)然這也是難度最大的,畢竟JVM源碼都是C語言坝辫;另一種方法就是不斷向自己提問篷就,然后不斷搜索資料,解答自己提出的問題近忙。

看似簡單的知識腻脏,深究起來,往往沒那么簡單银锻。
只有學(xué)會提問永品,才能透過表象,看清原理击纬;理解了原理鼎姐,遇到Bug才能不慌。

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末更振,一起剝皮案震驚了整個濱河市炕桨,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌肯腕,老刑警劉巖献宫,帶你破解...
    沈念sama閱讀 222,183評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異实撒,居然都是意外死亡姊途,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評論 3 399
  • 文/潘曉璐 我一進(jìn)店門知态,熙熙樓的掌柜王于貴愁眉苦臉地迎上來捷兰,“玉大人,你說我怎么就攤上這事负敏」泵” “怎么了?”我有些...
    開封第一講書人閱讀 168,766評論 0 361
  • 文/不壞的土叔 我叫張陵,是天一觀的道長顶考。 經(jīng)常有香客問我赁还,道長,這世上最難降的妖魔是什么驹沿? 我笑而不...
    開封第一講書人閱讀 59,854評論 1 299
  • 正文 為了忘掉前任艘策,我火速辦了婚禮,結(jié)果婚禮上甚负,老公的妹妹穿的比我還像新娘柬焕。我一直安慰自己审残,他們只是感情好梭域,可當(dāng)我...
    茶點故事閱讀 68,871評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著搅轿,像睡著了一般病涨。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上璧坟,一...
    開封第一講書人閱讀 52,457評論 1 311
  • 那天既穆,我揣著相機(jī)與錄音,去河邊找鬼雀鹃。 笑死幻工,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的黎茎。 我是一名探鬼主播囊颅,決...
    沈念sama閱讀 40,999評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼傅瞻!你這毒婦竟也來了踢代?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,914評論 0 277
  • 序言:老撾萬榮一對情侶失蹤嗅骄,失蹤者是張志新(化名)和其女友劉穎胳挎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體溺森,經(jīng)...
    沈念sama閱讀 46,465評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡慕爬,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,543評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了屏积。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片澡罚。...
    茶點故事閱讀 40,675評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖肾请,靈堂內(nèi)的尸體忽然破棺而出留搔,到底是詐尸還是另有隱情,我是刑警寧澤铛铁,帶...
    沈念sama閱讀 36,354評論 5 351
  • 正文 年R本政府宣布隔显,位于F島的核電站却妨,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏括眠。R本人自食惡果不足惜彪标,卻給世界環(huán)境...
    茶點故事閱讀 42,029評論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望掷豺。 院中可真熱鬧捞烟,春花似錦、人聲如沸当船。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽德频。三九已至苍息,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間壹置,已是汗流浹背竞思。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留钞护,地道東北人盖喷。 一個月前我還...
    沈念sama閱讀 49,091評論 3 378
  • 正文 我出身青樓,卻偏偏與公主長得像难咕,于是被迫代替她去往敵國和親课梳。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,685評論 2 360

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