Java線程-synchronized關(guān)鍵字學(xué)習(xí)(三)

1. 線程安全問題

??在進(jìn)行多線程編程的時(shí)候,有可能會出現(xiàn)多個線程同時(shí)訪問同一資源的情況,這種資源可以是變量借嗽,對象,文件转培,數(shù)據(jù)庫表等恶导,這時(shí)候就有可能出現(xiàn)最終訪問結(jié)果不一致的情況。來舉一個最簡單的例子浸须,下單與庫存的問題:

  • 下單的時(shí)候惨寿,先獲取剩余庫存邦泄;
  • 如果還有庫存,下單成功裂垦,庫存減1顺囊,如果沒有庫存,下單失斀堵!特碳;

如果線程thread-1和線程thread-2,同一時(shí)刻晕换,都讀取到庫存還剩1午乓,然后兩個線程都執(zhí)行下單成功,這時(shí)就會出現(xiàn)庫存超賣的情況闸准。

這其實(shí)就是一種線程安全問題硅瞧,也就是多個線程訪問同一資源時(shí),會導(dǎo)致程序的運(yùn)行結(jié)果并不是我們期望的結(jié)果恕汇。而這里的資源被稱為臨界資源或共享資源,臨界資源可以時(shí)一個對象或辖,對象中的屬性瘾英,一個文件,一個數(shù)據(jù)庫等颂暇,不過方法內(nèi)部的局部變量并不是臨界資源缺谴,因?yàn)榉椒ㄊ窃跅I蠄?zhí)行的,而Java棧是線程私有的耳鸯。

2. synchronized同步代碼塊

??基本上所有的并發(fā)模式在解決線程安全問題時(shí)湿蛔,都是采用的序列化訪問臨界資源的方案,也就是同一時(shí)刻县爬,只能有一個線程訪問臨界資源阳啥,也稱為同步互斥訪問。通常實(shí)現(xiàn)就是對臨界資源加一個鎖财喳,當(dāng)訪問完臨界資源后釋放鎖察迟,讓其他線程訪問,而synchronized關(guān)鍵字就是其中的一種實(shí)現(xiàn)方式耳高。

synchronized扎瓶,同步代碼塊,是Java內(nèi)置的一種鎖機(jī)制泌枪,其中包含了兩部分概荷,一部分是鎖的對象引用,另一部分是鎖保護(hù)的代碼塊碌燕。其中該同步代碼塊的鎖就是方法調(diào)用所在的對象误证,而靜態(tài)的synchronized方法的鎖是class對象继薛。

首先,我們要知道每個對象都有一個內(nèi)部鎖雷厂,這些鎖被稱為內(nèi)置鎖(Intrinsic Lock)或者監(jiān)視鎖(Monitor Lock)惋增,線程在進(jìn)入同步代碼塊之前會自動獲得鎖,并且在退出同步代碼塊時(shí)自動釋放鎖改鲫。并且該鎖有一個內(nèi)部條件诈皿,由鎖來管理那些試圖進(jìn)入synchronized方法的線程,由條件來管理那些調(diào)用wait的線程像棘。所以在多線程中要訪問某個對象前稽亏,必須要獲取了該對象的鎖才能訪問。

Java中缕题,synchronized關(guān)鍵字用于同步代碼塊(變量或者類名)和方法(實(shí)例方法或靜態(tài)方法)截歉,當(dāng)某個線程調(diào)用synchronized所修飾的方法或代碼塊時(shí),這個線程便獲得了該對象的鎖烟零,其他線程只能處于等待或阻塞中瘪松,等該線程執(zhí)行代碼塊完成釋放鎖后才能執(zhí)行。

2.1 synchronized修飾方法

我們先來看一個簡單的例子:

public class ThreadTest {
    public static void main(String[] args) {
        final Test test = new Test();
        new Thread(() -> test.test(Thread.currentThread())).start();
        new Thread(() -> test.test(Thread.currentThread())).start();
    }
}

class Test {
    void test(Thread thread) {
        for (int i = 0; i < 10; i++) {
            System.out.println(thread.getName() + ":" + i);
        }
    }
}

