Java多執行緒 - 執行緒同步 synchronized
synchronized是Java同步的基礎,只有徹底瞭解synchronized的物件鎖、可重入鎖...特性,將來在使用高階的ReentrantLock、ReentrantReadWriteLock才能得心應手
本文將透過大量的具體例項來了解synchronized的特性
-
具體例項
-
假設現在有一個MyObject物件myObject,他有methodA()和methodB()方法,創造兩個執行緒Thread1 和 Thread2 ,使他們都去執行這個myObject物件的methodA方法,程式碼如下
class MyObject { public void methodA() { System.out.println("begin method A, 誰執行我: " + Thread.currentThread().getName()); System.out.println("end"); } public void methodB() { System.out.println("begin method B, 誰執行我: " + Thread.currentThread().getName()); System.out.println("end"); } } class Thread1 extends Thread { private MyObject object; Thread1(MyObject object) { this.object = object; } @Override public void run() { object.methodA(); // thread 1 執行 myObject 物件的 methodA() 方法 } } class Thread2 extends Thread { private MyObject object; Thread2(MyObject object) { this.object = object; } @Override public void run() { object.methodA(); // thread 2 也執行 myObject 物件的 methodA() 方法 } } public class Main { public static void main(String[] args) { MyObject object = new MyObject(); Thread1 th1 = new Thread1(object); th1.setName("thread 1"); Thread2 th2 = new Thread2(object); th2.setName("thread 2"); th1.start(); th2.start(); } }
-
因為methodA()方法上沒有加上
synchronized
關鍵字進行執行緒同步,所以thread1和thread2會爭搶著執行methodA(),所以結果如下begin methodA, 誰執行我: thread 1 begin methodA, 誰執行我: thread 2 end end
-
這時如果在methodA上加上
synchronized
關鍵字,thread1和thread2會一個一個排隊執行methodA,結果如下class MyObject { synchronized public void methodA() { ... } public void methodB() { ... } }
begin method A, 誰執行我: thread 1 end begin method A, 誰執行我: thread 2 end
-
這時如果再新增一個物件MyObject物件myObject2,讓Thread2改為執行myObject2物件的mehodA()方法,則結果如下
-
會使得結果又變為不同步的原因是因為
synchronized
其實是一個物件鎖,而不是程式碼鎖 -
意思是指假設當Thread1開始執行myObject的methodA()時,實際上是鎖住了myObject這個物件的methodA()方法,而不是鎖住了class MyObject們的methodA()方法
-
所以在jvm中,若是建立了多個例項物件,則會產生多把鎖,每個物件自己一把鎖,物件間的鎖彼此不干擾
-
而在這個例子中,thread1取得的是myObject的物件鎖,而thread2取得的是myObject2的物件鎖,因為他們不是使用同一把鎖,所以才會使得執行緒不同步
class MyObject { synchronized public void methodA() { ... } public void methodB() { ... } } class Thread1 extends Thread { public void run(){ object.methodA(); //thread 1 執行的是 myObject 物件的methodA()方法 } } class Thread2 extends Thread { public void run(){ object.methodA(); //thread 2 執行的是 myObject2 物件的methodA()方法 } }
begin method A, 誰執行我: thread 1 begin method A, 誰執行我: thread 2 end end
-
-
如果將myObject2刪了,改回Thread1和Thread2都使用myObject這個物件,並且改成Thread1執行methodA(),Thread2執行methodB(),注意methodA有
synchronized
,但methodB沒有,其結果如下-
會使得執行緒又不同步的原因是因為methodB是一個非同步的方法,即是methodB沒有加上
synchronized
關鍵字 -
因此雖然thread1已經先搶到
synchonized
鎖了,但是因為methodB不是一個執行緒同步的方法,因此他根本不care什麼物件鎖,直接就能夠被呼叫了
class MyObject { synchronized public void methodA() { ... } public void methodB() { ... } } class Thread1 extends Thread { public void run(){ object.methodA(); //thread 1 執行myObject物件的 methodA() 方法 } } class Thread2 extends Thread { public void run(){ object.methodB(); //thread 2 執行myObject物件的 methodB() 方法 } }
begin method A, 誰執行我: thread 1 begin method B, 誰執行我: thread 2 end end
-
-
嘗試將methodB()也加上
synchronized
關鍵字,而Thread1仍舊是執行myObject的methodA()方法,Thread2仍舊執行myObject的methodB()方法,結果如下-
Thread1明明呼叫的是methodA,但是為什麼會使得同個物件的其他
synchronized
方法受到影響呢?為什麼會使得結果同步? -
原因是因為
synchronized
是一個物件鎖,所以當Thread1取得鎖開始執行methodA時,他實際上其實是取得了myObject這個物件的synchronized
鎖 -
因此當Thread2想要開始執行methodB時,他會去看myObject這個物件的
synchronized
鎖被取走了沒,而他就會發現已經被Thread1取走了,因此他必須等待Thread1完成後,才能去取得myObject的物件鎖,去執行methodB,所以結果才會是同步的
class MyObject { synchronized public void methodA() { ... } synchronized public void methodB() { ... } } class Thread1 extends Thread { public void run(){ object.methodA(); //thread 1 執行myObject物件的 methodA() 方法 } } class Thread2 extends Thread { public void run(){ object.methodB(); //thread 2 執行myObject物件的 methodB() 方法 } }
begin method A, 誰執行我: thread 1 end begin method B, 誰執行我: thread 2 end
-
-
如果把Thread1和Thread2都改成執行methodA方法,不過將methodA方法改成他還會去呼叫methodB方法,注意methodA和methodB方法都有加上
synchronized
關鍵字,結果如下-
可以看到當Thread1搶到myObject物件的鎖執行methodA到一半,當他想要執行methodB時,他就必須去取得methodB的物件鎖,也就是myObject物件鎖,也就是他當初近來methodA時已經拿到的鎖
-
由於Thread1已經拿到了這個鎖,所以當他再次取得這個物件鎖時,能夠再次取到,這就是鎖重入性
-
這也說明當一個
synchronized
方法methodA的內部呼叫本類的其他synchronized
方法methodB時,是永遠可以得到鎖的,因為當初進入methodA時此執行緒就已經得過物件鎖了
class MyObject { synchronized public void methodA() { System.out.println("begin method A"); methodB(); //methodA去呼叫同個物件的另一個synchronized方法methodB System.out.println("end"); } synchronized public void methodB() { System.out.println("begin method B"); System.out.println("end"); } } class Thread1 extends Thread { public void run(){ object.methodA(); //thread 1 執行myObject物件的 methodA() 方法 } } class Thread2 extends Thread { public void run(){ object.methodA(); //thread 2 執行myObject物件的 methodA() 方法 } }
begin method A, 誰執行我: thread 1 begin method B, 誰執行我: thread 1 end end begin method A, 誰執行我: thread 2 begin method B, 誰執行我: thread 2 end end
-
-
-
根據以上例項,小結
synchronized
關鍵字的特性-
synchronized
是一個物件鎖-
假設thread1執行緒已經持有某object物件的synchronized lock
-
那麼thread2仍然可以呼叫此object物件中的非同步的方法,即是thread2可以直接呼叫那些沒有加上sychronized關鍵字的方法,不需要等待
-
但是thread2如果呼叫了加上synchronized關鍵字的方法,就必須要等thread1釋放lock才行
-
-
synchronized
物件鎖具有鎖重入性-
當一個執行緒取得某object物件的物件鎖時,再次請求此物件鎖是可以再次得到該物件鎖的
-
也就是說,當一個
synchronized
方法methodA的內部呼叫本類的其他synchronized
方法methodB時,是永遠可以得到鎖的,因為當初進入methodA時此執行緒就已經得過物件鎖了
-
-
當執行緒在執行
synchronized
方法時出現異常,物件鎖會自動釋放 -
同步不具有繼承性
-
假設Parent類中有一個
synchronized public void hello()
方法,那麼繼承到Child類中時此方法會變成public void hello()
-
如果Child也想要讓此方法變成同步方法,那麼必須手動加上
synchronized
關鍵字才行
-
-
-
具體例項二
-
到剛才為止,我們都是將
synchronized
修飾在方法上,但是直接修飾方法可能會使得critical section範圍設定過大,而真正要同步的區塊其實沒有這麼大,因此synchronized
還支援修飾程式碼塊,或是修飾一個變數-
使用
synchronized (this)
可以同步某一段程式碼塊,而這個方式也是一個物件鎖,此例就是會鎖住myObject -
要注意只有在
synchronized (this)
裡的程式碼是同步的,其他地方都是非同步的-
所以當thread1執行到methodA的運作很久的方法時,因為他沒有被synchronized修飾,所以是非同步的,所以他不會去管什麼鎖,直接就能執行了
-
但是當thread1執行到
synchronized (this)
時,他會想要去取得myObject的物件鎖,而他會發現已經被thread2的methodB取走了,所以他要等thread2釋放物件鎖之後,才能開始執行synchronized (this)
裡面的程式碼塊
-
class MyObject { public void methodA() { System.out.println("method A 運作很久的一個方法"); //這是非同步的 synchronized (this) { System.out.println("begin method A"); System.out.println("end"); } } synchronized public void methodB() { System.out.println("begin method B"); System.out.println("end"); } } class Thread1 extends Thread { public void run(){ object.methodA(); //thread 1 執行myObject物件的 methodA() 方法 } } class Thread2 extends Thread { public void run(){ object.methodB(); //thread 2 執行myObject物件的 methodB() 方法 } }
method A 運作很久的一個方法 begin method B end begin method A end
-
-
接續剛才的例子,但是將
sychronized
改成用來修飾一個變數,methodA去同步myObject物件裡的變數x,methodB去同步myObject物件裡的變數y-
可以看到結果是不同步的,原因是因為當
synchonized
修飾變數時,他的鎖會變成這個變數物件的鎖,而不是myObject這個物件的鎖了 -
因此methodA取得的是myObject中的x的這個物件鎖,而methodB取得的是myObject中的y的物件鎖,因為他們拿的是兩個不同的鎖,所以他們才不會被同步
class MyObject { private String x = new String(); private String y = new String(); public void methodA() { synchronized (x) { System.out.println("begin method A"); System.out.println("end"); } } public void methodB() { synchronized (y) { System.out.println("begin method B"); System.out.println("end"); } } } class Thread1 extends Thread { public void run(){ object.methodA(); //thread 1 執行myObject物件的 methodA() 方法 } } class Thread2 extends Thread { public void run(){ object.methodB(); //thread 2 執行myObject物件的 methodB() 方法 } }
begin method A begin method B end end
-
-
如果再新增一個myObject2,讓thread1執行myObject的methodA,thread2執行myObject2的methodA方法,結果如下
-
結果會是不同步的,原因是因為
synchronized
是一個物件鎖,thread1取得的是myObject中的x的物件鎖,thread2取得的是myObject2中的x物件鎖,是兩個鎖,所以才會不同步
class MyObject { private String x = new String(); public void methodA() { synchronized (x) { System.out.println("begin method A, 誰執行我: " + Thread.currentThread().getName()); System.out.println("end"); } } public void methodB() { ... } } class Thread1 extends Thread { public void run(){ object.methodA(); //thread 1 執行 myObject 物件的 methodA() 方法 } } class Thread2 extends Thread { public void run(){ object.methodA(); //thread 2 執行 myObject2 物件的 methodA() 方法 } }
begin method A, 誰執行我: thread 1 end begin method A, 誰執行我: thread 2 end
-
-
如果將myObject2刪了,改回Thread1和Thread2都使用myObject這個物件,並且改回Thread1執行methodA,Thread2執行methodB,然後methodA去同步一個變數物件
x
,但methodB去同步一個類物件myObject
,結果如下-
結果會是不同步的,是因為他們其實取的鎖是不同的鎖,methodA取得的是myObject中的變數鎖x,而methodB是取得myObject鎖
-
所以可以知道,就算取得了類物件
myObject
的鎖,也不會影響其中變數x
的鎖,也就是說並非取得物件鎖myObject就會把所有他的變數鎖x
也都全部鎖起來,他們之間是獨立的兩個鎖,分別是myObject
和x in myObject
-
所以假設又來了一個新的物件myObject2,那麼鎖就有4把,分別是
myObject
、x in myObject
、myObject2
、x in myObject2
class MyObject { private String x = new String(); public void methodA() { synchronized (x) { System.out.println("begin method A"); System.out.println("end"); } } public void methodB() { synchronized (this) { System.out.println("begin method B"); System.out.println("end"); } } } class Thread1 extends Thread { public void run(){ object.methodA(); //thread 1 執行myObject物件的 methodA() 方法 } } class Thread2 extends Thread { public void run(){ object.methodB(); //thread 2 執行myObject物件的 methodB() 方法 } }
begin method A begin method B end end
-
-
使用
synchronized
修飾String變數時,要小心jvm中的String常量池特性-
jvm中的String常量池快取會導致String指向同個物件,像是a、b預期中應該是兩個
"1"
物件,但是因為String常量池的快取影響,所以a、b儲存的是同個物件,才導致a == b 輸出是truepublic class Test { public static void main() { String a = "1"; String b = "1"; System.out.println(a == b); //輸出true } }
-
將methodA改成使用
synchornized
修飾傳進來的input物件,只有當傳進來的物件一樣,那麼他們取得的鎖才會是同個物件鎖,由於因為String常量池的關係,Thread1和Thread2傳進來的"xx"
會被視為視同個物件,所以才會使得結果是同步的class MyObject { public void methodA(String input) { synchronized (input) { System.out.println("begin method A, 誰執行我: " + Thread.currentThread().getName()); System.out.println("end"); } } public void methodB() { ... } } class Thread1 extends Thread { public void run(){ object.methodA("xx"); //thread 1 執行myObject物件的 methodA() 方法 } } class Thread2 extends Thread { public void run(){ object.methodA("xx"); //thread 2 執行myObject物件的 methodA() 方法 } }
begin method A, 誰執行我: thread 1 end begin method A, 誰執行我: thread 2 end
-
如果傳進來的物件改成別的不是String型別的物件,那麼就不會有因為快取而帶來的這個問題,因此Thread1和Thread2傳進來的物件不會是同一個物件,因此他們取得的鎖也不是同把鎖,所以才會使得結果是不同步的
class MyObject { public void methodA(MyType type) { synchronized (type) { System.out.println("begin method A, 誰執行我: " + Thread.currentThread().getName()); System.out.println("end"); } } public void methodB() { ... } } class Thread1 extends Thread { public void run(){ object.methodA(new MyType()); } } class Thread2 extends Thread { public void run(){ object.methodA(new MyType()); } }
begin method A, 誰執行我: thread 1 begin method A, 誰執行我: thread 2 end end
-
-
synchronized
除了修飾方法、變數、程式碼塊,也可以用來修飾靜態方法static
,如果synchronized
加在static
方法上,那就是加上了一個class鎖,和使用synchronized(MyObject.class)
是一樣的,都是要去取得class鎖-
執行結果是同步的,原因就是因為Thread1和Thread2都想要去取得MyObject的class鎖,所以才會使得結果同步
class MyObject { synchronized public static void methodA() { System.out.println("begin static method A"); System.out.println("end"); } public void methodB() { synchronized (MyObject.class) { System.out.println("begin method B"); System.out.println("end"); } } } class Thread1 extends Thread { @Override public void run() { MyObject.methodA(); //thread 1 執行 MyObject.methodA() 靜態方法 } } class Thread2 extends Thread { @Override public void run() { object.methodB(); //thread 2 執行myObject物件的 methodB() 方法 } }
begin static method A end begin method B end
-
-
假設新增一個物件myObject2,並且改成Thread1執行myObject的methodB,Thread2執行myObject2的methodB,結果如下
-
結果會是同步的,原因是因為class鎖可以對所有物件的起作用,也就是所有這個類的物件們myObject、myObject2...只有一份class鎖MyObject.class,因此不管生成再多物件,他們使用的都是同一個class鎖,所以結果才會是同步的
class MyObject { synchronized public static void methodA() { ... } public void methodB() { synchronized (MyObject.class) { System.out.println("begin method B, 誰執行我: " + Thread.currentThread().getName()); System.out.println("end"); } } } class Thread1 extends Thread { @Override public void run() { object.methodA(); //thread 1 執行 myObject 物件的 methodB() 方法 } } class Thread2 extends Thread { @Override public void run() { object.methodB(); //thread 2 執行 myObject2 物件的 methodB() 方法 } }
begin method B, 誰執行我: thread 1 end begin method B, 誰執行我: thread 2 end
-
-
不過要注意,class鎖、myObject物件鎖、變數x物件鎖,他們彼此之間都是互相獨立的,所以取得了class鎖不代表可以暢行無阻myObject物件鎖和變數x物件鎖
-
把剛剛的myObject2刪掉,讓Thread1執行myObject的methodA,Thread2執行myObject的methodB,Thread3執行myObject的methodC
-
結果是不同步的是因為methodA、methodB、methodC各自用了3種不同的鎖
-
methodA是靜態方法,所以他使用的是class鎖
-
methodB使用的是myObject物件鎖
-
methodC使用的是變數x in myObject物件鎖
-
class MyObject { private String x = new String(); synchronized public static void methodA() { System.out.println("begin static method A"); System.out.println("end"); } synchronized public void methodB() { System.out.println("begin method B"); System.out.println("end"); } public void methodC() { synchronized (x) { System.out.println("begin method C"); System.out.println("end"); } } } class Thread1 extends Thread { @Override public void run() { MyObject.methodA(); //thread 1 執行 MyObject.methodA() 靜態方法 } } class Thread2 extends Thread { @Override public void run() { object.methodB(); //thread 2 執行myObject物件的 methodB() 方法 } } class Thread3 extends Thread { @Override public void run() { object.methodC(); //thread 3 執行myObject物件的 methodC() 方法 } }
begin static method A begin method B begin method C end end end
-
-
-
根據以上例項,總結
synchronized
關鍵字的特性-
synchronized
物件鎖具有鎖重入性-
也就是當一個執行緒取得某object物件的物件鎖時,再次請求此物件鎖是可以再次得到該物件鎖的
-
-
當執行緒在執行
synchronized
方法時出現異常,物件鎖會自動釋放 -
同步不具有繼承性
-
synchronized
不僅可以用在方法上,也能用在變數、程式碼塊、靜態方法上-
class鎖、myObject物件鎖、x變數 in myObject的物件鎖、y變數 in myObject的物件鎖,彼此間都是獨立的,並非取得myObject物件鎖就能夠取得所有他其中的變數的物件鎖,也並非取得class鎖就能取得所有物件myObject、myObject2...的物件鎖
-
class鎖可以對所有物件的起作用,也就是所有這個類的對像們myObject、myObject2...只有一份class鎖
-
-
使用
synchronized
在String變數上時,要注意String的常量池特性 -
就算改了某個物件中的某個屬性,像是myObject.setInfo("123),只要myObject物件不改變,那麼
synchronized
執行的結果仍然會是同步的
-