前言
通俗:形成線程安全問題的主要誘因有兩點:html
學術:形成線程安全問題的主要誘因有兩點:java
當存在多個線程操做共享數據時,須要保證同一時刻有且只有一個線程在操做共享數據,其餘線程必須等到該線程處理完數據後再進行,這種方式的名稱叫·互斥鎖,也就是說當一個共享數據被當前正在訪問的線程加上互斥鎖後,在同一個時刻,其餘線程只能處於等待的狀態,直到當前線程處理完畢釋放該鎖。node
關鍵字 synchronized能夠保證(1)在同一個時刻,只有一個線程能夠執行某個方法或者某個代碼塊(主要是對方法或者代碼塊中存在共享數據的操做),(2)synchronized保證一個線程的變化(主要是共享數據的變化)被其餘線程所看到(保證可見性,徹底能夠替代Volatile功能)也就是happens-before規則。segmentfault
標註:在學習中須要修改的內容以及筆記全在這裏 www.javanode.cn,謝謝!有任何不妥的地方望糾正數組
synchronized關鍵字最主要有如下3種應用方式,下面分別介紹緩存
實例
的鎖當前類對象
的鎖給定對象的鎖
。所謂的實例對象鎖
就是用synchronized修飾實例對象中的實例方法,注意是實例方法不包括靜態方法安全
public class AccountingSync implements Runnable{ //共享資源(臨界資源) static int i=0; /** * synchronized 修飾實例方法 */ public synchronized void increase(){ i++; } @Override public void run() { for(int j=0;j<1000000;j++){ increase(); } } public static void main(String[] args) throws InterruptedException { AccountingSync instance=new AccountingSync(); Thread t1=new Thread(instance); Thread t2=new Thread(instance); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i); } /** * 輸出結果: * 2000000 */ }
咱們應該注意到synchronized修飾的是實例方法increase,在這樣的狀況下,當前線程的鎖即是實例對象instance,注意Java中的線程同步鎖能夠是任意對象
。bash
這裏咱們還須要意識到,當一個線程正在訪問一個對象的 synchronized 實例方法,那麼其餘線程不能訪問該對象的其餘 synchronized 方法,畢竟一個對象只有一把鎖
,當一個線程獲取了該對象的鎖以後,其餘線程沒法獲取該對象的鎖,因此沒法訪問該對象的其餘synchronized實例方法,可是其餘線程仍是能夠訪問該實例對象的其餘非synchronized方法,固然若是是一個線程 A 須要訪問實例對象 obj1 的 synchronized 方法 f1(當前對象鎖是obj1),另外一個線程 B 須要訪問實例對象 obj2 的 synchronized 方法 f2(當前對象鎖是obj2),這樣是容許的,由於兩個實例對象鎖並不一樣相同,此時若是兩個線程操做數據並不是共享的,線程安全是有保障的
,數據結構
遺憾的是若是兩個線程操做的是共享數據,那麼線程安全就有可能沒法保證
了,以下代碼將演示出該現象多線程
public class AccountingSyncBad implements Runnable{ static int i=0; public synchronized void increase(){ i++; } @Override public void run() { for(int j=0;j<1000000;j++){ increase(); } } public static void main(String[] args) throws InterruptedException { //new新實例 Thread t1=new Thread(new AccountingSyncBad()); //new新實例 Thread t2=new Thread(new AccountingSyncBad()); t1.start(); t2.start(); //join含義:當前線程A等待thread線程終止以後才能從thread.join()返回 t1.join(); t2.join(); System.out.println(i); } }
上述代碼與前面不一樣的是咱們同時建立了兩個新實例AccountingSyncBad,而後啓動兩個不一樣的線程對共享變量i進行操做,但很遺憾操做結果是1452317
而不是指望結果2000000
,由於上述代碼犯了嚴重的錯誤,雖然咱們使用synchronized修飾了increase方法,但卻new了兩個不一樣的實例對象
,這也就意味着存在着兩個不一樣的實例對象鎖
,所以t1和t2都會進入各自的對象鎖
,也就是說t1和t2線程使用的是不一樣的鎖,所以線程安全是沒法保證的。解決這種困境的的方式是將synchronized做用於靜態的increase方法,這樣的話,對象鎖就當前類對象,因爲不管建立多少個實例對象,但對於的類對象擁有隻有一個,全部在這樣的狀況下對象鎖就是惟一的。下面咱們看看如何使用將synchronized做用於靜態的increase方法。
當synchronized做用於靜態方法時,其鎖就是當前類的class對象鎖。因爲靜態成員不專屬於任何一個實例對象,是類成員,所以經過class對象鎖能夠控制靜態 成員的併發操做。須要注意的是若是一個線程A調用一個實例對象的非static synchronized方法,而線程B須要調用這個實例對象所屬類的靜態 synchronized方法,是容許的,不會發生互斥現象,由於訪問靜態 synchronized 方法佔用的鎖是當前類的class對象,而訪問非靜態 synchronized 方法佔用的鎖是當前實例對象鎖,看以下代碼
public class AccountingSyncClass implements Runnable{ static int i=0; /** * 做用於靜態方法,鎖是當前class對象,也就是 * AccountingSyncClass類對應的class對象 */ public static synchronized void increase(){ i++; } /** * 非靜態,訪問時鎖不同不會發生互斥 */ public synchronized void increase4Obj(){ i++; } @Override public void run() { for(int j=0;j<1000000;j++){ increase(); } } public static void main(String[] args) throws InterruptedException { //new新實例 Thread t1=new Thread(new AccountingSyncClass()); //new心事了 Thread t2=new Thread(new AccountingSyncClass()); //啓動線程 t1.start();t2.start(); t1.join();t2.join(); System.out.println(i); } }
因爲synchronized關鍵字修飾的是靜態increase方法,與修飾實例方法不一樣的是,其鎖對象是當前類的class對象。注意代碼中的increase4Obj方法是實例方法,其對象鎖是當前實例對象,若是別的線程調用該方法,將不會產生互斥現象,畢竟鎖對象不一樣,但咱們應該意識到這種狀況下可能會發現線程安全問題(操做了共享靜態變量i)。
除了使用關鍵字修飾實例方法和靜態方法外,還可使用同步代碼塊,在某些狀況下,咱們編寫的方法體可能比較大,同時存在一些比較耗時的操做,而須要同步的代碼又只有一小部分,若是直接對整個方法進行同步操做,可能會得不償失,此時咱們可使用同步代碼塊的方式對須要同步的代碼進行包裹,這樣就無需對整個方法進行同步操做了,同步代碼塊的使用示例以下:
public class AccountingSync implements Runnable{ static AccountingSync instance=new AccountingSync(); static int i=0; @Override public void run() { //省略其餘耗時操做.... //使用同步代碼塊對變量i進行同步操做,鎖對象爲instance synchronized(instance){ for(int j=0;j<1000000;j++){ i++; } } } 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做用於一個給定的實例對象instance
,即當前實例對象就是鎖對象,每次當線程進入synchronized包裹的代碼塊時就會要求當前線程持有instance實例對象鎖,若是當前有其餘線程正持有該對象鎖,那麼新到的線程就必須等待,這樣也就保證了每次只有一個線程執行i++;
操做。固然除了instance做爲對象外。
咱們還可使用this對象(表明當前實例)或者當前類的class對象做爲鎖,以下代碼:
//this,當前實例對象鎖 synchronized(this){ for(int j=0;j<1000000;j++){ i++; } } //class對象鎖 synchronized(AccountingSync.class){ for(int j=0;j<1000000;j++){ i++; } }
synchronized鎖對象和鎖類是本質都是對對象來加鎖。類也是一個特殊的對象。只不過類對象只有一個。對象內鎖不一樣的屬性,兩個同步方法能夠同時訪問
在JVM中,對象在內存中的佈局分爲三塊區域:對象頭、實例數據和對齊填充。以下:
對象頭由mark word ,指向對象實例數據的指針(Class Metadata Address),length組成,其結構說明以下表:
虛擬機位數 | 頭對象結構 | 說明 |
---|---|---|
32/64bit | Mark Word | 存儲對象的hashCode、鎖信息或分代年齡或GC標誌等信息 |
32/64bit | Class Metadata Address | 類型指針指向對象的類元數據,JVM經過這個指針肯定該對象是哪一個類的實例。 |
32/64bit | length | 當對象是數組時,length保存數組的長度 |
其中Mark Word
在默認狀況下存儲着對象的HashCode、分代年齡、鎖標記位等如下是32位JVM的Mark Word默認存儲結構
鎖狀態 | 25bit | 4bit | 1bit是不是偏向鎖 | 2bit 鎖標誌位 |
---|---|---|---|---|
無鎖狀態 | 對象HashCode | 對象分代年齡 | 0 | 01 |
因爲對象頭的信息是與對象自身定義的數據沒有關係的額外存儲成本,所以考慮到JVM的空間效率,Mark Word 被設計成爲一個非固定的數據結構
,以便存儲更多有效的數據,它會根據對象自己的狀態複用本身的存儲空間,
Java 虛擬機中的同步(Synchronization)基於進入和退出管程(Monitor)對象實現, 不管是顯式同步(有明確的 monitorenter 和 monitorexit 指令,即同步代碼塊)仍是隱式同步(方法級的同步)都是如此。在 Java 語言中,同步用的最多的地方多是被 synchronized 修飾的同步方法。同步方法 並非由 monitorenter 和 monitorexit 指令來實現同步的,而是由方法調用指令讀取運行時常量池中方法的 ACC_SYNCHRONIZED 標誌來隱式實現的,
深刻JVM看字節碼,建立以下的代碼:
public class SynchronizedDemo2 { Object object = new Object(); public void method1() { synchronized (object) { } } }
從字節碼中可知同步語句塊的實現使用的是monitorenter 和 monitorexit 指令
。這也是添Synchronized關鍵字以後獨有的。執行同步代碼塊首先要先執行monitorenter指令,退出的時候是monitorexit指令。使用Synchronized進行同步,其關鍵就是必需要對對象的監視器monitor進行獲取
,當執行monitorenter指令時,<u>當前線程將試圖獲取 objectref(即對象鎖) 所對應的 monitor 的持有權,當 objectref 的 monitor 的進入計數器爲 0,那線程能夠成功取得 monitor,並將計數器值設置爲 1,</u>取鎖成功。Synchronized先天具備重入性
。若是當前線程已經擁有 objectref 的 monitor 的持有權,那它能夠重入這個 monitor (關於重入性稍後會分析),重入時計數器的值也會加 1。假若其餘線程已經擁有 objectref 的 monitor 的全部權,那當前線程將被阻塞,直到正在執行線程執行完畢,即monitorexit指令被執行,執行線程將釋放 monitor(鎖)並設置計數器值爲0 ,其餘線程將有機會持有 monitor 。值得注意的是編譯器將會確保不管方法經過何種方式完成,方法中調用過的每條 monitorenter 指令都有執行其對應 monitorexit 指令,而不管這個方法是正常結束仍是異常結束。爲了保證在方法異常完成時 monitorenter 和 monitorexit 指令依然能夠正確配對執行,<u>編譯器會自動產生一個異常處理器
,這個異常處理器聲明可處理全部的異常,它的目的就是用來執行 monitorexit 指令。</u>從字節碼中也能夠看出多了一個monitorexit指令,它就是異常結束時被執行的釋放monitor 的指令。
方法級的同步是隱式,即無需經過字節碼指令來控制的,它實如今方法調用和返回操做之中。JVM能夠從方法常量池中的方法表結構(method_info Structure) 中的 ACC_SYNCHRONIZED
訪問標誌區分一個方法是否同步方法。當方法調用時,調用指令將會 檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設置,若是設置了,執行線程將先持有monitor(虛擬機規範中用的是管程一詞), 而後再執行方法,最後再方法完成(不管是正常完成仍是非正常完成)時釋放monitor。在方法執行期間,執行線程持有了monitor,其餘任何線程都沒法再得到同一個monitor。若是一個同步方法執行期間拋出了異常,而且在方法內部沒法處理此異常,那這個同步方法所持有的monitor將在異常拋到同步方法以外時自動釋放。
//方法級的同步是隱式, public class SyncMethod { public int i; public synchronized void syncTask(){ i++; } }
## 反編譯之後的字節碼 public synchronized void syncTask(); descriptor: ()V flags: ACC_PUBLIC, ACC_SYNCHRONIZED Code: stack=3, locals=1, args_size=1 0: aload_0 1: dup 2: getfield #2 // Field i:I 5: iconst_1 6: iadd 7: putfield #2 // Field i:I 10: return LineNumberTable: line 9: 0 line 10: 10 LocalVariableTable: Start Length Slot Name Signature 0 11 0 this Lcn/javanode/concurrent/key/synchronizedDesc/SyncMethod; }
從字節碼中能夠看出,<u>synchronized修飾的方法並無monitorenter指令和monitorexit指令,取得代之的確實是ACC_SYNCHRONIZED標識,該標識指明瞭該方法是一個同步方法</u>,JVM經過該ACC_SYNCHRONIZED訪問標誌來辨別一個方法是否聲明爲同步方法,從而執行相應的同步調用。這即是synchronized鎖在同步代碼塊和同步方法上實現的基本原理。同時咱們還必須注意到的是在Java早期版本中,synchronized屬於重量級鎖,效率低下,由於監視器鎖(monitor)是依賴於底層的操做系統的Mutex Lock來實現的,而操做系統實現線程之間的切換時須要從用戶態轉換到核心態,這個狀態之間的轉換須要相對比較長的時間,時間成本相對較高,這也是爲何早期的synchronized效率低的緣由。慶幸的是在Java 6以後Java官方對從JVM層面對synchronized較大優化,因此如今的synchronized鎖效率也優化得很不錯了,Java 6以後,爲了減小得到鎖和釋放鎖所帶來的性能消耗,引入了輕量級鎖和偏向鎖。
鎖的狀態總共有四種,無鎖狀態、偏向鎖、輕量級鎖和重量級鎖。隨着鎖的競爭,鎖能夠從偏向鎖升級到輕量級鎖,再升級的重量級鎖,可是鎖的升級是單向的,也就是說只能從低到高升級,不會出現鎖的降級,
無鎖
能夠看到mark word 裏面此時存了
偏向鎖
在jdk1.6後被提出:在大多數狀況下,鎖並不存在競爭,<u>一把鎖每每是同一個線程得到的,並不須要加鎖和解鎖</u>。所以爲了減小同一線程獲取鎖(會涉及到一些CAS操做,耗時)的代價而引入偏向鎖
。偏向鎖的核心思想是,若是一個線程得到了鎖,那麼鎖就進入偏向模式,此時Mark Word 的結構也變爲偏向鎖結構,當這個線程再次請求鎖時,無需再作任何同步操做,即獲取鎖的過程,這樣就省去了大量有關鎖申請的操做,從而也就提供程序的性能。因此,對於沒有鎖競爭的場合,偏向鎖有很好的優化效果,畢竟極有可能連續屢次是同一個線程申請相同的鎖。可是對於鎖競爭比較激烈的場合,偏向鎖就失效了,由於這樣場合極有可能每次申請鎖的線程都是不相同的,所以這種場合下不該該使用偏向鎖,不然會得不償失,須要注意的是,偏向鎖失敗後,並不會當即膨脹爲重量級鎖,而是先升級爲輕量級鎖。
輕量級鎖
假若偏向鎖失敗,虛擬機並不會當即升級爲重量級鎖,它還會嘗試使用一種稱爲輕量級鎖的優化手段(1.6以後加入的),此時Mark Word 的結構也變爲輕量級鎖的結構。輕量級鎖可以提高程序性能的依據是「對絕大部分的鎖,在整個同步週期內都不存在競爭」,注意這是經驗數據。須要瞭解的是,輕量級鎖所適應的場景是線程交替執行同步塊的場合,若是存在同一時間訪問同一鎖的場合,就會致使輕量級鎖膨脹爲重量級鎖。
自旋鎖
輕量級鎖失敗後,虛擬機爲了不線程真實地在操做系統層面掛起,還會進行一項稱爲自旋鎖的優化手段。這是基於在大多數狀況下,線程持有鎖的時間都不會太長,若是直接掛起操做系統層面的線程可能會得不償失,畢竟操做系統實現線程之間的切換時須要從用戶態轉換到核心態,這個狀態之間的轉換須要相對比較長的時間,時間成本相對較高,所以自旋鎖會假設在不久未來,當前的線程能夠得到鎖,所以虛擬機會讓當前想要獲取鎖的線程作幾個空循環(這也是稱爲自旋的緣由),通常不會過久,多是50個循環或100循環,在通過若干次循環後,若是獲得鎖,就順利進入臨界區。若是還不能得到鎖,那就會將線程在操做系統層面掛起,這就是自旋鎖的優化方式,這種方式確實也是能夠提高效率的。最後沒辦法也就只能升級爲重量級鎖了。
鎖消除
消除鎖是虛擬機另一種鎖的優化,這種優化更完全,<u>Java虛擬機在JIT編譯時(能夠簡單理解爲當某段代碼即將第一次被執行時進行編譯,又稱即時編譯),經過對運行上下文的掃描,去除不可能存在共享資源競爭的鎖,經過這種方式消除沒有必要的鎖</u>,能夠節省毫無心義的請求鎖時間,以下StringBuffer的append是一個同步方法,可是在add方法中的StringBuffer屬於一個局部變量,而且不會被其餘線程所使用,所以StringBuffer不可能存在共享資源競爭的情景,JVM會自動將其鎖消除。
/** * 消除StringBuffer同步鎖 */ public class StringBufferRemoveSync { public void add(String str1, String str2) { //StringBuffer是線程安全,因爲sb只會在append方法中使用,不可能被其餘線程引用 //所以sb屬於不可能共享的資源,JVM會自動消除內部的鎖 StringBuffer sb = new StringBuffer(); sb.append(str1).append(str2); } public static void main(String[] args) { StringBufferRemoveSync rmsync = new StringBufferRemoveSync(); for (int i = 0; i < 10000000; i++) { rmsync.add("abc", "123"); } } }
鎖的優缺點對比
鎖 | 優勢 | 缺點 | 使用場景 |
---|---|---|---|
偏向鎖 | 加鎖和解鎖不須要CAS操做,沒有額外的性能消耗,和執行非同步方法相比僅存在納秒級的差距 | 若是線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗 | 適用於只有一個線程訪問同步快的場景 |
輕量級鎖 | 競爭的線程不會阻塞,提升了響應速度 | 如線程成始終得不到鎖競爭的線程,使用自旋會消耗CPU性能 | 追求響應時間,同步快執行速度很是快 |
重量級鎖 | 線程競爭不適用自旋,不會消耗CPU | 線程阻塞,響應時間緩慢,在多線程下,頻繁的獲取釋放鎖,會帶來巨大的性能消耗 | 追求吞吐量,同步快執行速度較長 |
從互斥鎖的設計上來講,當一個線程試圖操做一個由其餘線程持有的對象鎖的臨界資源時,將會處於阻塞狀態,但當一個線程再次請求本身持有對象鎖的臨界資源時,這種狀況屬於重入鎖,請求將會成功,在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。
簡單地說可見性就是把工做內存中的數據刷入主內存,加載數據。具體到內存屏障
int b = 0; int c = 0; synchronized(this) { -> monitorenter Load內存屏障 Acquire內存屏障 int a = b; c = 1; => synchronized代碼塊裏面仍是可能會發生指令重排 Release內存屏障 } -> monitorexit Store內存屏障
基於synchronized代碼塊字節碼層面上來講:
如上面代碼所示
Acquire屏障
,這個屏障的做用是禁止讀操做和讀寫操做之間發生指令重排序。Release屏障
,這個屏障的做用是禁止寫操做和讀寫操做之間發生重排序。因此說,經過 Acquire屏障和Release屏障,就可讓synchronzied保證有序性,只有synchronized內部的指令能夠重排序,可是絕對 不會跟外部的指令發生重排序。
找了幾個例子,鞏固一下上面學的,看一下能不能想出來執行順序呢!
案例一
public class SynchronizedObjectLock implements Runnable { static SynchronizedObjectLock instence = new SynchronizedObjectLock(); @Override public void run() { // 同步代碼塊形式——鎖爲this,兩個線程使用的鎖是同樣的,線程1必需要等到線程0釋放了該鎖後,才能執行 synchronized (this) { System.out.println("我是線程" + Thread.currentThread().getName()); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "結束"); } } public static void main(String[] args) { Thread t1 = new Thread(instence); Thread t2 = new Thread(instence); t1.start(); t2.start(); } } // 執行結果 /** 我是線程Thread-0 Thread-0結束 我是線程Thread-1 Thread-1結束 **/
案例二
public class SynchronizedObjectLock implements Runnable { static SynchronizedObjectLock instence = new SynchronizedObjectLock(); // 建立2把鎖 Object block1 = new Object(); Object block2 = new Object(); @Override public void run() { // 這個代碼塊使用的是第一把鎖,當他釋放後,後面的代碼塊因爲使用的是第二把鎖,所以能夠立刻執行 synchronized (block1) { System.out.println("block1鎖,我是線程" + Thread.currentThread().getName()); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("block1鎖,"+Thread.currentThread().getName() + "結束"); } synchronized (block2) { System.out.println("block2鎖,我是線程" + Thread.currentThread().getName()); try { Thread.sleep(3000);//sleep方法並不會失去鎖。 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("block2鎖,"+Thread.currentThread().getName() + "結束"); } } public static void main(String[] args) { Thread t1 = new Thread(instence); Thread t2 = new Thread(instence); t1.start(); t2.start(); } } // 執行結果 /** block1鎖,我是線程Thread-0 block1鎖,Thread-0結束 block2鎖,我是線程Thread-0 //能夠看到當第一個線程在執行完第一段同步代碼塊以後,第二個同步代碼塊能夠立刻獲得執行,由於他們使用的鎖不是同一把 block1鎖,我是線程Thread-1 block1鎖,Thread-1結束 block2鎖,Thread-0結束 block2鎖,我是線程Thread-1 block2鎖,Thread-1結束 **/
方法鎖形式:synchronized修飾普通方法,鎖對象默認爲this
//當前線程的鎖即是實例對象 //當一個線程獲取了該對象的鎖以後,其餘線程沒法獲取該對象的鎖,因此沒法訪問該對象的其餘synchronized實例方法 public class SynchronizedObjectLock implements Runnable { static SynchronizedObjectLock instence = new SynchronizedObjectLock(); @Override public void run() { method(); } public synchronized void method() { System.out.println("我是線程" + Thread.currentThread().getName()); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "結束"); } public static void main(String[] args) { Thread t1 = new Thread(instence); Thread t2 = new Thread(instence); t1.start(); t2.start(); } } // 執行結果 /** 我是線程Thread-1 Thread-1結束 我是線程Thread-0 Thread-0結束 **/
方法鎖形式:synchronized修飾普通方法,鎖對象默認爲this
//t1和t2對應的this是兩個不一樣的實例,持有鎖不一樣 普通鎖只是當前實例 public class SynchronizedObjectLock implements Runnable { static SynchronizedObjectLock instence1 = new SynchronizedObjectLock(); static SynchronizedObjectLock instence2 = new SynchronizedObjectLock(); @Override public void run() { method(); } // synchronized用在普通方法上,默認的鎖就是this,當前實例 public synchronized void method() { System.out.println("我是線程" + Thread.currentThread().getName()); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "結束"); } public static void main(String[] args) { // t1和t2對應的this是兩個不一樣的實例,因此代碼不會串行 Thread t1 = new Thread(instence1); Thread t2 = new Thread(instence2); t1.start(); t2.start(); } } // 執行結果 /** 我是線程Thread-0 我是線程Thread-1 Thread-1結束 Thread-0結束 **/
類鎖形式
public class SynchronizedObjectLock implements Runnable { static SynchronizedObjectLock instence1 = new SynchronizedObjectLock(); static SynchronizedObjectLock instence2 = new SynchronizedObjectLock(); @Override public void run() { method(); } // synchronized用在靜態方法上,默認的鎖就是當前所在的Class類,因此不管是哪一個線程訪問它,須要的鎖都只有一把 public static synchronized void method() { System.out.println("我是線程" + Thread.currentThread().getName()); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "結束"); } public static void main(String[] args) { Thread t1 = new Thread(instence1); Thread t2 = new Thread(instence2); t1.start(); t2.start(); } } // 執行結果 /** 我是線程Thread-0 Thread-0結束 我是線程Thread-1 Thread-1結束 **/
同步代碼塊
public class SynchronizedObjectLock implements Runnable { static SynchronizedObjectLock instence1 = new SynchronizedObjectLock(); static SynchronizedObjectLock instence2 = new SynchronizedObjectLock(); @Override public void run() { // 全部線程須要的鎖都是同一把 synchronized(SynchronizedObjectLock.class){ System.out.println("我是線程" + Thread.currentThread().getName()); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "結束"); } } public static void main(String[] args) { Thread t1 = new Thread(instence1); Thread t2 = new Thread(instence2); t1.start(); t2.start(); } } // 執行結果 /** 我是線程Thread-0 Thread-0結束 我是線程Thread-1 Thread-1結束 **/
標註:在學習中須要修改的內容以及筆記全在這裏 www.javanode.cn,謝謝!有任何不妥的地方望糾正