Synchronized修飾靜態方法,對類對象進行加鎖,是類鎖。java
Synchronized修飾實例方法,對方法所屬對象進行加鎖,是對象鎖。數組
Synchronized修飾代碼塊時,對一段代碼塊進行加鎖,是對象鎖。數據結構
/** * synchronized示例 * 一、修飾靜態方法 * 二、修飾實例方法 * 三、修飾代碼塊 */ public class SyncDemo2 { private static int num = 0; /** * 修飾靜態方法 */ public static synchronized void count1() { for (int i = 0; i < 100000000; i++) { num++; } } /** * 修飾實例方法 */ public synchronized void count2() { for (int i = 0; i < 100000000; i++) { num++; } } /** * 修飾代碼塊 * 效果與修飾靜態方法相同 */ public void count3() { synchronized(SyncDemo2.class) { for (int i = 0; i < 100000000; i++) { num++; } } } /** * 修飾代碼塊 * 效果與修飾實例方法相同 */ public void count4() { synchronized(this) { for (int i = 0; i < 100000000; i++) { num++; } } } public static void main(String[] args) { //兩個線程運行一個類的兩個對象,運行類的靜態方法count1, //產生同步,num=200000000 //兩個線程運行一個類的兩個對象,運行類的實例方法count2 //由於調用的是不一樣的對象,並未產生同步,num<=200000000 SyncDemo2 syncDemo1 = new SyncDemo2(); SyncDemo2 syncDemo2 = new SyncDemo2(); //兩個線程運行一個對象,運行類的實例方法count2 //由於調用的是同一個對象,產生同步,num=200000000 //SyncDemo2 syncDemo3 = new SyncDemo2(); //syncDemo1 = syncDemo3; //syncDemo2 = syncDemo3; //啓動兩個線程進行運算 Thread thread1 = new Thread(new ThreadDemo(syncDemo1)); Thread thread2 = new Thread(new ThreadDemo(syncDemo2)); thread1.start(); thread2.start(); try { thread1.join(); thread2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(SyncDemo2.num); } } class ThreadDemo implements Runnable { SyncDemo2 syncDemo2; public ThreadDemo(SyncDemo2 syncDemo2){ this.syncDemo2 = syncDemo2; } @Override public void run() { //syncDemo2.count1(); //syncDemo2.count2(); syncDemo2.count3(); //syncDemo2.count4(); } }
Java 虛擬機中的同步(Synchronization)基於進入和退出管程(Monitor)對象實現,不管是顯式同步(有明確的 monitorenter 和 monitorexit 指令,即同步代碼塊)仍是隱式同步都是如此。在 Java 語言中,同步用的最多的地方多是被 synchronized 修飾的同步方法。同步方法並非由 monitorenter 和 monitorexit 指令來實現同步的,而是由方法調用指令讀取運行時常量池中方法的 ACC_SYNCHRONIZED 標誌來隱式實現的。多線程
鎖是加在對象上的,不管是類對象仍是實例對象。每一個對象主要由一個對象頭、實例變量、填充數據三部分組成,結構如圖:併發
synchronized使用的鎖對象是存儲在Java對象頭裏的,jvm中採用2個字來存儲對象頭(若是對象是數組則會分配3個字,多出來的1個字記錄的是數組長度),其主要結構是由Mark Word 和 Class Metadata Address 組成,其結構說明以下:jvm
其中Mark Word在默認狀況下存儲着對象的HashCode、分代年齡、鎖標記位等如下是32位JVM的Mark Word默認存儲結構:socket
因爲對象頭的信息是與對象自身定義的數據沒有關係的額外存儲成本,所以考慮到JVM的空間效率,Mark Word 被設計成爲一個非固定的數據結構,以便存儲更多有效的數據,它會根據對象自己的狀態複用本身的存儲空間,如32位JVM下,除了上述列出的Mark Word默認存儲結構外,還有以下可能變化的結構:ide
Synchronized屬於結構中的重量級鎖,鎖標識位爲10,其中指針指向的是monitor對象的起始地址。每一個對象都存在着一個 monitor 與之關聯,對象與其 monitor 之間的關係有存在多種實現方式,如monitor能夠與對象一塊兒建立銷燬或當線程試圖獲取對象鎖時自動生成,但當一個 monitor 被某個線程持有後,它便處於鎖定狀態。在Java虛擬機(HotSpot)中,monitor是由ObjectMonitor實現的,其主要數據結構以下(位於HotSpot虛擬機源碼ObjectMonitor.hpp文件,C++實現的)。高併發
ObjectMonitor() { _header = NULL; _count = 0; //記錄個數 _waiters = 0, _recursions = 0; _object = NULL; _owner = NULL; _WaitSet = NULL; //處於wait狀態的線程,會被加入到_WaitSet _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; //處於等待鎖block狀態的線程,會被加入到該列表 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; }
結構中幾個重要的字段要關注,_count、_owner、_EntryList、_WaitSet。性能
count用來記錄線程進入加鎖代碼的次數。
owner記錄當前持有鎖的線程,即持有ObjectMonitor對象的線程。
EntryList是想要持有鎖的線程的集合。
WaitSet 是加鎖對象調用wait()方法後,等待被喚醒的線程的集合。
每一個等待鎖的線程都會被封裝成ObjectWaiter對象,當多個線程同時訪問一段同步代碼(臨界區)時,首先會進入 _EntryList 集合,當線程獲取到對象的monitor 後進入 _Owner 區域並把monitor中的owner變量設置爲當前線程,_owner指向持有ObjectMonitor對象的線程。同時monitor中的計數器count加1。
若線程調用 wait() 方法,將釋放當前持有的monitor,owner變量恢復爲null,count自減1,同時該線程進入 WaitSet集合中等待被喚醒。
若當前線程執行完畢也將釋放monitor並復位變量的值,以便其餘線程進入獲取monitor(鎖)。
(圖摘自:https://blog.csdn.net/javazejian/article/details/72828483)
Synchronized在jvm字節碼上的體現
咱們以以前的例子爲例,使用javac編譯代碼,而後使用javap進行反編譯。
反編譯後部分片斷以下圖:
對於使用synchronized修飾的方法,反編譯後字節碼中會有ACC_SYNCHRONIZED關鍵字。
而synchronized修飾的代碼塊中,在代碼塊的先後會有monitorenter、monitorexit關鍵字,此處的字節碼中有兩個monitorexit是由於咱們有try-catch語句塊,有兩個出口。
Synchronized與等待喚醒
等待喚醒是指調用對象的wait、notify、notifyAll方法。調用這三個方法時,對象必須被synchronized修飾,由於這三個方法在執行時,必須得到當前對象的監視器monitor對象。
另外,與sleep方法不一樣的是wait方法調用完成後,線程將被暫停,但wait方法將會釋放當前持有的監視器鎖(monitor),直到有線程調用notify/notifyAll方法後方能繼續執行。而sleep方法只讓線程休眠並不釋放鎖。notify/notifyAll方法調用後,並不會立刻釋放監視器鎖,而是在相應的synchronized代碼塊或synchronized方法執行結束後才自動釋放鎖。
重入
當多個線程請求同一個臨界資源,執行到同一個臨界區時會產生互斥,未得到資源的線程會阻塞。而當一個已得到臨界資源的線程再次請求此資源時並不會發生阻塞,仍能獲取到資源、進入臨界區,這就是重入。Synchronized是可重入的。
中斷
在Thread類中與線程中斷相關的方法有三個:
/** * Interrupt設置一個線程爲中斷狀態 * Interrupt操做的線程處於sleep,wait,join 阻塞等狀態的時候,清除「中斷」狀態,拋出一個InterruptedException * Interrupt操做的線程在可中斷通道上因調用某個阻塞的 I/O 操做(serverSocketChannel. accept()、socketChannel.connect、socketChannel.open、 * socketChannel.read、socketChannel.write、fileChannel.read、fileChannel.write),會拋出一個ClosedByInterruptException **/ public void interrupt(); /** * 判斷線程是否處於「中斷」狀態,而後將「中斷」狀態清除 **/ public static boolean interrupted(); /** * 判斷線程是否處於「中斷」狀態 **/ public boolean isInterrupted();
在實際使用中,當線程正處於調用sleep、wait、join方法後,調用interrupt會清除線程中斷狀態,並拋出異常。而當線程已進入臨界區、正在執行,則須要isInterrupted()或interrupted()與interrupt()配合使用中斷執行中的線程。
Sychronized修飾的方法、代碼塊被多個線程請求時,調用中斷。正在執行的線程響應中斷。正在阻塞的線程、執行中的線程都會標記中斷狀態,但阻塞的線程不會馬上處理中斷,而是在進入臨界區後再響應。
示例:中斷對執行synchronized方法線程的影響
import java.util.concurrent.TimeUnit; /** * 示例:中斷對執行synchronized方法線程的影響 * 正在執行的線程響應中斷 * 正在阻塞的線程、執行中的線程都會標記中斷狀態, * 但阻塞的線程不會馬上處理中斷,而是在進入臨界區後再響應。 */ public class SyncDemo3 { public static boolean flag = true; public static synchronized void m1() { System.out.println(Thread.currentThread().getName() + " hold resource!"); while (flag) { if (!Thread.currentThread().isInterrupted()) { //不用sleep,由於sleep會對中斷拋出異常 Thread.yield(); } else { System.out.println(Thread.currentThread().getName() + " interrupted and release !"); return; } } } public static void main(String[] args) { SyncDemo3 syncDemo1 = new SyncDemo3(); SyncDemo3 syncDemo2 = new SyncDemo3(); //啓動兩個線程 Thread thread1 = new Thread(new ThreadDemo3(syncDemo1), "thread1"); Thread thread2 = new Thread(new ThreadDemo3(syncDemo2), "thread2"); thread1.start(); //休眠1秒,讓thread1獲取資源 try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } thread2.start(); //休眠1秒 try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } //thread1中斷 thread1.interrupt(); //thread2中斷 thread2.interrupt(); if (thread1.isInterrupted()) { System.out.println("thread1 interrupt!"); } if (thread2.isInterrupted()) { System.out.println("thread2 interrupt!"); } //休眠1秒,讓thread2獲取資源 try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } } class ThreadDemo3 implements Runnable { SyncDemo3 syncDemo3; public ThreadDemo3(SyncDemo3 syncDemo3) { this.syncDemo3 = syncDemo3; } @Override public void run() { syncDemo3.m1(); } }
在JDK6之前synchronized的性能並不高,但在以後進行了優化,咱們在以前的Mark Word的結構中能夠看到,鎖的狀態總共有四種,無鎖狀態、偏向鎖、輕量級鎖和重量級鎖。隨着鎖的競爭,鎖能夠從偏向鎖升級到輕量級鎖,再升級的重量級鎖,可是鎖的升級是單向的,也就是說只能從低到高升級,不會出現鎖的降級。
偏向鎖
偏向鎖是Java 6以後加入的新鎖,它是一種針對加鎖操做的優化手段。通過研究發現,在大多數狀況下,鎖不只不存在多線程競爭,並且老是由同一線程屢次得到,所以爲了減小同一線程獲取鎖(會涉及到一些CAS操做,耗時)的代價而引入偏向鎖。
偏向鎖的核心思想是,若是一個線程得到了鎖,那麼鎖就進入偏向模式,此時Mark Word 的結構也變爲偏向鎖結構,當這個線程再次請求鎖時,無需再作任何同步操做,即獲取鎖的過程,這樣就省去了大量有關鎖申請的操做,從而也就提供程序的性能。
因此,對於沒有鎖競爭的場合,偏向鎖有很好的優化效果,畢竟極有可能連續屢次是同一個線程申請相同的鎖。可是對於鎖競爭比較激烈的場合,偏向鎖就失效了,由於這樣場合極有可能每次申請鎖的線程都是不相同的,所以這種場合下不該該使用偏向鎖,不然會得不償失。但偏向鎖失敗後,並不會當即膨脹爲重量級鎖,而是先升級爲輕量級鎖。
輕量級鎖
若偏向鎖失敗,虛擬機並不會當即升級爲重量級鎖,它還會嘗試使用一種稱爲輕量級鎖的優化手段,此時Mark Word 的結構也變爲輕量級鎖的結構。輕量級鎖可以提高程序性能的依據是「對絕大部分的鎖,在整個同步週期內都不存在競爭」,注意這是經驗數據。須要瞭解的是,輕量級鎖所適應的場景是線程交替執行同步塊的場合,若是存在同一時間訪問同一鎖的場合,就會致使輕量級鎖膨脹爲重量級鎖。
自旋鎖
輕量級鎖失敗後,虛擬機爲了不線程真實地在操做系統層面掛起,還會進行一項稱爲自旋鎖的優化手段。這是基於在大多數狀況下,線程持有鎖的時間都不會太長,若是直接掛起操做系統層面的線程可能會得不償失,畢竟操做系統實現線程之間的切換時須要從用戶態轉換到核心態,這個狀態之間的轉換須要相對比較長的時間,時間成本相對較高,所以自旋鎖會假設在不久未來,當前的線程能夠得到鎖,所以虛擬機會讓當前想要獲取鎖的線程作幾個空循環(這也是稱爲自旋的緣由),通常不會過久,多是50個循環或100循環,在通過若干次循環後,若是獲得鎖,就順利進入臨界區。若是還不能得到鎖,那就會將線程在操做系統層面掛起,這就是自旋鎖的優化方式,這種方式確實也是能夠提高效率的。最後沒辦法也就只能升級爲重量級鎖了。
鎖消除
消除鎖是虛擬機另一種鎖的優化,這種優化更完全,Java虛擬機在JIT編譯時(能夠簡單理解爲當某段代碼即將第一次被執行時進行編譯,又稱即時編譯),經過對運行上下文的掃描,去除不可能存在共享資源競爭的鎖,經過這種方式消除沒有必要的鎖,能夠節省毫無心義的請求鎖時間。
鎖粗化
若是虛擬機探測到有這樣一串零碎的操做都對同一個對象加鎖,將會把加鎖同步的範圍擴展到整個操做序列的外部,這樣就只須要加鎖一次就夠了。
參考:
《實戰Java高併發程序設計》 葛一鳴,郭超 著
https://blog.csdn.net/javazejian/article/details/72828483