首先锨阿,我們不使用synchronized關(guān)鍵字宵睦,查看下打印結(jié)果(截取部分):

Thread-0:0
Thread-0:1
Thread-0:2
Thread-0:3
Thread-1:0
Thread-1:1
Thread-1:2
Thread-0:4
Thread-1:3
Thread-0:5

可以看到,兩個線程在同時(shí)執(zhí)行test方法墅诡,然后我們給test方法添加synchronized參數(shù)壳嚎,再次查看下結(jié)果:

Thread-0:0
Thread-0:1
Thread-0:2
Thread-0:3
Thread-0:4
Thread-0:5
Thread-0:6
Thread-0:7
Thread-0:8
Thread-0:9
Thread-1:0
Thread-1:1

可以看到,Thread-1是等Thread-0插入完成之后才進(jìn)行的末早,它們之間是一種順序執(zhí)行的關(guān)系烟馅。
不過可能需要注意下:

當(dāng)一個線程正在訪問一個對象的synchronized方法,那么其他線程不能訪問該對象的其他synchronized方法然磷,但是可以訪問該對象的非synchronized方法郑趁。這個原因很簡單,因?yàn)橐粋€對象只有一把鎖姿搜,當(dāng)一個線程獲取了該對象的鎖之后穿撮,其他線程無法獲取該對象的鎖,所以無法訪問該對象的其他synchronized方法痪欲,而訪問非synchronized方法是不需要獲取對象鎖的悦穿。

2.2 synchronized代碼塊

synchronized代碼塊格式如下:

synchronized (object) {
    
}

當(dāng)某個線程執(zhí)行這段代碼塊時(shí),該線程會獲取對象object的鎖业踢,從而使得其他線程無法同時(shí)訪問該代碼塊栗柒。而object可以是this,代表調(diào)用這個方法的對象的鎖,也可以是類中的一個屬性瞬沦,代表獲取該屬性的鎖太伊,而如果沒有明確的對象作為鎖,也可以創(chuàng)建一個特殊的變量來充當(dāng)鎖逛钻。而針對上面例子中的test方法僚焦,可以修改為:

void test(Thread thread) {
    synchronized (this) {
        for (int i = 0; i < 10; i++) {
            System.out.println(thread.getName() + ":" + i);
        }
    }
}

synchronized代碼塊可以實(shí)現(xiàn)只對需要同步的地方進(jìn)行同步,而不用像synchronized方法曙痘,會對整個方法進(jìn)行同步芳悲。

2.3 synchronized靜態(tài)方法

先說一下,除了每個對象有一個對象鎖之外边坤,每個類還有一個類對象的內(nèi)部鎖名扛,也可以稱為類鎖,用于對static方法的線程同步控制茧痒。那么肮韧,也就是說:

如果一個線程執(zhí)行一個對象的synchronized修飾的instance方法,而另一個線程執(zhí)行該對象所屬類的synchronized的static方法旺订,這時(shí)候不會發(fā)生互斥現(xiàn)象弄企,因?yàn)樗鼈兊逆i類型都不一樣,一個是對象鎖区拳,一個是類鎖拘领,所以不存在互斥線程。

2.4 synchronized類

synchronized鎖整個類的形式如下:

synchronized (MyClass.class) {
    
}

這種情況下劳闹,這個類對應(yīng)的class對象就會被鎖住。因?yàn)閟ynchronized鎖的是同一個對象的同步代碼塊洽瞬,而如果我們想某段代碼在多線程且多個對象的訪問下也線程同步本涕,我們就可以通過這種方式。當(dāng)然還有一種方式伙窃,就是在synchronized的括號中定義一個固定對象菩颖。

3. synchronized字節(jié)碼
3.1 反編譯synchronized同步代碼塊

??為了更深入的理解synchronized,我們使用javap來反編譯一下synchronized代碼塊为障,來了解一下在字節(jié)碼層面的執(zhí)行過程晦闰,在開發(fā)工具IDEA中配置External Tools即可,比如對如下代碼塊進(jìn)行反編譯:

