線程A釋放鎖後,會將共享變動操做刷新到主內存中java
線程B獲取鎖時,JMM會將該線程的本地內存置爲無效,被監視器保護的臨界區代碼必須從主內存中讀取共享變量數組
/** * 先定義一個測試模板類 * 這裏補充一個知識點:Thread.sleep(long)不會釋放鎖 * 讀者可參見筆者的`併發番@Thread一文通` */
public class SynchronizedDemo {
public static synchronized void staticMethod(){
System.out.println(Thread.currentThread().getName() + "訪問了靜態同步方法staticMethod");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "結束訪問靜態同步方法staticMethod");
}
public static void staticMethod2(){
System.out.println(Thread.currentThread().getName() + "訪問了靜態同步方法staticMethod2");
synchronized (SynchronizedDemo.class){
System.out.println(Thread.currentThread().getName() + "在staticMethod2方法中獲取了SynchronizedDemo.class");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void synMethod(){
System.out.println(Thread.currentThread().getName() + "訪問了同步方法synMethod");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "結束訪問同步方法synMethod");
}
public synchronized void synMethod2(){
System.out.println(Thread.currentThread().getName() + "訪問了同步方法synMethod2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "結束訪問同步方法synMethod2");
}
public void method(){
System.out.println(Thread.currentThread().getName() + "訪問了普通方法method");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "結束訪問普通方法method");
}
private Object lock = new Object();
public void chunkMethod(){
System.out.println(Thread.currentThread().getName() + "訪問了chunkMethod方法");
synchronized (lock){
System.out.println(Thread.currentThread().getName() + "在chunkMethod方法中獲取了lock");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void chunkMethod2(){
System.out.println(Thread.currentThread().getName() + "訪問了chunkMethod2方法");
synchronized (lock){
System.out.println(Thread.currentThread().getName() + "在chunkMethod2方法中獲取了lock");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void chunkMethod3(){
System.out.println(Thread.currentThread().getName() + "訪問了chunkMethod3方法");
//同步代碼塊
synchronized (this){
System.out.println(Thread.currentThread().getName() + "在chunkMethod3方法中獲取了this");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void stringMethod(String lock){
synchronized (lock){
while (true){
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
複製代碼
當一個線程進入同步方法時,其餘線程能夠正常訪問其餘非同步方法緩存
public static void main(String[] args) {
SynchronizedDemo synDemo = new SynchronizedDemo();
Thread thread1 = new Thread(() -> {
//調用普通方法
synDemo.method();
});
Thread thread2 = new Thread(() -> {
//調用同步方法
synDemo.synMethod();
});
thread1.start();
thread2.start();
}
複製代碼
輸出:安全
Thread-1訪問了同步方法synMethod
Thread-0訪問了普通方法method
Thread-0結束訪問普通方法method
Thread-1結束訪問同步方法synMethod
複製代碼
分析:經過結果可知,普通方法和同步方法是非阻塞執行的數據結構
當一個線程執行同步方法時,其餘線程不能訪問任何同步方法多線程
public static void main(String[] args) {
SynchronizedDemo synDemo = new SynchronizedDemo();
Thread thread1 = new Thread(() -> {
synDemo.synMethod();
synDemo.synMethod2();
});
Thread thread2 = new Thread(() -> {
synDemo.synMethod2();
synDemo.synMethod();
});
thread1.start();
thread2.start();
}
複製代碼
輸出:併發
Thread-0訪問了同步方法synMethod
Thread-0結束訪問同步方法synMethod
Thread-0訪問了同步方法synMethod2
Thread-0結束訪問同步方法synMethod2
Thread-1訪問了同步方法synMethod2
Thread-1結束訪問同步方法synMethod2
Thread-1訪問了同步方法synMethod
Thread-1結束訪問同步方法synMethod
複製代碼
分析:經過結果可知,任務的執行是阻塞的,顯然Thread-1必須等待Thread-0執行完畢以後才能繼續執行app
當同步代碼塊都是同一個鎖時,方法能夠被全部線程訪問,但同一個鎖的同步代碼塊同一時刻只能被一個線程訪問ide
public static void main(String[] args) {
SynchronizedDemo synDemo = new SynchronizedDemo();
Thread thread1 = new Thread(() -> {
//調用同步塊方法
synDemo.chunkMethod();
synDemo.chunkMethod2();
});
Thread thread2 = new Thread(() -> {
//調用同步塊方法
synDemo.chunkMethod();
synDemo.synMethod2();
});
thread1.start();
thread2.start();
}
複製代碼
輸出:工具
Thread-0訪問了chunkMethod方法
Thread-1訪問了chunkMethod方法
Thread-0在chunkMethod方法中獲取了lock
...停頓等待...
Thread-1在chunkMethod方法中獲取了lock
...停頓等待...
Thread-0訪問了chunkMethod2方法
Thread-0在chunkMethod2方法中獲取了lock
...停頓等待...
Thread-1訪問了chunkMethod2方法
Thread-1在chunkMethod2方法中獲取了lock
複製代碼
分析可知:
線程間同時訪問同一個鎖多個同步代碼的執行順序不定,即便是使用同一個對象鎖
public static void main(String[] args) {
SynchronizedDemo synDemo = new SynchronizedDemo();
Thread thread1 = new Thread(() -> {
//調用同步塊方法
synDemo.chunkMethod();
synDemo.chunkMethod2();
});
Thread thread2 = new Thread(() -> {
//調用同步塊方法
synDemo.chunkMethod2();
synDemo.chunkMethod();
});
thread1.start();
thread2.start();
}
---------------------
//輸出:
Thread-0訪問了chunkMethod方法
Thread-1訪問了chunkMethod2方法
Thread-0在chunkMethod方法中獲取了lock
...停頓等待...
Thread-0訪問了chunkMethod2方法
Thread-1在chunkMethod2方法中獲取了lock
...停頓等待...
Thread-1訪問了chunkMethod方法
Thread-0在chunkMethod2方法中獲取了lock
...停頓等待...
Thread-1在chunkMethod方法中獲取了lock
//分析可知:
//現象:對比20行、22行和24行、25行可知,雖然是同一個lock對象,但其不一樣代碼塊的訪問是非阻塞的
//緣由:根源在於鎖的釋放和從新競爭,當Thread-0訪問完chunkMethod方法後會先釋放鎖,這時Thread-1就有機會能獲取到鎖從而優先執行,依次類推到24行、25行時,Thread-0又從新獲取到鎖優先執行了
//注意:但有一點是必須的,對於同一個鎖的同步代碼塊的訪問必定是阻塞的
//補充:同步方法之因此會被所有阻塞,是由於synDemo對象一直被線程在內部把持住就沒釋放過
複製代碼
因爲三種使用方式的鎖對象都不同,所以相互之間不會有任何影響但有兩種狀況除外:
public static void main(String[] args) {
SynchronizedDemo synDemo = new SynchronizedDemo();
Thread thread1 = new Thread(() -> synDemo.chunkMethod() );
Thread thread2 = new Thread(() -> synDemo.chunkMethod3());
Thread thread3 = new Thread(() -> staticMethod());
Thread thread4 = new Thread(() -> staticMethod2());
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
---------------------
//輸出:
Thread-1訪問了chunkMethod3方法
Thread-1在chunkMethod3方法中獲取了this
Thread-2訪問了靜態同步方法staticMethod
Thread-0訪問了chunkMethod方法
Thread-0在chunkMethod方法中獲取了lock
Thread-3訪問了靜態同步方法staticMethod2
...停頓等待...
Thread-2結束訪問靜態同步方法staticMethod
Thread-3在staticMethod2方法中獲取了SynchronizedDemo.class
//分析可知:
//現象:雖然是同一個lock對象,但其不一樣代碼塊的訪問是非阻塞的
//緣由:根源在於鎖的釋放和從新競爭,當Thread-0訪問完chunkMethod方法後會先釋放鎖,這時Thread-1就有機會能獲取到鎖從而優先執行,,Thread-0又從新獲取到鎖優先執行了
複製代碼
重入鎖:當一個線程再次請求本身持有對象鎖的臨界資源時,這種狀況屬於重入鎖,請求將會成功實現:一個線程獲得一個對象鎖後再次請求該對象鎖,是容許的,每重入一次,monitor進入次數+1
public static void main(String[] args) {
SynchronizedDemo synDemo = new SynchronizedDemo();
Thread thread1 = new Thread(() -> {
synDemo.synMethod();
synDemo.synMethod2();
});
Thread thread2 = new Thread(() -> {
synDemo.synMethod2();
synDemo.synMethod();
});
thread1.start();
thread2.start();
}
---------------------
//輸出:
Thread-0訪問了同步方法synMethod
Thread-0結束訪問同步方法synMethod
Thread-0訪問了同步方法synMethod2
Thread-0結束訪問同步方法synMethod2
Thread-1訪問了同步方法synMethod2
Thread-1結束訪問同步方法synMethod2
Thread-1訪問了同步方法synMethod
Thread-1結束訪問同步方法synMethod
//分析:在代碼塊中繼續調用了當前實例對象的另一個同步方法,再次請求當前實例鎖時,將被容許,進而執行方法體代碼,這就是重入鎖最直接的體現
複製代碼
隱患:因爲在JVM中具備String常量池緩存的功能,所以相同字面量是同一個鎖!!!注意:嚴重不推薦將String做爲鎖對象,而應該改用其餘非緩存對象提示:對字面量有疑問的話請先回顧一下String的基礎
public static void main(String[] args) {
SynchronizedDemo synDemo = new SynchronizedDemo();
Thread thread1 = new Thread(() -> synDemo.stringMethod("sally"));
Thread thread2 = new Thread(() -> synDemo.stringMethod("sally"));
thread1.start();
thread2.start();
}
---------------------
//輸出:
Thread-0
Thread-0
Thread-0
Thread-0
...死循環...
//分析:輸出結果永遠都是Thread-0的死循環,也就是說另外一個線程,即Thread-1線程根本不會運行
//緣由:同步塊中的鎖是同一個字面量
複製代碼
隱患:當使用不可變類對象(finalClass)做爲對象鎖時,使用synchronized一樣會有併發問題緣由:因爲不可變特性,看成爲鎖但同步塊內部仍然有計算操做,會生成一個新的鎖對象注意:嚴重不推薦將final Class做爲鎖對象時仍對其有計算操做補充:雖然String也是final Class,但它的緣由倒是字面量常量池
public class SynchronizedDemo {
static Integer i = 0; //Integer是final Class
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new Runnable() {
@Override
public void run() {
for (int j = 0;j<10000;j++){
synchronized (i){
i++;
}
}
}
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(i);
}
}
---------------------
//輸出:
14134
//分析:跟預想中的20000不一致,當使用Integer做爲對象鎖時但還有計算操做就會出現併發問題
複製代碼咱們經過反編譯發現執行i++操做至關於執行了i = Integer.valueOf(i.intValue()+1)經過查看Integer的valueOf方法實現可知,其每次都new了一個新的Integer對象,鎖變了有木有!!!
複製代碼
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i); //每次都new一個新的鎖有木有!!!
}
複製代碼
死鎖:當線程間須要相互等待對方已持有的鎖時,就造成死鎖,進而產生死循環
public static void main(String[] args) {
Object lock = new Object();
Object lock2 = new Object();
Thread thread1 = new Thread(() -> {
synchronized (lock){
System.out.println(Thread.currentThread().getName() + "獲取到lock鎖");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2){
System.out.println(Thread.currentThread().getName() + "獲取到lock2鎖");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (lock2){
System.out.println(Thread.currentThread().getName() + "獲取到lock2鎖");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock){
System.out.println(Thread.currentThread().getName() + "獲取到lock鎖");
}
}
});
thread1.start();
thread2.start();
}
---------------------
//輸出:
Thread-1獲取到lock2鎖
Thread-0獲取到lock鎖
.....
//分析:線程0得到lock鎖,線程1得到lock2鎖,但以後因爲兩個線程還要獲取對方已持有的鎖,但已持有的鎖都不會被雙方釋放,線程"假死",沒法往下執行,從而造成死循環,即死鎖,以後一直在作無用的死循環,嚴重浪費系統資源
複製代碼
咱們用 jstack 查看一下這個任務的各個線程運行狀況,能夠發現兩個線程都被阻塞 BLOCKED
咱們很明顯的發現,Java-level=deadlock,即死鎖,兩個線程相互等待對方的鎖
爲了能直觀瞭解Synchronized的工做原理,咱們經過反編譯SynchronizedDeme類的class文件的方式看看都發生了什麼
public class SynchronizedDemo {
public static synchronized void staticMethod() throws InterruptedException {
System.out.println("靜態同步方法開始");
Thread.sleep(1000);
System.out.println("靜態同步方法結束");
}
public synchronized void method() throws InterruptedException {
System.out.println("實例同步方法開始");
Thread.sleep(1000);
System.out.println("實例同步方法結束");
}
public synchronized void method2() throws InterruptedException {
System.out.println("實例同步方法2開始");
Thread.sleep(3000);
System.out.println("實例同步方法2結束");
}
public static void main(String[] args) {
final SynchronizedDemo synDemo = new SynchronizedDemo();
Thread thread1 = new Thread(() -> {
try {
synDemo.method();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread thread2 = new Thread(() -> {
try {
synDemo.method2();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread1.start();
thread2.start();
}
}
複製代碼
javac -encoding UTF-8 SynchronizedDemo.java
最終咱們將獲得一個 .class 文件,即 SynchronizedDemo.class
javap -v SynchronizedDemo
複製代碼經過反編譯咱們會獲得常量池、同步方法、同步代碼塊的不一樣編譯結果
常量池除了會包含基本類型和字符串及數組的常量值外,還包含以文本形式出現的符號引用:
類和接口的全限定名
字段的名稱和描述符
方法和名稱和描述符
複製代碼
同步方法會包含一個ACC_SYNCHCRONIZED標記符
同步代碼塊會在代碼中插入 monitorenter 和 monitorexist 指令
每一個對象都有一個監視器。當該監視器被佔用時便是鎖定狀態(或者說獲取監視器便是得到同步鎖)。線程執行monitorenter指令時會嘗試獲取監視器的全部權,過程以下:
執行monitorexit指令將遵循如下步驟:
因爲 wait/notify 等方法底層實現是基於監視器,所以只有在同步方法(塊)中才能調用wait/notify等方法,不然會拋出 java.lang.IllegalMonitorStateException 的異常的緣由
區別於同步代碼塊的監視器實現,同步方法經過使用 ACC_SYNCHRONIZED 標記符隱示的實現原理是經過方法調用指令檢查該方法在常量池中是否包含 ACC_SYNCHRONIZED 標記符,若是有,JVM 要求線程在調用以前請求鎖
Monitor實際上是一種同步工具,也能夠說是一種同步機制,它一般被描述爲一個對象,主要特色是互斥和信號機制
在 Monitor Object 模式中,主要有四種類型的參與者:
同步方法的調用和串行化:
同步方法線程掛起:
監視條件通知:
同步方法線程恢復:
在JVM中,對象在內存中的佈局分紅三塊區域:對象頭、示例數據和對齊填充
對象頭: 對象頭主要存儲 Mark Word(對象的hashCode、鎖信息)、類型指針、數組長度(如果數組的話)等信息
示例數據:存放類的屬性數據信息,包括父類的屬性信息,若是是數組的實例部分還包括數組長度,這部份內存按4字節對齊
填充數據:因爲JVM要求對象起始地址必須是8字節的整數倍,當不知足8字節時會自動填充(所以填充數據並非必須的,僅僅是爲了字節對齊)
synchcronized的鎖是存放在Java對象頭中的
若是對象是數組類型,JVM用3個字寬(Word)存儲對象頭,不然是用2個子寬在32位虛擬機中,1字寬等於4個字節,即32bit;64位的話就是8個字節,即64bit
32位JVM的Mark Word的默認存儲結構(無鎖狀態)
在運行期間,Mark Word裏存儲的數據會隨着鎖標誌位的變化而變化(32位)
64位JVM的Mark Word的默認存儲結構(對於32位無鎖狀態,有25bit沒有使用)
公平鎖就是得到鎖的順序按照先到先得的原則,從實現上說,要求當一個線程競爭某個對象鎖時,只要這個鎖的等待隊列非空,就必須把這個線程阻塞並塞入隊尾(插入隊尾通常經過一個CAS操做保持插入過程當中沒有鎖釋放)
相對的,非公平鎖場景下,每一個線程都先要競爭鎖,在競爭失敗或當前已被加鎖的前提下才會被塞入等待隊列,在這種實現下,後到的線程有可能無需進入等待隊列直接競爭到鎖(隨機性)
/** * StringBuffer是線程安全的字符串處理類 * 每次調用stringBuffer.append方法都須要加鎖和解鎖,若是虛擬機檢測到有一系列連串的對同一個對象加鎖和解鎖操做,就會將其合併成一次範圍更大的加鎖和解鎖操做,即在第一次append方法時進行加鎖,最後一次append方法結束後進行解鎖 */
StringBuffer stringBuffer = new StringBuffer();
public void append(){
stringBuffer.append("kira");
stringBuffer.append("sally");
stringBuffer.append("mengmeng");
}
複製代碼
/** * 好比執行10000次字符串的拼接 */
public static void main(String[] args) {
SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
for (int i = 0 ; i < 10000 ; i++){
synchronizedDemo.append("kira","sally");
}
}
public void append(String str1,String str2){
//因爲StringBuffer對象被封裝在方法內部,不可能存在共享資源競爭的狀況
//所以JVM會認爲該加鎖是無心義的,會在編譯期就刪除相關的加鎖操做
//還有一點特別要註明:明知道不會有線程安全問題,代碼階段就應該使用StringBuilder
//不然在沒有開啓鎖消除的狀況下,StringBuffer不會被優化,性能可能只有StringBuilder的1/3
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(str1).append(str2);
}/** 複製代碼
從JDK1.6開始,鎖一共有四種狀態:無鎖狀態、偏向鎖狀態、輕量鎖狀態、重量鎖狀態
鎖的狀態會隨着競爭狀況逐漸升級,鎖容許升級但不容許降級
不容許降級的目的是提升得到鎖和釋放鎖的效率
後面會經過倒序的方式,即重量級鎖->輕量級鎖->偏向鎖進行講解,由於一般後者是前者的優化
鎖的升級過程
線程1和線程2同時爭奪鎖,並致使鎖膨脹成重量級鎖
隱患:對於輕量級鎖有個使用前提是"沒有多線程競爭環境",一旦越過這個前提,除了互斥開銷外,還會增長額外的CAS操做的開銷,在多線程競爭環境下,輕量級鎖甚至比重量級鎖還要慢
偏向鎖使用一種等到競爭出現才釋放鎖的機制,只有當其餘線程競爭鎖時,持有偏向鎖的線程纔會釋放鎖
偏向鎖的撤銷須要等待全局安全點(該時間點上沒有字節碼正在執行)
偏向鎖的撤銷須要遵循如下步驟: -
要麼從新偏向於其餘線程(即將偏向鎖交給其餘線程,至關於當前線程"被"釋放了鎖)
要麼恢復到無鎖或者標記鎖對象不適合做爲偏向鎖(此時鎖會被升級爲輕量級鎖)
最後喚醒暫停的線程,被阻塞在安全點的線程繼續往下執行同步代碼塊