關鍵字synchronized
synchronized關鍵可以修飾函數、函數內語句。無論它加上方法還是對象上,它取得的鎖都是對象,而不是把一段代碼或是函數當作鎖。
1,當兩個并發線程訪問同一個對象object中的這個synchronized(this)同步代碼塊時,一段時間只能有一個線程得到執行,而另一個線程只有等當前線程執行完以后才能執行這塊代碼。
2,當一個線程訪問object中的一個synchronized(this)同步代碼塊時,其它線程仍可以訪問這個object中是其它非synchronized (this)代碼塊。
3,這里需要注意的是,當一個線程訪問object的一個synchronized(this)代碼塊時,其它線程對這個object中其它synchronized (this)同步代碼塊的訪問將被阻塞。
4,以上所述也適用于其它的同步代碼塊,也就是說,當一個線程訪問object的一個synchronized(this)同步代碼塊時,這個線程就獲得了object的對象鎖。而且每個對象(即類實例)對應著一把鎖,每個synchronized(this)都必須獲得調用該代碼塊兒(可以函數,也可以是變量)的對象的鎖才能執行,否則所屬線程阻塞,方法一旦執行就會獨占該鎖,直到從方法返回時,也釋放這個鎖,重新進入可執行狀態。這種機制確保了同一時刻對于每一個對象,其所有聲明為synchronized的成員函數中至多只有一個處于可執行狀態(因為至多只有一個線程可以獲取該對象的鎖),從而避免了類成員變量的訪問沖突。
synchronized方式的缺點:
由于synchronized鎖定的是調用這個同步方法的對象,也就是說,當一個線程P1在不同的線程中執行這個方法時,它們之間會形成互斥,從而達到同步的效果。但這里需要注意的是,這個對象所性的Class的另一個對象卻可以任意調用這個被加了synchronized關鍵字的方法。同步方法的實質是將synchronized作用于object reference,對于拿到了P1對象鎖的線程才可以調用這個synchronized方法,而對于P2來說,P1與它毫不相干,程序也可能在這種情況下擺脫同步機制的控制,造成數據混亂。以下我們將對這種情況進行詳細地說明:
首先我們先介紹synchronized關鍵字的兩種加鎖對象:對象和類——synchronized可以為資源加對象鎖或是類鎖,類鎖對這個類的所有對象(實例)均起作用,而對象鎖只是針對該類的一個指定的對象加鎖,這個類的其它對象仍然可以使用已經對前一個對象加鎖的synchronized方法。
在這里我們主要討論的一個問題就是:“同一個類,不同實例調用同一個方法,會產生同步問題嗎?”
同步問題只和資源有關系,要看這個資源是不是靜態的。同一個靜態數據,你相同函數分屬不同線程同時對其進行讀寫,CPU也不會產生錯誤,它會保證你代碼的執行邏輯,而這個邏輯是否是你想要的,那就要看你需要什么樣的同步了。即便你兩個不同的代碼,在CPU的不同的兩個core里跑,同時寫一個內存地址,Cache機制也會在L2里先鎖定一個。然后更新,再share給另一個core,也不會出錯,不然intel,amd就白養那么多人了。
因此,只要你沒有兩個代碼共享的同一個資源或變量,就不會出現數據不一致的情況。而且同一個類的不同對象的調用有完全不同的堆棧,它們之間完全不相干。
以下我們以一個售票過程舉例說明,在這里,我們的共享資源就是票的剩余張數。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
package com.test; public class ThreadSafeTest extends Thread implements Runnable { private static int num = 1 ; public ThreadSafeTest(String name) { setName(name); } public void run() { sell(getName()); } private synchronized void sell(String name){ if (num > 0 ) { System. out.println(name + ": 檢測票數大于0" ); System. out.println(name + ": \t正在收款(大約5秒完成)。。。" ); try { Thread. sleep( 5000 ); System. out.println(name + ": \t打印票據,售票完成" ); num--; printNumInfo(); } catch (InterruptedException e) { e.printStackTrace(); } } else { System. out.println(name+ ": 沒有票了,停止售票" ); } } private static void printNumInfo() { System. out.println( "系統:當前票數:" + num); if (num < 0 ) { System. out.println( "警告:票數低于0,出現負數" ); } } public static void main(String args[]) { try { new ThreadSafeTest( "售票員李XX" ).start(); Thread. sleep( 2000 ); new ThreadSafeTest( "售票員王X" ).start(); } catch (InterruptedException e) { e.printStackTrace(); } } } |
運行上述代碼,我們得到的輸出是:
1
2
3
4
5
6
7
8
9
|
售票員李XX: 檢測票數大于0 售票員李XX: 正在收款(大約5秒完成)。。。 售票員王X: 檢測票數大于0 售票員王X: 正在收款(大約5秒完成)。。。 售票員李XX: 打印票據,售票完成 系統:當前票數:0 售票員王X: 打印票據,售票完成 系統:當前票數:-1 警告:票數低于0,出現負數 |
根據輸出結果,我們可以發現,剩余票數為-1,出現了同步錯誤的問題。之所以出現這種情況的原因是,我們建立的兩個實例對象,對共享的靜態資源static int num = 1同時進行了修改。那么我們將上面代碼中方框內的修飾詞static去掉,然后再運行程序,可以得到:
1
2
3
4
5
6
7
8
|
售票員李XX: 檢測票數大于0 售票員李XX: 正在收款(大約5秒完成)。。。 售票員王X: 檢測票數大于0 售票員王X: 正在收款(大約5秒完成)。。。 售票員李XX: 打印票據,售票完成 系統:當前票數:0 售票員王X: 打印票據,售票完成 系統:當前票數:0 |
對程度修改之后,程序運行貌似沒有問題了,每個對象擁有各自不同的堆棧,分別獨立運行。但這樣卻違背了我們希望多線程同時對共享資源的處理(去static后,num就從共享資源變成了每個實例各自擁有的成員變量),這顯然不是我們想要的。
在以上兩種代碼中,采取的主要是對對象的鎖定。由于我之前談到的原因,當一個類的兩個不同的實例對同一共享資源進行修改時,CPU為了保證程序的邏輯會默認這種做法,至于是不是想要的結果,這個只能由程序員自己來決定。因此,我們需要改變鎖的作用范圍,若作用對象只是實例,那么這種問題是無法避免的;只有當鎖的作用范圍是整個類的時候,才可能排除同一個類的不同實例對共享資源同時修改的問題。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
package com.test; public class ThreadSafeTest extends Thread implements Runnable { private static int num = 1 ; public ThreadSafeTest(String name) { setName(name); } public void run() { sell(getName()); } private synchronized static void sell(String name){ if (num > 0 ) { System. out.println(name + ": 檢測票數大于0" ); System. out.println(name + ": \t正在收款(大約5秒完成)。。。" ); try { Thread. sleep( 5000 ); System. out.println(name + ": \t打印票據,售票完成" ); num--; printNumInfo(); } catch (InterruptedException e) { e.printStackTrace(); } } else { System. out.println(name+ ": 沒有票了,停止售票" ); } } private static void printNumInfo() { System. out.println( "系統:當前票數:" + num); if (num < 0 ) { System. out.println( "警告:票數低于0,出現負數" ); } } public static void main(String args[]) { try { new ThreadSafeTest( "售票員李XX" ).start(); Thread. sleep( 2000 ); new ThreadSafeTest( "售票員王X" ).start(); } catch (InterruptedException e) { e.printStackTrace(); } } } |
將程序做如上修改,可以得到運行結果:
1
2
3
4
5
|
售票員李XX: 檢測票數大于0 售票員李XX: 正在收款(大約5秒完成)。。。 售票員李XX: 打印票據,售票完成 系統:當前票數:0 售票員王X: 沒有票了,停止售票 |
對sell()方法加上了static修飾符,這樣就將鎖的作用對象變成了類,當該類的一個實例對共享變量進行操作時將會阻塞這個類的其它實例對其的操作。從而得到我們如期想要的結果。
總結:
1,synchronized關鍵字有兩種用法:synchronized方法和synchronized塊。
2,在Java中不單是類實例,每一個類也可以對應一把鎖
在使用synchronized關鍵字時,有以下幾點兒需要注意:
1,synchronized關鍵字不能被繼承。雖然可以用synchronized來定義方法,但是synchronized卻并不屬于方法定義的一部分,所以synchronized關鍵字并不能被繼承。如果父類中的某個方法使用了synchronized關鍵字,而子類中也覆蓋了這個方法,默認情況下子類中的這個方法并不是同步的,必須顯示的在子類的這個方法中加上synchronized關鍵字才可。當然,也可以在子類中調用父類中相應的方法,這樣雖然子類中的方法并不是同步的,但子類調用了父類中的同步方法,也就相當子類方法也同步了。如,
在子類中加synchronized關鍵字:
1
2
3
4
5
6
|
class Parent { public synchronized void method() { } } class Child extends Parent { public synchronized void method () { } } |
調用父類方法:
1
2
3
4
5
6
|
class Parent { public synchronized void method() { } } class Child extends Parent { public void method() { super .method(); } } |
2,在接口方法定義時不能使用synchronized關鍵字。
3,構造方法不能使用synchronized關鍵字,但可以使用synchronized塊來進行同步。
4,synchronized位置可以自由放置,但是不能放置在方法的返回類型后面。
5,synchronized關鍵字不可以用來同步變量,如下面代碼是錯誤的:
1
2
|
public synchronized int n = 0 ; public static synchronized int n = 0 ; |
6,雖然使用synchronized關鍵字是最安全的同步方法,但若是大量使用也會造成不必要的資源消耗以及性能損失。從表面上看synchronized鎖定的是一個方法,但實際上鎖定的卻是一個類,比如,對于兩個非靜態方法method1()和method2()都使用了synchronized關鍵字,在執行其中的一個方法時,另一個方法是不能執行的。靜態方法和非靜態方法情況類似。但是靜態方法和非靜態方法之間不會相互影響,見如下代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
public class MyThread1 extends Thread { public String methodName ; public static void method(String s) { System. out .println(s); while ( true ); } public synchronized void method1() { method( "非靜態的method1方法" ); } public synchronized void method2() { method( "非靜態的method2方法" ); } public static synchronized void method3() { method( "靜態的method3方法" ); } public static synchronized void method4() { method( "靜態的method4方法" ); } public void run() { try { getClass().getMethod( methodName ).invoke( this ); } catch (Exception e) { } } public static void main(String[] args) throws Exception { MyThread1 myThread1 = new MyThread1(); for ( int i = 1 ; i <= 4 ; i++) { myThread1. methodName = "method" + String.valueOf (i); new Thread(myThread1).start(); sleep( 100 ); } } } |
運行結果為:
1
2
|
非靜態的method1方法 靜態的method3方法 |
從上面的運行結果可以看出,method2和method4在method1和method3運行完之前是不會運行的。因此,可以得出一個結論,如查在類中使用synchronized來定義非靜態方法,那么將影響這個類中的所有synchronized定義的非靜態方法;如果定義的靜態方法,那么將影響這個類中所有以synchronized定義的靜態方法。這有點兒像數據表中的表鎖,當修改一條記錄時,系統就將整個表都鎖住了。因此,大量使用這種同步方法會使程序的性能大幅度地下降。
對共享資源的同步訪問更加安全的技巧:
1,定義private的instance變量+它的get方法,而不要定義public/protected的instance變量。如果將變量定義為public,對象可以在外界繞過同步方法的控制而直接取得它,并且改動它。這也是JavaBean的標準實現之一。
2,如果instance變量是一個對象,如數組或ArrayList等,那上述方法仍然不安全,因為當外界通過get方法拿到這個instance對象的引用后,又將其指向另一個對象,那么這個private變量也就變了,豈不是很危險。這個時候就需要將get方法也加上synchronized同步,并且只返回這個private對象的clone()。這樣,調用端得到的就只是對象副本的一個引用了。
wait()與notify()獲取對象監視器(鎖)的三種方式
在某個線程方法中對wait()和notify()的調用必須指定一個Object對象,而且該線程必須擁有該Object對象的monitor。而獲取對象monitor最簡單的辦法就是,在對象上使用synchronized關鍵字。當調用wait()方法以后,該線程會釋放掉對象鎖,并進入sleep狀態。而在其它線程調用notify()方法時,必須使用同一個Object對象,notify()方法調用成功后,所在這個對象上的相應的等侍線程將被喚醒。
對于被一個對象鎖定的多個方法,在調用notify()方法時將會任選其中一個進行喚醒,而notifyAll()則是將其所有等待線程喚醒。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
|
package net.mindview.util; import javax.swing.JFrame; public class WaitAndNotify { public static void main(String[] args) { System. out.println( "Hello World!" ); WaitAndNotifyJFrame frame = new WaitAndNotifyJFrame(); frame.setDefaultCloseOperation(JFrame. EXIT_ON_CLOSE); // frame.show(); frame.setVisible( true ); } } @SuppressWarnings ( "serial" ) class WaitAndNotifyJFrame extends JFrame { private WaitAndNotifyThread t ; public WaitAndNotifyJFrame() { setSize( 300 , 100 ); setLocation( 250 , 250 ); JPanel panel = new JPanel(); JButton start = new JButton( new AbstractAction( "Start" ) { public void actionPerformed(ActionEvent event) { if (t == null ) { t = new WaitAndNotifyThread(WaitAndNotifyJFrame. this ); t.start(); } else if (t .isWait ) { t. isWait = false ; t.n(); // t.notify(); } } }); panel.add(start); JButton pause = new JButton( new AbstractAction( "Pause" ) { public void actionPerformed(ActionEvent e) { if (t != null ) { t. isWait = true ; } } }); panel.add(pause); JButton end = new JButton( new AbstractAction( "End" ) { public void actionPerformed(ActionEvent e) { if (t != null ) { t.interrupt(); t = null ; } } }); panel.add(end); getContentPane().add(panel); } } @SuppressWarnings ( "unused" ) class WaitAndNotifyThread extends Thread { public boolean isWait ; private WaitAndNotifyJFrame control ; private int count ; public WaitAndNotifyThread(WaitAndNotifyJFrame f) { control = f; isWait = false ; count = 0 ; } public void run() { try { while ( true ) { synchronized ( this ) { System. out.println( "Count:" + count++); sleep( 100 ); if (isWait ) wait(); } } } catch (Exception e) { } } public void n() { synchronized ( this ) { notify(); } } } |
如上面例子方框中的代碼,若去掉同步代碼塊,執行就會拋出java.lang.IllegalMonitorStateException異常。
查看JDK,我們可以看到,出現此異常的原因是當前線程不是此對象監視器的所有者。
此方法只應由作為此對象監視器的所有者的線程來調用,通過以下三種方法之一,可以使線程成為此對象監視器的所有者:
1,通過執行此對象的同步實例方法,如:
1
2
3
|
public synchronized void n() { notify(); } |
2,通過執行在此對象上進行同步的synchronized語句的正文,如:
1
2
3
4
5
|
public void n() { synchronized ( this ) { notify(); } } |
3,對于Class類型的對象,可以通過執行該類的同步靜態方法。
在調用靜態方法時,我們并不一定創建一個實例對象。因此,就不能使用this來同步靜態方法,所以必須使用Class對象來同步靜態方法,由于notify()方法不是靜態方法,所以我們無法將n()方法設置成靜態方法,所以采用另外一個例子加以說明:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
public class SynchronizedStatic implements Runnable { private static boolean flag = true ; //類對象同步方法一: // 注意static修飾的同步方法,監視器:SynchronizedStatic.class private static synchronized void testSyncMethod() { for ( int i = 0 ; i < 100 ; i++) { try { Thread. sleep( 100 ); } catch (InterruptedException e) { e.printStackTrace(); } System. out.println( "testSyncMethod:" + i); } } //類對象同步方法二: private void testSyncBlock() { // 顯示使用獲取class做為監視器.它與static synchronized method隱式獲取class監視器一樣. synchronized (SynchronizedStatic. class ) { for ( int i = 0 ; i < 100 ; i++) { try { Thread. sleep( 100 ); } catch (InterruptedException e) { e.printStackTrace(); } System. out.println( "testSyncBlock:" + i); } } } public void run() { // flag是static的變量.所以,不同的線程會執行不同的方法,只有這樣才能看到不同的鎖定效果. if (flag ) { flag = false ; testSyncMethod(); } else { flag = true ; testSyncBlock(); } } public static void main(String[] args) { ExecutorService exec = Executors. newFixedThreadPool( 2 ); SynchronizedStatic rt = new SynchronizedStatic(); SynchronizedStatic rt1 = new SynchronizedStatic(); exec.execute(rt); exec.execute(rt1); exec.shutdown(); } } |
以上代碼的運行結果是,讓兩個同步方法同時打印從0到99這100個數,其中方法一是一個靜態同步方法,它的作用域為類;方法二顯示的聲明了代碼塊的作用域是類。這兩個方法的異曲同工的。由于方法一和方法二的作用域同為類,所以它們兩個方法間是互斥的,也就是說,當一個線程調用了這兩個方法中的一個,剩余沒有調用的方法也會對其它線程形成阻塞。因此,程序的運行結果會是:
1
2
3
4
5
6
7
|
testSyncMethod:0 testSyncMethod:1 ... ... testSyncMethod:99 testSyncBlock:0 ... ... testSyncBlock:99 |
但是,如果我們將方法二中的SynchronizedStatic. class替換成this的話,由于作用域的沒,這兩個方法就不會形成互斥,程序的輸出結果也會交替進行,如下所示:
1
2
3
4
5
6
7
|
testSyncBlock:0 testSyncMethod:0 testSyncBlock:1 testSyncMethod:1 ... ... testSyncMethod:99 testSyncBlock:99 |
鎖(lock)的作用域有兩種,一種是類的對象,另一種的類本身。在以上代碼中給出了兩種使鎖的作用范圍為類的方法,這樣就可以使同一個類的不同對象之間也能完成同步。
總結以上,需要注意的有以下幾點:
1,wait()、notify()、notifyAll()都需要在擁有對象監視器的前提下執行,否則就會拋出java.lang.IllegalMonitorStateException異常。
2,多個線程可以同時在一個對象上等待。
3,notify()是隨機喚醒一個在對象上等待的線程,若沒有等待的線程,則什么也不做。
4,notify()喚醒的線程,并不是在notify()執行以后就立即喚醒,而是在notify()線程釋放了對象監視器之后才真正執行被喚醒的線程。
5,Object的這些方法與Thread的sleep、interrupt方法相差還是很遠的,不要混為一談。