public void test(Thread thread) {
    synchronized (this) {
        System.out.println(thread.getName() + ":");
    }
}
public void test(java.lang.Thread);
    descriptor: (Ljava/lang/Thread;)V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=4, args_size=2
         0: aload_0
         1: dup
         2: astore_2
         3: monitorenter
         4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         7: new           #3                  // class java/lang/StringBuilder
        10: dup
        ... 省略
        32: aload_2
        33: monitorexit
        34: goto          42
        37: astore_3
        38: aload_2
        39: monitorexit
        40: aload_3

synchronized是通過monitorentermonitorexit這兩條指令實(shí)現(xiàn)了鎖的獲取和釋放過程鳍怨。我們來看下JVM規(guī)范中這兩條指令的描述呻右,先看monitorenter

Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:

  • If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
  • If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
  • If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.

這段話的大概意思是:每個對象有一個監(jiān)視器鎖(monitor),當(dāng)monitor被占用時(shí)就會處于鎖定狀態(tài)鞋喇,線程執(zhí)行monitorenter指令時(shí)嘗試獲取monitor的所有權(quán)声滥,過程如下:

  • 如果monitor的進(jìn)入數(shù)為0,則該線程進(jìn)入monitor侦香,然后將進(jìn)入數(shù)設(shè)置為1落塑,該線程即為monitor的所有者纽疟。
  • 如果線程已經(jīng)占有該monitor,只是重新進(jìn)入憾赁,則進(jìn)入monitor的進(jìn)入數(shù)加1.
  • 如果其他線程已經(jīng)占用了monitor污朽,則該線程進(jìn)入阻塞狀態(tài),直到monitor的進(jìn)入數(shù)為0龙考,再重新嘗試獲取monitor的所有權(quán)蟆肆。

再來看一下monitorexit指令:

The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.

JVM規(guī)范地址:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorexit

這段話的大概意思為:

  • 執(zhí)行monitorexit的線程必須是objectref所對應(yīng)的monitor的所有者。
  • 指令執(zhí)行時(shí)洲愤,monitor的進(jìn)入數(shù)減1颓芭,如果減1后進(jìn)入數(shù)為0,那線程退出monitor柬赐,不再是這個monitor的所有者亡问。其他被這個monitor阻塞的線程可以嘗試去獲取這個 monitor 的所有權(quán)。

??其實(shí)從字節(jié)碼的角度我們可以看出肛宋,Synchronized代碼塊的底層是通過一個monitor的對象來完成的州藕,分別通過monitorentermonitorexit 指令來指向同步代碼塊的開始和結(jié)束位置。值得注意的是編譯器將會確保無論方法通過何種方式完成酝陈,方法中的每條monitorenter指令都將對應(yīng)于一條monitorexit指令床玻。并且編譯器會自動產(chǎn)生一個異常處理器來處理所有的異常,它的目的就是用來執(zhí)行 monitorexit 指令沉帮。所以可以看到锈死,字節(jié)碼中多了一個monitorexit指令,它就是異常結(jié)束時(shí)被執(zhí)行的釋放monitor 的指令穆壕。

3.2 反編譯synchronized方法

??synchronized方法與synchronized代碼塊有些不同待牵,方法的同步是一種隱式的同步,即無需通過字節(jié)碼指令來控制的喇勋。JVM可以從方法常量池中的方法表結(jié)構(gòu)(method_info Structure) 中的 ACC_SYNCHRONIZED 訪問標(biāo)志區(qū)分一個方法是否同步方法缨该。
??當(dāng)方法調(diào)用時(shí),調(diào)用指令將會 檢查方法的 ACC_SYNCHRONIZED 訪問標(biāo)志是否被設(shè)置川背,如果設(shè)置了贰拿,執(zhí)行線程將先持有monitor對象,然后執(zhí)行方法熄云,執(zhí)行完成釋放monitor對象膨更。同樣,在方法執(zhí)行期間缴允,執(zhí)行線程持有了monitor询一,其他任何線程都無法再獲得同一個monitor。

