這需要從synchronized的原理開始講起。synchronized關鍵字有下面三種用法:
修飾實例方法:
對當前實例加鎖,進入方法需要獲得當前實例的鎖修飾靜態方法:
對當前類對象加鎖,進入靜態方法需要獲得當前類對象的鎖修飾代碼塊:
對指定對象進行加鎖,進入代碼塊需要獲得指定對象的鎖
那么上面三種方式有什么區別呢?
這需要先理解下synchronized的底層語義。java中的同步是基於進入和退出管程(Moniter)對象來實現的,無論是顯式同步(有明確的monitorenter和monitorexit指令,即同步代碼塊)還是隱式同步(同步方法,方法調用指令讀取運行時常量池中的方法的ACC_SYNCHRONIZED標志來隱式實現的)。先看下基於對象實現的,需要了解下java對象。在JVM中,對象在內存中的區域分成三部分:對象頭,實例變量,填充數據。
實例變量:存放類的屬性數據信息,包括父類的屬性信息,如果是數組的實例部分還包括數組的長度,這部分內存按4字節對齊。填充數據:由於虛擬機要求對象起始地址必須是8字節的整數倍。填充數據不是必須存在的,僅僅是為了字節對齊,這點了解即可。
對象頭:是實現synchronized鎖對象的基礎。synchronized使用的鎖對象是存儲在Java對象頭里的,其主要結構是由Mark Word 和 Class Metadata Address 組成。
其中Mark Word在默認情況下存儲着對象的HashCode、分代年齡、鎖標記位等以下是32位JVM的Mark Word默認存儲結構
synchronized的對象鎖,鎖標識位為10,其中指針指向的是monitor對象(也稱為管程或監視器鎖)的起始地址。每個對象都存在着一個 monitor 與之關聯,對象與其 monitor 之間的關系有存在多種實現方式,如monitor可以與對象一起創建銷毀或當線程試圖獲取對象鎖時自動生成,但當一個 monitor 被某個線程持有后,它便處於鎖定狀態。了解了對象頭,我們就可以知道為什么上面的代碼雖然都使用了synchronized修飾,但是還是有線程安全問題,因為靜態方法和實例方法鎖的對象是不一致的(Monitor不是同一個),所以導致最終沒有達到預期效果。