1.synchronized的三種加鎖方式
-
對於普通同步方法,鎖是當前實例對象(對象鎖)
在這種使用方式中,要注意鎖是對象的實例,因為要保證多個線程使用的是同一個實例,否則仍然會有問題。
比如如下代碼,因為每個線程的實例是不同的,因為他們獲取的都不是同一把鎖
public class Demo1 {
static int num = 0;
public synchronized void m1(){
for(int i=0;i<10000;i++){
num++;
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{Demo1 demo1 = new Demo1();demo1.m1();});
Thread t2 = new Thread(()->{Demo1 demo1 = new Demo1();demo1.m1();});
Thread t3 = new Thread(()->{Demo1 demo1 = new Demo1();demo1.m1();});
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println(num);
}
}
要想執行結果正確,就必須保證多個線程的實例是相同的,如下所示:
public class Demo1 {
static int num = 0;
public synchronized void m1(){
for(int i=0;i<10000;i++){
num++;
}
}
public static void main(String[] args) throws InterruptedException {
Demo1 demo1 = new Demo1();
Thread t1 = new Thread(()->demo1.m1());
Thread t2 = new Thread(()->demo1.m1());
Thread t3 = new Thread(()->demo1.m1());
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println(num);
}
}
- 對於靜態同步方法,鎖是當前類Class對象(類鎖)
public class Demo1 {
static int num = 0;
public synchronized static void m1(){
for(int i=0;i<10000;i++){
num++;
}
}
public static class T1 extends Thread{
@Override
public void run() {
Demo1.m1();
}
}
public static void main(String[] args) throws InterruptedException {
T1 t1 = new T1();
T1 t2 = new T1();
T1 t3 = new T1();
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println(num);
}
}
- 對於同步方法塊,鎖是Synchronized括號里配置的對象
1.括號里的對象是class對象
public class Demo1 {
static int num = 0;
public void m1(){
// class對象鎖
synchronized (Demo1.class){
for(int i=0;i<10000;i++){
num++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{Demo1 demo1 = new Demo1();demo1.m1();});
Thread t2 = new Thread(()->{Demo1 demo1 = new Demo1();demo1.m1();});
Thread t3 = new Thread(()->{Demo1 demo1 = new Demo1();demo1.m1();});
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println(num);
}
}
2. 括號里的對象是當前實例
public class Demo1 {
static int num = 0;
public void m1(){
// class對象鎖
synchronized (this){
for(int i=0;i<10000;i++){
num++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Demo1 demo1 = new Demo1();
Thread t1 = new Thread(()->demo1.m1());
Thread t2 = new Thread(()->demo1.m1());
Thread t3 = new Thread(()->demo1.m1());
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println(num);
}
}
2.synchronized的實現原理
從JVM規范中可以看到Synchonized在JVM里的實現原理,JVM基於進入和退出Monitor對象來實現方法同步和代碼塊同步,但兩者的實現細節不一樣。代碼塊同步是使用monitorenter和monitorexit指令實現的,而方法同步是使用另外一種方式實現的,細節在JVM規范里並沒有詳細說明。但是,方法的同步同樣可以使用這兩個指令來實現。
monitorenter指令是在編譯后插入到同步代碼塊的開始位置,而monitorexit是插入到方法結束處和異常處,JVM要保證每個monitorenter必須有對應的monitorexit與之配對。任何對象都有一個monitor與之關聯,當且一個monitor被持有后,它將處於鎖定狀態。線程執行到monitorenter指令時,將會嘗試獲取對象所對應的monitor的所有權,即嘗試獲得對象的鎖。
3.java對象頭
在JVM中,對象在內存中的布局分為三塊區域:對象頭、實例數據和對齊填充。如下:

- 實例變量:存放類的屬性數據信息,包括父類的屬性信息,如果是數組的實例部分還包括數組的長度,這部分內存按4字節對齊。
- 填充數據:由於虛擬機要求對象起始地址必須是8字節的整數倍。填充數據不是必須存在的,僅僅是為了字節對齊,這點了解即可。
- 對象頭:synchronized用的鎖是存在Java對象頭里的。如果對象是數組類型,則虛擬機用3個字寬(Word)存儲對象頭,如果對象是非數組類型,則用2字寬存儲對象頭。在32位虛擬機中,1字寬等於4字節,即32bit

Java對象頭里的Mark Word里默認存儲對象的HashCode、分代年齡和鎖標記位。32位JVM的Mark Word的默認存儲結構如表圖所示:

在運行期間,Mark Word里存儲的數據會隨着鎖標志位的變化而變化。Mark Word可能變化為存儲以下4種數據

在64位虛擬機下,Mark Word是64bit大小的,其存儲結構如下圖所示:

4.鎖的三種狀態
1.偏向鎖
HotSpot的作者經過研究發現,大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價更低而引入了偏向鎖。當一個線程訪問同步塊並獲取鎖時,會在對象頭和棧幀中的鎖記錄里存儲鎖偏向的線程ID,以后該線程在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,只需簡單地測試一下對象頭的Mark Word里是否存儲着指向當前線程的偏向鎖。如果測試成功,表示線程已經獲得了鎖。如果測試失敗,則需要再測試一下Mark Word中偏向鎖的標識是否設置成1(表示當前是偏向鎖):如果沒有設置,則使用CAS競爭鎖;如果設置了,則嘗試使用CAS將對象頭的偏向鎖指向當前線程。
2.輕量級鎖
線程在執行同步塊之前,JVM會先在當前線程的棧楨中創建用於存儲鎖記錄的空間,並將對象頭中的Mark Word復制到鎖記錄中,官方稱為Displaced Mark Word。然后線程嘗試使用CAS將對象頭中的Mark Word替換為指向鎖記錄的指針。如果成功,當前線程獲得鎖,如果失敗,表示其他線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖。
3.重量級鎖
內置鎖在Java中被抽象為監視器鎖(monitor)。在JDK 1.6之前,監視器鎖可以認為直接對應底層操作系統中的互斥量(mutex)。這種同步方式的成本非常高,包括系統調用引起的內核態與用戶態切換、線程阻塞造成的線程切換等。因此,后來稱這種鎖為“重量級鎖”。
鎖的優缺點的對比:

5.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。
6.synchronized與等待喚醒機制
所謂等待喚醒機制本篇主要指的是notify/notifyAll和wait方法,在使用這3個方法時,必須處於synchronized代碼塊或者synchronized方法中,否則就會拋出IllegalMonitorStateException異常,這是因為調用這幾個方法前必須拿到當前對象的監視器monitor對象,也就是說notify/notifyAll和wait方法依賴於monitor對象,在前面的分析中,我們知道monitor 存在於對象頭的Mark Word 中(存儲monitor引用指針),而synchronized關鍵字可以獲取 monitor ,這也就是為什么notify/notifyAll和wait方法必須在synchronized代碼塊或者synchronized方法調用的原因。
synchronized (obj) {
obj.wait();
obj.notify();
obj.notifyAll();
}
需要特別理解的一點是,與sleep方法不同的是wait方法調用完成后,線程將被暫停,但wait方法將會釋放當前持有的監視器鎖(monitor),直到有線程調用notify/notifyAll方法后方能繼續執行,而sleep方法只讓線程休眠並不釋放鎖。同時notify/notifyAll方法調用后,並不會馬上釋放監視器鎖,而是在相應的synchronized(){}/synchronized方法執行結束后才自動釋放鎖。
7.synchronized與中斷機制
事實上線程的中斷操作對於正在等待獲取的鎖對象的synchronized方法或者代碼塊並不起作用,也就是對於synchronized來說,如果一個線程在等待鎖,那么結果只有兩種,要么它獲得這把鎖繼續執行,要么它就保存等待,即使調用中斷線程的方法,也不會生效。演示代碼如下:
public class SynchronizedBlocked implements Runnable{
public synchronized void f() {
System.out.println("Trying to call f()");
while(true) // Never releases lock
Thread.yield();
}
/**
* 在構造器中創建新線程並啟動獲取對象鎖
*/
public SynchronizedBlocked() {
//該線程已持有當前實例鎖
new Thread() {
public void run() {
f(); // Lock acquired by this thread
}
}.start();
}
public void run() {
//中斷判斷
while (true) {
if (Thread.interrupted()) {
System.out.println("中斷線程!!");
break;
} else {
f();
}
}
}
public static void main(String[] args) throws InterruptedException {
SynchronizedBlocked sync = new SynchronizedBlocked();
Thread t = new Thread(sync);
//啟動后調用f()方法,無法獲取當前實例鎖處於等待狀態
t.start();
TimeUnit.SECONDS.sleep(1);
//中斷線程,無法生效
t.interrupt();
}
}
以上內容為自己學習時所記錄的筆記,主要來源於以下資料:
1.java並發編程的藝術
2.http://www.itsoku.com/article/168