我們同樣來看一下反編譯之后的代碼:

public synchronized void test(Thread thread) {
    System.out.println(thread.getName() + ":");
}
public synchronized void test(java.lang.Thread);
    descriptor: (Ljava/lang/Thread;)V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=2, args_size=2
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: new           #3                  // class java/lang/StringBuilder
         6: dup
         7: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
        10: aload_1
        11: invokevirtual #5                  // Method java/lang/Thread.getName:()Ljava/lang/String;
        14: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        17: ldc           #7                  // String :
        19: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        22: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        25: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        28: return

其中,flags的ACC_PUBLIC表示方法訪問類型是public健蕊, ACC_SYNCHRONIZED表示該方法是同步方法菱阵。

  1. 從字節(jié)碼層面可以看出,同步方法并沒有同步代碼塊的monitorenter指令和monitorexit指令缩功,取得代之的確實(shí)是ACC_SYNCHRONIZED標(biāo)識晴及,JVM通過該訪問標(biāo)志來辨別一個方法是否聲明為同步方法,從而執(zhí)行相應(yīng)的同步調(diào)用嫡锌。
  1. 再多說下虑稼,在Java早期版本中,synchronized屬于重量級鎖势木,效率低下蛛倦,因?yàn)楸O(jiān)視器鎖(monitor)是依賴于底層的操作系統(tǒng)的Mutex Lock來實(shí)現(xiàn)的,而操作系統(tǒng)實(shí)現(xiàn)線程之間的切換是需要從用戶態(tài)轉(zhuǎn)換到核心態(tài)啦桌,這個狀態(tài)之間的轉(zhuǎn)換需要相對比較長的時(shí)間溯壶,時(shí)間成本相對較高,這也是為什么早期的synchronized效率低的原因甫男。慶幸的是在Java 6之后Java官方對從JVM層面對synchronized較大優(yōu)化且改,所以現(xiàn)在的synchronized鎖效率也優(yōu)化得很不錯了,Java 6之后板驳,為了減少獲得鎖和釋放鎖所帶來的性能消耗又跛,引入了輕量級鎖和偏向鎖。
4. 注意事項(xiàng)及總結(jié)

內(nèi)部鎖有一些局限性:

  • 無法中斷一個正在試圖獲得鎖的線程若治;
  • 獲取鎖時(shí)無法設(shè)置超時(shí)時(shí)間慨蓝;
  • 每個鎖只有一個單一的條件,這對于復(fù)雜的場景端幼,可能不夠礼烈;

最后,我們來總結(jié)下:

  1. Java的內(nèi)置鎖其實(shí)就是一種互斥鎖静暂,這意味著最多只有一個線程能持有這種鎖济丘,因此谱秽,由這個鎖保護(hù)的同步代碼塊會以原子方式執(zhí)行洽蛀,多個線程在執(zhí)行該代碼塊時(shí)也不會相互干擾。
  2. synchronized只能防止多個線程訪問同一個對象的同步代碼塊疟赊,如果是多個對象的話郊供,那其實(shí)synchronized就沒什么作用。還有一點(diǎn)近哟,synchronized鎖的是對象驮审,而不是代碼塊,這點(diǎn)要注意下。
  3. 對于synchronized方法或者synchronized代碼塊疯淫,當(dāng)出現(xiàn)異常時(shí)地来,JVM會自動釋放當(dāng)前線程占用的鎖,異常也只會在當(dāng)前線程拋出熙掺,不會影響到其他線程未斑,因此不會由于異常導(dǎo)致出現(xiàn)死鎖現(xiàn)象。
  4. 我們在用synchronized的時(shí)候币绩,要注意減小鎖的粒度蜡秽,也就是能減少同步代碼塊的范圍就盡量減小,能在代碼塊加同步就不要在整個方法上加同步缆镣。

