深入探究Java多線程并發(fā)編程的要點(diǎn)
關(guān)鍵字synchronized
synchronized關(guān)鍵可以修飾函數(shù)、函數(shù)內(nèi)語句。無論它加上方法還是對象上,它取得的鎖都是對象,而不是把一段代碼或是函數(shù)當(dāng)作鎖。
1,當(dāng)兩個(gè)并發(fā)線程訪問同一個(gè)對象object中的這個(gè)synchronized(this)同步代碼塊時(shí),一段時(shí)間只能有一個(gè)線程得到執(zhí)行,而另一個(gè)線程只有等當(dāng)前線程執(zhí)行完以后才能執(zhí)行這塊代碼。
2,當(dāng)一個(gè)線程訪問object中的一個(gè)synchronized(this)同步代碼塊時(shí),其它線程仍可以訪問這個(gè)object中是其它非synchronized (this)代碼塊。
3,這里需要注意的是,當(dāng)一個(gè)線程訪問object的一個(gè)synchronized(this)代碼塊時(shí),其它線程對這個(gè)object中其它synchronized (this)同步代碼塊的訪問將被阻塞。
4,以上所述也適用于其它的同步代碼塊,也就是說,當(dāng)一個(gè)線程訪問object的一個(gè)synchronized(this)同步代碼塊時(shí),這個(gè)線程就獲得了object的對象鎖。而且每個(gè)對象(即類實(shí)例)對應(yīng)著一把鎖,每個(gè)synchronized(this)都必須獲得調(diào)用該代碼塊兒(可以函數(shù),也可以是變量)的對象的鎖才能執(zhí)行,否則所屬線程阻塞,方法一旦執(zhí)行就會(huì)獨(dú)占該鎖,直到從方法返回時(shí),也釋放這個(gè)鎖,重新進(jìn)入可執(zhí)行狀態(tài)。這種機(jī)制確保了同一時(shí)刻對于每一個(gè)對象,其所有聲明為synchronized的成員函數(shù)中至多只有一個(gè)處于可執(zhí)行狀態(tài)(因?yàn)橹炼嘀挥幸粋€(gè)線程可以獲取該對象的鎖),從而避免了類成員變量的訪問沖突。
synchronized方式的缺點(diǎn):
由于synchronized鎖定的是調(diào)用這個(gè)同步方法的對象,也就是說,當(dāng)一個(gè)線程P1在不同的線程中執(zhí)行這個(gè)方法時(shí),它們之間會(huì)形成互斥,從而達(dá)到同步的效果。但這里需要注意的是,這個(gè)對象所性的Class的另一個(gè)對象卻可以任意調(diào)用這個(gè)被加了synchronized關(guān)鍵字的方法。同步方法的實(shí)質(zhì)是將synchronized作用于object reference,對于拿到了P1對象鎖的線程才可以調(diào)用這個(gè)synchronized方法,而對于P2來說,P1與它毫不相干,程序也可能在這種情況下擺脫同步機(jī)制的控制,造成數(shù)據(jù)混亂。以下我們將對這種情況進(jìn)行詳細(xì)地說明:
首先我們先介紹synchronized關(guān)鍵字的兩種加鎖對象:對象和類——synchronized可以為資源加對象鎖或是類鎖,類鎖對這個(gè)類的所有對象(實(shí)例)均起作用,而對象鎖只是針對該類的一個(gè)指定的對象加鎖,這個(gè)類的其它對象仍然可以使用已經(jīng)對前一個(gè)對象加鎖的synchronized方法。
在這里我們主要討論的一個(gè)問題就是:“同一個(gè)類,不同實(shí)例調(diào)用同一個(gè)方法,會(huì)產(chǎn)生同步問題嗎?”
同步問題只和資源有關(guān)系,要看這個(gè)資源是不是靜態(tài)的。同一個(gè)靜態(tài)數(shù)據(jù),你相同函數(shù)分屬不同線程同時(shí)對其進(jìn)行讀寫,CPU也不會(huì)產(chǎn)生錯(cuò)誤,它會(huì)保證你代碼的執(zhí)行邏輯,而這個(gè)邏輯是否是你想要的,那就要看你需要什么樣的同步了。即便你兩個(gè)不同的代碼,在CPU的不同的兩個(gè)core里跑,同時(shí)寫一個(gè)內(nèi)存地址,Cache機(jī)制也會(huì)在L2里先鎖定一個(gè)。然后更新,再share給另一個(gè)core,也不會(huì)出錯(cuò),不然intel,amd就白養(yǎng)那么多人了。
因此,只要你沒有兩個(gè)代碼共享的同一個(gè)資源或變量,就不會(huì)出現(xiàn)數(shù)據(jù)不一致的情況。而且同一個(gè)類的不同對象的調(diào)用有完全不同的堆棧,它們之間完全不相干。
以下我們以一個(gè)售票過程舉例說明,在這里,我們的共享資源就是票的剩余張數(shù)。
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 + ": 檢測票數(shù)大于0" ); System. out.println(name + ": \t正在收款(大約5秒完成)。。。" ); try { Thread. sleep(5000); System. out.println(name + ": \t打印票據(jù),售票完成" ); num--; printNumInfo(); } catch (InterruptedException e) { e.printStackTrace(); } } else { System. out.println(name+": 沒有票了,停止售票" ); } } private static void printNumInfo() { System. out.println("系統(tǒng):當(dāng)前票數(shù):" + num); if (num < 0) { System. out.println("警告:票數(shù)低于0,出現(xiàn)負(fù)數(shù)" ); } } public static void main(String args[]) { try { new ThreadSafeTest("售票員李XX" ).start(); Thread. sleep(2000); new ThreadSafeTest("售票員王X" ).start(); } catch (InterruptedException e) { e.printStackTrace(); } } }
運(yùn)行上述代碼,我們得到的輸出是:
售票員李XX: 檢測票數(shù)大于0 售票員李XX: 正在收款(大約5秒完成)。。。 售票員王X: 檢測票數(shù)大于0 售票員王X: 正在收款(大約5秒完成)。。。 售票員李XX: 打印票據(jù),售票完成 系統(tǒng):當(dāng)前票數(shù):0 售票員王X: 打印票據(jù),售票完成 系統(tǒng):當(dāng)前票數(shù):-1 警告:票數(shù)低于0,出現(xiàn)負(fù)數(shù)
根據(jù)輸出結(jié)果,我們可以發(fā)現(xiàn),剩余票數(shù)為-1,出現(xiàn)了同步錯(cuò)誤的問題。之所以出現(xiàn)這種情況的原因是,我們建立的兩個(gè)實(shí)例對象,對共享的靜態(tài)資源static int num = 1同時(shí)進(jìn)行了修改。那么我們將上面代碼中方框內(nèi)的修飾詞static去掉,然后再運(yùn)行程序,可以得到:
售票員李XX: 檢測票數(shù)大于0 售票員李XX: 正在收款(大約5秒完成)。。。 售票員王X: 檢測票數(shù)大于0 售票員王X: 正在收款(大約5秒完成)。。。 售票員李XX: 打印票據(jù),售票完成 系統(tǒng):當(dāng)前票數(shù):0 售票員王X: 打印票據(jù),售票完成 系統(tǒng):當(dāng)前票數(shù):0
對程度修改之后,程序運(yùn)行貌似沒有問題了,每個(gè)對象擁有各自不同的堆棧,分別獨(dú)立運(yùn)行。但這樣卻違背了我們希望多線程同時(shí)對共享資源的處理(去static后,num就從共享資源變成了每個(gè)實(shí)例各自擁有的成員變量),這顯然不是我們想要的。
在以上兩種代碼中,采取的主要是對對象的鎖定。由于我之前談到的原因,當(dāng)一個(gè)類的兩個(gè)不同的實(shí)例對同一共享資源進(jìn)行修改時(shí),CPU為了保證程序的邏輯會(huì)默認(rèn)這種做法,至于是不是想要的結(jié)果,這個(gè)只能由程序員自己來決定。因此,我們需要改變鎖的作用范圍,若作用對象只是實(shí)例,那么這種問題是無法避免的;只有當(dāng)鎖的作用范圍是整個(gè)類的時(shí)候,才可能排除同一個(gè)類的不同實(shí)例對共享資源同時(shí)修改的問題。
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 + ": 檢測票數(shù)大于0" ); System. out.println(name + ": \t正在收款(大約5秒完成)。。。" ); try { Thread. sleep(5000); System. out.println(name + ": \t打印票據(jù),售票完成" ); num--; printNumInfo(); } catch (InterruptedException e) { e.printStackTrace(); } } else { System. out.println(name+": 沒有票了,停止售票" ); } } private static void printNumInfo() { System. out.println("系統(tǒng):當(dāng)前票數(shù):" + num); if (num < 0) { System. out.println("警告:票數(shù)低于0,出現(xiàn)負(fù)數(shù)" ); } } public static void main(String args[]) { try { new ThreadSafeTest("售票員李XX" ).start(); Thread. sleep(2000); new ThreadSafeTest("售票員王X" ).start(); } catch (InterruptedException e) { e.printStackTrace(); } } }
將程序做如上修改,可以得到運(yùn)行結(jié)果:
售票員李XX: 檢測票數(shù)大于0 售票員李XX: 正在收款(大約5秒完成)。。。 售票員李XX: 打印票據(jù),售票完成 系統(tǒng):當(dāng)前票數(shù):0 售票員王X: 沒有票了,停止售票
對sell()方法加上了static修飾符,這樣就將鎖的作用對象變成了類,當(dāng)該類的一個(gè)實(shí)例對共享變量進(jìn)行操作時(shí)將會(huì)阻塞這個(gè)類的其它實(shí)例對其的操作。從而得到我們?nèi)缙谙胍慕Y(jié)果。
總結(jié):
1,synchronized關(guān)鍵字有兩種用法:synchronized方法和synchronized塊。
2,在Java中不單是類實(shí)例,每一個(gè)類也可以對應(yīng)一把鎖
在使用synchronized關(guān)鍵字時(shí),有以下幾點(diǎn)兒需要注意:
1,synchronized關(guān)鍵字不能被繼承。雖然可以用synchronized來定義方法,但是synchronized卻并不屬于方法定義的一部分,所以synchronized關(guān)鍵字并不能被繼承。如果父類中的某個(gè)方法使用了synchronized關(guān)鍵字,而子類中也覆蓋了這個(gè)方法,默認(rèn)情況下子類中的這個(gè)方法并不是同步的,必須顯示的在子類的這個(gè)方法中加上synchronized關(guān)鍵字才可。當(dāng)然,也可以在子類中調(diào)用父類中相應(yīng)的方法,這樣雖然子類中的方法并不是同步的,但子類調(diào)用了父類中的同步方法,也就相當(dāng)子類方法也同步了。如,
在子類中加synchronized關(guān)鍵字:
class Parent { public synchronized void method() { } } class Child extends Parent { public synchronized void method () { } }
調(diào)用父類方法:
class Parent { public synchronized void method() { } } class Child extends Parent { public void method() { super.method(); } }
2,在接口方法定義時(shí)不能使用synchronized關(guān)鍵字。
3,構(gòu)造方法不能使用synchronized關(guān)鍵字,但可以使用synchronized塊來進(jìn)行同步。
4,synchronized位置可以自由放置,但是不能放置在方法的返回類型后面。
5,synchronized關(guān)鍵字不可以用來同步變量,如下面代碼是錯(cuò)誤的:
public synchronized int n = 0; public static synchronized int n = 0;
6,雖然使用synchronized關(guān)鍵字是最安全的同步方法,但若是大量使用也會(huì)造成不必要的資源消耗以及性能損失。從表面上看synchronized鎖定的是一個(gè)方法,但實(shí)際上鎖定的卻是一個(gè)類,比如,對于兩個(gè)非靜態(tài)方法method1()和method2()都使用了synchronized關(guān)鍵字,在執(zhí)行其中的一個(gè)方法時(shí),另一個(gè)方法是不能執(zhí)行的。靜態(tài)方法和非靜態(tài)方法情況類似。但是靜態(tài)方法和非靜態(tài)方法之間不會(huì)相互影響,見如下代碼:
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( "非靜態(tài)的method1方法" ); } public synchronized void method2() { method( "非靜態(tài)的method2方法" ); } public static synchronized void method3() { method( "靜態(tài)的method3方法" ); } public static synchronized void method4() { method( "靜態(tài)的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); } } }
運(yùn)行結(jié)果為:
非靜態(tài)的method1方法 靜態(tài)的method3方法
從上面的運(yùn)行結(jié)果可以看出,method2和method4在method1和method3運(yùn)行完之前是不會(huì)運(yùn)行的。因此,可以得出一個(gè)結(jié)論,如查在類中使用synchronized來定義非靜態(tài)方法,那么將影響這個(gè)類中的所有synchronized定義的非靜態(tài)方法;如果定義的靜態(tài)方法,那么將影響這個(gè)類中所有以synchronized定義的靜態(tài)方法。這有點(diǎn)兒像數(shù)據(jù)表中的表鎖,當(dāng)修改一條記錄時(shí),系統(tǒng)就將整個(gè)表都鎖住了。因此,大量使用這種同步方法會(huì)使程序的性能大幅度地下降。
對共享資源的同步訪問更加安全的技巧:
1,定義private的instance變量+它的get方法,而不要定義public/protected的instance變量。如果將變量定義為public,對象可以在外界繞過同步方法的控制而直接取得它,并且改動(dòng)它。這也是JavaBean的標(biāo)準(zhǔn)實(shí)現(xiàn)之一。
2,如果instance變量是一個(gè)對象,如數(shù)組或ArrayList等,那上述方法仍然不安全,因?yàn)楫?dāng)外界通過get方法拿到這個(gè)instance對象的引用后,又將其指向另一個(gè)對象,那么這個(gè)private變量也就變了,豈不是很危險(xiǎn)。這個(gè)時(shí)候就需要將get方法也加上synchronized同步,并且只返回這個(gè)private對象的clone()。這樣,調(diào)用端得到的就只是對象副本的一個(gè)引用了。
wait()與notify()獲取對象監(jiān)視器(鎖)的三種方式
在某個(gè)線程方法中對wait()和notify()的調(diào)用必須指定一個(gè)Object對象,而且該線程必須擁有該Object對象的monitor。而獲取對象monitor最簡單的辦法就是,在對象上使用synchronized關(guān)鍵字。當(dāng)調(diào)用wait()方法以后,該線程會(huì)釋放掉對象鎖,并進(jìn)入sleep狀態(tài)。而在其它線程調(diào)用notify()方法時(shí),必須使用同一個(gè)Object對象,notify()方法調(diào)用成功后,所在這個(gè)對象上的相應(yīng)的等侍線程將被喚醒。
對于被一個(gè)對象鎖定的多個(gè)方法,在調(diào)用notify()方法時(shí)將會(huì)任選其中一個(gè)進(jìn)行喚醒,而notifyAll()則是將其所有等待線程喚醒。
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(); } } }
如上面例子方框中的代碼,若去掉同步代碼塊,執(zhí)行就會(huì)拋出java.lang.IllegalMonitorStateException異常。
查看JDK,我們可以看到,出現(xiàn)此異常的原因是當(dāng)前線程不是此對象監(jiān)視器的所有者。
此方法只應(yīng)由作為此對象監(jiān)視器的所有者的線程來調(diào)用,通過以下三種方法之一,可以使線程成為此對象監(jiān)視器的所有者:
1,通過執(zhí)行此對象的同步實(shí)例方法,如:
public synchronized void n() { notify(); }
2,通過執(zhí)行在此對象上進(jìn)行同步的synchronized語句的正文,如:
public void n() { synchronized (this ) { notify(); } }
3,對于Class類型的對象,可以通過執(zhí)行該類的同步靜態(tài)方法。
在調(diào)用靜態(tài)方法時(shí),我們并不一定創(chuàng)建一個(gè)實(shí)例對象。因此,就不能使用this來同步靜態(tài)方法,所以必須使用Class對象來同步靜態(tài)方法,由于notify()方法不是靜態(tài)方法,所以我們無法將n()方法設(shè)置成靜態(tài)方法,所以采用另外一個(gè)例子加以說明:
public class SynchronizedStatic implements Runnable { private static boolean flag = true; //類對象同步方法一: // 注意static修飾的同步方法,監(jiān)視器: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做為監(jiān)視器.它與static synchronized method隱式獲取class監(jiān)視器一樣. 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的變量.所以,不同的線程會(huì)執(zhí)行不同的方法,只有這樣才能看到不同的鎖定效果. 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(); } }
以上代碼的運(yùn)行結(jié)果是,讓兩個(gè)同步方法同時(shí)打印從0到99這100個(gè)數(shù),其中方法一是一個(gè)靜態(tài)同步方法,它的作用域?yàn)轭?;方法二顯示的聲明了代碼塊的作用域是類。這兩個(gè)方法的異曲同工的。由于方法一和方法二的作用域同為類,所以它們兩個(gè)方法間是互斥的,也就是說,當(dāng)一個(gè)線程調(diào)用了這兩個(gè)方法中的一個(gè),剩余沒有調(diào)用的方法也會(huì)對其它線程形成阻塞。因此,程序的運(yùn)行結(jié)果會(huì)是:
testSyncMethod:0 testSyncMethod:1 ... ... testSyncMethod:99 testSyncBlock:0 ... ... testSyncBlock:99
但是,如果我們將方法二中的SynchronizedStatic. class替換成this的話,由于作用域的沒,這兩個(gè)方法就不會(huì)形成互斥,程序的輸出結(jié)果也會(huì)交替進(jìn)行,如下所示:
testSyncBlock:0 testSyncMethod:0 testSyncBlock:1 testSyncMethod:1 ... ... testSyncMethod:99 testSyncBlock:99
鎖(lock)的作用域有兩種,一種是類的對象,另一種的類本身。在以上代碼中給出了兩種使鎖的作用范圍為類的方法,這樣就可以使同一個(gè)類的不同對象之間也能完成同步。
總結(jié)以上,需要注意的有以下幾點(diǎn):
1,wait()、notify()、notifyAll()都需要在擁有對象監(jiān)視器的前提下執(zhí)行,否則就會(huì)拋出java.lang.IllegalMonitorStateException異常。
2,多個(gè)線程可以同時(shí)在一個(gè)對象上等待。
3,notify()是隨機(jī)喚醒一個(gè)在對象上等待的線程,若沒有等待的線程,則什么也不做。
4,notify()喚醒的線程,并不是在notify()執(zhí)行以后就立即喚醒,而是在notify()線程釋放了對象監(jiān)視器之后才真正執(zhí)行被喚醒的線程。
5,Object的這些方法與Thread的sleep、interrupt方法相差還是很遠(yuǎn)的,不要混為一談。
相關(guān)文章
SpringCloud超詳細(xì)講解微服務(wù)網(wǎng)關(guān)Zuul
這篇文章主要介紹了SpringCloud Zuul微服務(wù)網(wǎng)關(guān),負(fù)載均衡,熔斷和限流,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-07-07詳解JAVA高質(zhì)量代碼之?dāng)?shù)組與集合
在學(xué)習(xí)編程的過程中,我覺得不止要獲得課本的知識(shí),更多的是通過學(xué)習(xí)技術(shù)知識(shí)提高解決問題的能力,這樣我們才能走在最前方,本文主要講述Java高質(zhì)量代碼之?dāng)?shù)組與集合2013-08-08mybatis映射內(nèi)部類的使用及注意事項(xiàng)說明
這篇文章主要介紹了mybatis映射內(nèi)部類的使用及注意事項(xiàng)說明,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-12-12關(guān)于在Java中如何使用yaml的實(shí)例
這篇文章主要介紹了關(guān)于在Java中如何使用yaml的實(shí)例,YAML是一種輕量級的數(shù)據(jù)序列化格式。它以易讀、易寫的文本格式表示數(shù)據(jù),支持列表、字典等各種數(shù)據(jù)結(jié)構(gòu),被廣泛應(yīng)用于配置文件、數(shù)據(jù)傳輸協(xié)議等領(lǐng)域,需要的朋友可以參考下2023-08-08詳解如何保護(hù)SpringBoot配置文件中的敏感信息
使用過SpringBoot配置文件的朋友都知道,資源文件中的內(nèi)容通常情況下是明文顯示,安全性就比較低一些,所以為了提高安全性,就需要對配置文件中的敏感信息進(jìn)行保護(hù),下面就為大家介紹一下實(shí)現(xiàn)方法吧2023-07-07java日常練習(xí)題,每天進(jìn)步一點(diǎn)點(diǎn)(1)
下面小編就為大家?guī)硪黄狫ava基礎(chǔ)的幾道練習(xí)題(分享)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧,希望可以幫到你2021-07-07