最近遇到多線程處理的問題,原來只使用過synchronized的方法鎖,對於其中的對象鎖和類鎖了解,但是沒仔細研究過。所以回去查了相關資料進行整理。
基礎知識
首先介紹一下對象鎖(也叫方法鎖)與類鎖有那些不同。下文中使用對象鎖稱呼代替方法鎖。
對於對象鎖,是針對一個對象的,它只在該對象的某個內存位置聲明一個標志位標識該對象是否擁有鎖,所以它只會鎖住當前的對象。一般一個對象鎖是對一個非靜態成員變量進行syncronized修飾,或者對一個非靜態方法進行syncronized修飾。對於對象鎖,不同對象訪問同一個被syncronized修飾的方法的時候不會阻塞住。
類鎖是鎖住整個類的,當有多個線程來聲明這個類的對象的時候將會被阻塞,直到擁有這個類鎖的對象被銷毀或者主動釋放了類鎖。這個時候在被阻塞住的線程被挑選出一個占有該類鎖,聲明該類的對象。其他線程繼續被阻塞住。
無論是類鎖還是對象鎖,父類和子類之間是否阻塞沒有直接關系。當對一個父類加了類鎖,子類是不會受到影響的,相反也是如此。因為synchronized關鍵字並不是方法簽名的一部分,它是對方法進行修飾的。當子類覆寫父類中的同步方法或是接口中聲明的同步方法的時候,synchronized修飾符是不會被自動繼承的,所以相應的阻塞問題不會出現。
注意:這裡的阻塞問題是指的按照正常情況下應該阻塞,而因為synchronized是父類與子類之間不可傳遞導致不會阻塞。那正常情況下阻塞是什麼那,下面會詳細介紹。但是,當一個子類沒有覆蓋父類的方法的時候,這時候通過子類訪問方法則會產生阻塞。
插入一句:構造方法不可能是真正同步的(盡管可以在構造方法中使用同步塊)。
下面截圖給出了如何聲明一個對象鎖和如何聲明一個類鎖:
代碼測試
如下面代碼,我聲明了一個類Runtest,在該類中包含無鎖方法noSyn、對象鎖方法 outMethod與一個類鎖方法plus。聲明了一個繼承了線程Thread的類Obj,在該類中用來訪問Runtest的方法,模擬各種測試場景。啟動測試類是MutiThread,在該類中有兩種測試方法,一種是聲明同一個測試類對象而開辟多個線程,用來測試對象鎖;另外一種是每當聲明一個新的線程則同時聲明一個新的測試類對象,用來測試類鎖。
具體測試流程分為兩個步驟。第一個步驟是直接運行如下代碼,測試結果是用來測試對象鎖的鎖效果;第二個步驟是把for循環中的前兩行代碼注釋掉,把其余三行有注釋的代碼刪去注釋,還有,類Obj最後一行注釋代碼刪去注釋,用來測試類鎖的效果。
public class MutiThread {
public static void main(String[] args) {
Runtest runtest = new Runtest();
for (int i = 0; i < 10; i++) {
Thread thread = new Obj(runtest, i);// 1同一個RunTest1對象但每次有個新的線程
thread.start();
// Runtest rruntest = new Runtest(); //2 循環每次都聲明一個新的對象
// Thread thread = new Obj(rruntest, i);
// thread.start();
}
}
}
class Obj extends Thread {
Runtest r;
int i = 0;
public Obj(Runtest r, int i) {
this.r = r;
this.i = i;
}
public void run() {
r.noSyn(this.getId());
//用以測試同一個對象在不同線程中訪問不同方法
if(i % 2 == 0){
r.outMethod2();//對象鎖方法2
}else{
r.outMethod();//對象鎖方法1
}
//Runtest.plus(); //類鎖方法
}
}
class Runtest {
static int i = 0;
public void noSyn(long threadId) {
System.out.println("nosyn: class obj->" + this + ", threadId->" + threadId);
}
synchronized public void outMethod() {
System.out.println("in outMethod begin");
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
}
System.out.println("in outMethod end");
}
synchronized public void outMethod2() {
System.out.println("in outMethod2 begin");
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
}
System.out.println("in outMethod2 end");
}
public static void plus() {
synchronized (Runtest.class) {
System.out.println("start: " + i);
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
}
i++;
System.out.println("i is " + i);
}
}
}
執行結果
按照上述測試步驟, 對於類鎖的測試結果如下:
第一部分輸出:
nosyn: class obj->test.Runtest@3851ddd2, threadId->10
in outMethod2 begin
nosyn: class obj->test.Runtest@1722fe15, threadId->13
in outMethod begin
nosyn: class obj->test.Runtest@6e1b0caf, threadId->15
nosyn: class obj->test.Runtest@31ddeda2, threadId->16
in outMethod2 begin
nosyn: class obj->test.Runtest@5be9d36, threadId->14
in outMethod2 begin
in outMethod begin
nosyn: class obj->test.Runtest@1624bef5, threadId->11
in outMethod begin
nosyn: class obj->test.Runtest@1f92ee25, threadId->18
in outMethod2 begin
nosyn: class obj->test.Runtest@2543472c, threadId->17
in outMethod begin
nosyn: class obj->test.Runtest@6750cf54, threadId->19
in outMethod begin
nosyn: class obj->test.Runtest@8afcd0c, threadId->12
in outMethod2 begin
/**************************上述輸出幾乎同時*********/
第二部分輸出:
in outMethod2 end
in outMethod end
in outMethod end
in outMethod2 end
in outMethod end
in outMethod end
in outMethod2 end
in outMethod end
in outMethod2 end
in outMethod2 end
/**************************sleep一段時間*********/
第三部分輸出
start: 0
i is 1
start: 1
i is 2
start: 2
i is 3
start: 3
i is 4
start: 4
i is 5
start: 5
i is 6
start: 6
i is 7
start: 7
i is 8
start: 8
i is 9
start: 9
i is 10
執行代碼之後分析如下,這裡也會解釋為何執行代碼導致阻塞:
1:它會首先執行沒加鎖的方法,無論是一個對象多個線程還是每個線程中一個對象,對無鎖方法都是沒有影響的。對於對象鎖和類鎖來說,只會對加了鎖的方法產生不同的影響。
2:當多個對象對同一個加了對象鎖的方法進行調用則會被阻塞,而不同對象對不同方法訪問則不會被阻塞,就算加了對象鎖。當同一個對象在線程1中訪問一個方法,在線程2中再去訪問另外一個加鎖方法,則同樣也會被阻塞。針對上面代碼就是,在線程1中對象runTest訪問outMethod,而在線程2中訪問outMethod2則會被阻塞。
3:對於類鎖,則會把整個類鎖住,也就說只能有一個對象擁有當前類的鎖。當一個對象擁有了類鎖之後,另外一個對象還想競爭鎖的話則會被阻塞。兩個對象A,B,如果A正在訪問一個被類鎖修飾的方法function,那麼B則不能訪問。因為類鎖只能在同一時刻被一個對象擁有。相對於對象鎖,則是不同。還是A,B兩個對象,如果A正在訪問對象鎖修飾的function,那麼這個時候B也可以同時訪問。
對於類鎖的輸出進行分析,它的輸出我表示成三個部分:
在第一部分輸出幾乎同時輸出,是因為每個線程都是一個新的對象,不同對象訪問對象鎖是不會被阻塞的,所以幾乎是按照程序的先後輸出;
第二部分輸出就是兩個方法中的sleep時間消耗,沒有什麼問題;
第三部分就是計算i++,然後輸出結果,這部分輸出是比較慢的。因為plus方法是類鎖,在同一時刻只能是一個對象擁有該鎖,所以多個線程必須順序執行結果,所以最後i的輸出也是10.
其中對於對象鎖,當一個對象擁有鎖之後,訪問一個加了對象鎖的方法,而該方法中又調用了該類中其他加了對象鎖的方法,那麼這個時候是不會阻塞住的。這是java通過可重入鎖機制實現的。可重入鎖指的是當一個對象擁有對象鎖之後,可以重復獲取該鎖。因為synchronized塊是可重入的,所以當你訪問一個對象鎖的方法的時候,在該方法中繼續訪問其他對象鎖方法是不會被阻塞的。