本文參考自:
海子-Java并發(fā)編程:synchronized
Java并發(fā)編程:Synchronized及其實(shí)現(xiàn)原理
《Java并發(fā)編程實(shí)戰(zhàn)》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末芽突,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子董瞻,更是在濱河造成了極大的恐慌寞蚌,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,948評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件力细,死亡現(xiàn)場離奇詭異睬澡,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)眠蚂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,371評論 3 385
  • 文/潘曉璐 我一進(jìn)店門煞聪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人逝慧,你說我怎么就攤上這事昔脯。” “怎么了笛臣?”我有些...
    開封第一講書人閱讀 157,490評論 0 348
  • 文/不壞的土叔 我叫張陵云稚,是天一觀的道長。 經(jīng)常有香客問我沈堡,道長静陈,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,521評論 1 284
  • 正文 為了忘掉前任诞丽,我火速辦了婚禮鲸拥,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘僧免。我一直安慰自己刑赶,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,627評論 6 386
  • 文/花漫 我一把揭開白布懂衩。 她就那樣靜靜地躺著撞叨,像睡著了一般金踪。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上牵敷,一...
    開封第一講書人閱讀 49,842評論 1 290
  • 那天胡岔,我揣著相機(jī)與錄音,去河邊找鬼枷餐。 笑死姐军,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的尖淘。 我是一名探鬼主播奕锌,決...
    沈念sama閱讀 38,997評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼村生!你這毒婦竟也來了惊暴?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,741評論 0 268
  • 序言:老撾萬榮一對情侶失蹤趁桃,失蹤者是張志新(化名)和其女友劉穎辽话,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體卫病,經(jīng)...
    沈念sama閱讀 44,203評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡油啤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,534評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蟀苛。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片益咬。...
    茶點(diǎn)故事閱讀 38,673評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖帜平,靈堂內(nèi)的尸體忽然破棺而出幽告,到底是詐尸還是另有隱情,我是刑警寧澤裆甩,帶...
    沈念sama閱讀 34,339評論 4 330
  • 正文 年R本政府宣布冗锁,位于F島的核電站,受9級特大地震影響嗤栓,放射性物質(zhì)發(fā)生泄漏冻河。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,955評論 3 313
  • 文/蒙蒙 一茉帅、第九天 我趴在偏房一處隱蔽的房頂上張望叨叙。 院中可真熱鬧,春花似錦担敌、人聲如沸摔敛。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,770評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽马昙。三九已至,卻和暖如春刹悴,著一層夾襖步出監(jiān)牢的瞬間行楞,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,000評論 1 266
  • 我被黑心中介騙來泰國打工土匀, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留子房,地道東北人。 一個月前我還...
    沈念sama閱讀 46,394評論 2 360
  • 正文 我出身青樓就轧,卻偏偏與公主長得像证杭,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子妒御,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,562評論 2 349

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

  • 進(jìn)程和線程 進(jìn)程 所有運(yùn)行中的任務(wù)通常對應(yīng)一個進(jìn)程,當(dāng)一個程序進(jìn)入內(nèi)存運(yùn)行時(shí),即變成一個進(jìn)程.進(jìn)程是處于運(yùn)行過程中...
    小徐andorid閱讀 2,799評論 3 53
  • 1.ios高性能編程 (1).內(nèi)層 最小的內(nèi)層平均值和峰值(2).耗電量 高效的算法和數(shù)據(jù)結(jié)構(gòu)(3).初始化時(shí)...
    歐辰_OSR閱讀 29,339評論 8 265
  • 前言 本人主要是結(jié)合《Java多線程編程核心技術(shù)》這本書的第二章內(nèi)容解愤,對synchronized關(guān)鍵字的知識進(jìn)行梳...
    AR7_閱讀 889評論 0 4
  • Java多線程學(xué)習(xí) [-] 一擴(kuò)展javalangThread類 二實(shí)現(xiàn)javalangRunnable接口 三T...
    影馳閱讀 2,952評論 1 18
  • 這周的周征文的主題“向往的生活”公布之后,很多小伙伴都跟慕寶表示很喜歡這個主題乎莉,其實(shí)慕寶自己也很喜歡送讲,但是大家開心...
    林子慕閱讀 1,160評論 22 26