Java開發中synchronized的定義及用法詳解
概念
是利用鎖的機制來實現同步的。互斥性:即在同一時間只允許一個線程持有某個對象鎖,通過這種特性來實現多線程中的協調機制,這樣在同一時間只有一個線程對需同步的代碼塊(復合操作)進行訪問。互斥性我們也往往稱為操作的原子性。可見性:必須確保在鎖被釋放之前,對共享變量所做的修改,對于隨后獲得該鎖的另一個線程是可見的(即在獲得鎖時應獲得最新共享變量的值),否則另一個線程可能是在本地緩存的某個副本上繼續操作從而引起不一致。
用法
修飾靜態方法:
//同步靜態方法 public synchronized static void methodName() { try { TimeUnit.SECONDS.sleep(2); System.out.println(Thread.currentThread().getName()+' aaa'); } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) { for (int i = 0; i < 5; i++) { new Thread(SynchronizedDemo::methodName).start(); } }
當synchronized作用于靜態方法時,其鎖就是當前類的class對象鎖。由于靜態成員不專屬于任何一個實例對象,是類成員,因此通過class對象鎖可以控制靜態 成員的并發操作。需要注意的是如果一個線程A調用一個實例對象的非static synchronized方法,而線程B需要調用這個實例對象所屬類的靜態 synchronized方法,是允許的,不會發生互斥現象,因為訪問靜態 synchronized 方法占用的鎖是當前類的class對象,而訪問非靜態 synchronized 方法占用的鎖是當前實例對象鎖
修飾實例方法:
//同步非靜態方法 ,當前線程的鎖便是實例對象methodName public synchronized void methodName() { try { TimeUnit.SECONDS.sleep(2); System.out.println(Thread.currentThread().getName()+' aaa'); } catch (InterruptedException e) { e.printStackTrace(); } }
當一個線程正在訪問一個對象的 synchronized 實例方法,那么其他線程不能訪問該對象的其他 synchronized 方法,畢竟一個對象只有一把鎖,當一個線程獲取了該對象的鎖之后,其他線程無法獲取該對象的鎖,所以無法訪問該對象的其他synchronized實例方法,但是其他線程還是可以訪問該實例對象的其他非synchronized方法,當然如果是一個線程 A 需要訪問實例對象 obj1 的 synchronized 方法 f1(當前對象鎖是obj1),另一個線程 B 需要訪問實例對象 obj2 的 synchronized 方法 f2(當前對象鎖是obj2),這樣是允許的,因為兩個實例對象鎖并不同相同。此時如果兩個線程操作數據并非共享的,線程安全是有保障的,遺憾的是如果兩個線程操作的是共享數據,那么線程安全就有可能無法保證了。
代碼塊方式(this):
//修飾非靜態方法 public void methodName() { //修飾代碼塊,this=當前對象(誰調用就指待誰) synchronized (this) { try { TimeUnit.SECONDS.sleep(2); System.out.println(Thread.currentThread().getName() + ' aaa'); } catch (InterruptedException e) { e.printStackTrace(); } } }
synchronized(this|object) {}:在 Java 中,每個對象都會有一個 monitor 對象,這個對象其實就是 Java 對象的鎖,通常會被稱為“內置鎖”或“對象鎖”。類的對象可以有多個,所以每個對象有其獨立的對象鎖,互不干擾。
代碼塊方式(Class):
//修飾非靜態方法 public void methodName() { //修飾代碼塊,使用Class類 //使用ClassLoader 加載字節碼的時候會向堆里面存放Class類,所有的對象都對應唯一的Class類 //SynchronizedDemo.class 這里拿到的就是堆里面的Class類,也就是所有的Class的對象都共同使用這個synchronized synchronized (SynchronizedDemo.class) { try { TimeUnit.SECONDS.sleep(2); System.out.println(Thread.currentThread().getName() + ' aaa'); } catch (InterruptedException e) { e.printStackTrace(); } } }
synchronized(類.class) {}:在 Java 中,針對每個類也有一個鎖,可以稱為“類鎖”,類鎖實際上是通過對象鎖實現的,即類的 Class 對象鎖。每個類只有一個 Class 對象,所以每個類只有一個類鎖。
在 Java 中,每個對象都會有一個 monitor 對象,監視器。
某一線程占有這個對象的時候,先monitor 的計數器是不是0,如果是0還沒有線程占有,這個時候線程占有這個對象,并且對這個對象的monitor+1;如果不為0,表示這個線程已經被其他線程占有,這個線程等待。當線程釋放占有權的時候,monitor-1; 同一線程可以對同一對象進行多次加鎖,+1,+1,重入性。堆棧分析:
jconsole.exe
JVM指令分析
Javap -V 反編譯
Monitorenter 互斥入口 Monitorexit 互斥出口
monitorexit有兩個,一個是正常出口,一個是異常出口
以上是對代碼塊加鎖
ACC_SYNCHRONIZED : 對方法加鎖。加鎖標記
java虛擬機對synchronized的優化
對象頭與monitor:
一個漢字兩個字節,一個字節8bit實例變量:存放類的屬性數據信息,包括父類的屬性信息,如果是數組的實例部分還包括數組的長度,這部分內存按4字節對齊。填充數據:由于虛擬機要求對象起始地址必須是8字節的整數倍。填充數據不是必須存在的,僅僅是為了字節對齊。對象頭:它實現synchronized的鎖對象的基礎,一般而言,synchronized使用的鎖對象是存儲在Java對象頭里的,jvm中采用2個字來存儲對象頭(如果對象是數組則會分配3個字,多出來的1個字記錄的是數組長度),其主要結構是由Mark Word 和 Class Metadata Address 組成
虛擬機位數 頭對象結構 說明 32/64bit Mark Word 存儲對象的hashCode、鎖信息或分代年齡或GC標志等信息 32/64bit Class Metadata Address 類型指針指向對象的類元數據,JVM通過這個指針確定該對象是哪個類的實例其中Mark Word在默認情況下存儲著對象的HashCode、分代年齡、鎖標記位等以下是32位JVM的Mark Word默認存儲結構.
偏向鎖:偏向鎖是Java 6之后加入的新鎖,它是一種針對加鎖操作的優化手段,經過研究發現,在大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,因此為了減少同一線程獲取鎖(會涉及到一些CAS操作,耗時)的代價而引入偏向鎖。偏向鎖的核心思想是,如果一個線程獲得了鎖,那么鎖就進入偏向模式,此時Mark Word 的結構也變為偏向鎖結構,當這個線程再次請求鎖時,無需再做任何同步操作,即獲取鎖的過程,這樣就省去了大量有關鎖申請的操作,從而也就提供程序的性能。所以,對于沒有鎖競爭的場合,偏向鎖有很好的優化效果,畢竟極有可能連續多次是同一個線程申請相同的鎖。但是對于鎖競爭比較激烈的場合,偏向鎖就失效了,因為這樣場合極有可能每次申請鎖的線程都是不相同的,因此這種場合下不應該使用偏向鎖,否則會得不償失,需要注意的是,偏向鎖失敗后,并不會立即膨脹為重量級鎖,而是先升級為輕量級鎖。下面我們接著了解輕量級鎖。。
輕量級鎖:倘若偏向鎖失敗,虛擬機并不會立即升級為重量級鎖,它還會嘗試使用一種稱為輕量級鎖的優化手段(1.6之后加入的),此時Mark Word 的結構也變為輕量級鎖的結構。輕量級鎖能夠提升程序性能的依據是“對絕大部分的鎖,在整個同步周期內都不存在競爭”,注意這是經驗數據。需要了解的是,輕量級鎖所適應的場景是線程交替執行同步塊的場合,如果存在同一時間訪問同一鎖的場合,就會導致輕量級鎖膨脹為重量級鎖。
重量級鎖:synchronized的對象鎖,鎖標識位為10,其中指針指向的是monitor對象(也稱為管程或監視器鎖)的起始地址。每個對象都存在著一個 monitor 與之關聯,對象與其 monitor 之間的關系有存在多種實現方式,如monitor可以與對象一起創建銷毀或當線程試圖獲取對象鎖時自動生成,但當一個 monitor 被某個線程持有后,它便處于鎖定狀態。在Java虛擬機(HotSpot)中,monitor是由ObjectMonitor實現的。
自旋鎖:輕量級鎖失敗后,虛擬機為了避免線程真實地在操作系統層面掛起,還會進行一項稱為自旋鎖的優化手段。這是基于在大多數情況下,線程持有鎖的時間都不會太長,如果直接掛起操作系統層面的線程可能會得不償失,畢竟操作系統實現線程之間的切換時需要從用戶態轉換到核心態,這個狀態之間的轉換需要相對比較長的時間,時間成本相對較高,因此自旋鎖會假設在不久將來,當前的線程可以獲得鎖,因此虛擬機會讓當前想要獲取鎖的線程做幾個空循環(這也是稱為自旋的原因),一般不會太久,可能是50個循環或100循環,在經過若干次循環后,如果得到鎖,就順利進入臨界區。如果還不能獲得鎖,那就會將線程在操作系統層面掛起,這就是自旋鎖的優化方式,這種方式確實也是可以提升效率的。最后沒辦法也就只能升級為重量級鎖了。
鎖消除:消除鎖是虛擬機另外一種鎖的優化,這種優化更徹底,Java虛擬機在JIT編譯時(可以簡單理解為當某段代碼即將第一次被執行時進行編譯,又稱即時編譯),通過對運行上下文的掃描,去除不可能存在共享資源競爭的鎖,通過這種方式消除沒有必要的鎖,可以節省毫無意義的請求鎖時間,如下StringBuffer的append是一個同步方法,但是在add方法中的StringBuffer屬于一個局部變量,并且不會被其他線程所使用,因此StringBuffer不可能存在共享資源競爭的情景,JVM會自動將其鎖消除。
synchronized的可重入性
從互斥鎖的設計上來說,當一個線程試圖操作一個由其他線程持有的對象鎖的臨界資源時,將會處于阻塞狀態,但當一個線程再次請求自己持有對象鎖的臨界資源時,這種情況屬于重入鎖,請求將會成功,在java中synchronized是基于原子性的內部鎖機制,是可重入的,因此在一個線程調用synchronized方法的同時在其方法體內部調用該對象另一個synchronized方法,也就是說一個線程得到一個對象鎖后再次請求該對象鎖,是允許的,這就是synchronized的可重入性。
public class AccountingSync implements Runnable{ static AccountingSync instance=new AccountingSync(); static int i=0; static int j=0; @Override public void run() { for(int j=0;j<1000000;j++){ //this,當前實例對象鎖 synchronized(this){ i++; increase();//synchronized的可重入性 } } } public synchronized void increase(){ j++; } public static void main(String[] args) throws InterruptedException { Thread t1=new Thread(instance); Thread t2=new Thread(instance); t1.start();t2.start(); t1.join();t2.join(); System.out.println(i); }}
正如代碼所演示的,在獲取當前實例對象鎖后進入synchronized代碼塊執行同步代碼,并在代碼塊中調用了當前實例對象的另外一個synchronized方法,再次請求當前實例鎖時,將被允許,進而執行方法體代碼,這就是重入鎖最直接的體現,需要特別注意另外一種情況,當子類繼承父類時,子類也是可以通過可重入鎖調用父類的同步方法。注意由于synchronized是基于monitor實現的,因此每次重入,monitor中的計數器仍會加1.
到此這篇關于Java開發中synchronized的定義及用法詳解的文章就介紹到這了,更多相關Java synchronized內容請搜索好吧啦網以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持好吧啦網!
相關文章: