面試官:Java 實現(xiàn)線程安全的三種方式
一個程序在運行起來的時候會轉(zhuǎn)換成進程,通常含有多個線程。
通常情況下,一個進程中的比較耗時的操作(如長循環(huán)、文件上傳下載、網(wǎng)絡(luò)資源獲取等),往往會采用多線程來解決。
比如顯示生活中,銀行取錢問題、火車票多個售票窗口的問題,通常會涉及到并發(fā)的問題,從而需要多線程的技術(shù)。
當(dāng)進程中有多個并發(fā)線程進入一個重要數(shù)據(jù)的代碼塊時,在修改數(shù)據(jù)的過程中,很有可能引發(fā)線程安全問題,從而造成數(shù)據(jù)異常。例如,正常邏輯下,同一個編號的火車票只能售出一次,卻由于線程安全問題而被多次售出,從而引起實際業(yè)務(wù)異常。
現(xiàn)在我們就以售票問題來演示線程安全的問題
1, 在不對多線程數(shù)據(jù)進行保護的情況下會引發(fā)的狀況
public?class?ThreadUnSecurity?{
????
????static?int?tickets?=?10;
????
????class?SellTickets?implements?Runnable{
????????@Override
????????public?void?run()?{
????????????//?未加同步時產(chǎn)生臟數(shù)據(jù)
????????????while(tickets?>?0)?{
????????????????
????????????????System.out.println(Thread.currentThread().getName()+"--->售出第:??"+tickets+"?票");
????????????????tickets--;
????????????????
????????????????try?{
????????????????????Thread.sleep(1000);
????????????????}?catch?(InterruptedException?e)?{
????????????????????e.printStackTrace();
????????????????}
????????????????
????????????}
????????????
????????????if?(tickets?<=?0)?{
????????????????
????????????????System.out.println(Thread.currentThread().getName()+"--->售票結(jié)束!");
????????????}
????????}
????}
????
????
????public?static?void?main(String[]?args)?{
????????
????????
????????SellTickets?sell?=?new?ThreadUnSecurity().new?SellTickets();
????????
????????Thread?thread1?=?new?Thread(sell,?"1號窗口");
????????Thread?thread2?=?new?Thread(sell,?"2號窗口");
????????Thread?thread3?=?new?Thread(sell,?"3號窗口");
????????Thread?thread4?=?new?Thread(sell,?"4號窗口");
????????
????????thread1.start();
????????thread2.start();
????????thread3.start();
????????thread4.start();
????????
????????
????}
????
}
上述代碼運行的結(jié)果:
1號窗口--->售出第:? 10?票
3號窗口--->售出第:? 10?票
2號窗口--->售出第:? 10?票
4號窗口--->售出第:? 10?票
2號窗口--->售出第:? 6 票
1號窗口--->售出第:? 5 票
3號窗口--->售出第:? 4 票
4號窗口--->售出第:? 3 票
2號窗口--->售出第:? 2 票
4號窗口--->售出第:? 1 票
1號窗口--->售出第:? 1 票
3號窗口--->售票結(jié)束!
2號窗口--->售票結(jié)束!
1號窗口--->售票結(jié)束!
4號窗口--->售票結(jié)束!
我們可以看出同一張票在不對票數(shù)進行保護時會出現(xiàn)同一張票會被出售多次!由于線程調(diào)度中的不確定性,讀者在演示上述代碼時,出現(xiàn)的運行結(jié)果會有不同。
第一種方式:同步代碼塊
package?com.bpan.spring.beans.thread;
import?com.sun.org.apache.regexp.internal.recompile;
public?class?ThreadSynchronizedSecurity?{
????
????static?int?tickets?=?10;
????
????class?SellTickets?implements?Runnable{
????????@Override
????????public?void?run()?{
????????????//?同步代碼塊
????????????while(tickets?>?0)?{
????????????????
????????????????synchronized?(this)?{
????????????????????
//????????????????????System.out.println(this.getClass().getName().toString());
????????????????????
????????????????????if?(tickets?<=?0)?{
????????????????????????
????????????????????????return;
????????????????????}
????????????????????
????????????????????System.out.println(Thread.currentThread().getName()+"--->售出第:??"+tickets+"?票");
????????????????????tickets--;
????????????????????
????????????????????try?{
????????????????????????Thread.sleep(100);
????????????????????}?catch?(InterruptedException?e)?{
????????????????????????e.printStackTrace();
????????????????????}
????????????????}
????????????????
????????????????if?(tickets?<=?0)?{
????????????????????
????????????????????System.out.println(Thread.currentThread().getName()+"--->售票結(jié)束!");
????????????????}
????????????}
????????}
????}
????
????
????public?static?void?main(String[]?args)?{
????????
????????
????????SellTickets?sell?=?new?ThreadSynchronizedSecurity().new?SellTickets();
????????
????????Thread?thread1?=?new?Thread(sell,?"1號窗口");
????????Thread?thread2?=?new?Thread(sell,?"2號窗口");
????????Thread?thread3?=?new?Thread(sell,?"3號窗口");
????????Thread?thread4?=?new?Thread(sell,?"4號窗口");
????????
????????thread1.start();
????????thread2.start();
????????thread3.start();
????????thread4.start();
????????
????????
????}
????
}
輸出結(jié)果讀者可自行調(diào)試,不會出現(xiàn)同一張票被出售多次的情況。
第二種方式:同步方法
package?com.bpan.spring.beans.thread;
public?class?ThreadSynchroniazedMethodSecurity?{
????
????
????static?int?tickets?=?10;
????
????class?SellTickets?implements?Runnable{
????????@Override
????????public?void?run()?{
????????????//同步方法
????????????while?(tickets?>?0)?{
????????????????
????????????????synMethod();
????????????????
????????????????try?{
????????????????????Thread.sleep(100);
????????????????}?catch?(InterruptedException?e)?{
????????????????????//?TODO?Auto-generated?catch?block
????????????????????e.printStackTrace();
????????????????}
????????????????
????????????????if?(tickets<=0)?{
????????????????????
????????????????????System.out.println(Thread.currentThread().getName()+"--->售票結(jié)束");
????????????????}
????????????????
????????????}
????????????
????????????
????????}
????????
????????synchronized?void?synMethod()?{
????????????
????????????synchronized?(this)?{
????????????????if?(tickets?<=0)?{
????????????????????
????????????????????return;
????????????????}
????????????????
????????????????System.out.println(Thread.currentThread().getName()+"---->售出第?"+tickets+"?票?");
????????????????tickets--?;
????????????}
????????????
????????}
????????
????}
????public?static?void?main(String[]?args)?{
????????
????????
????????SellTickets?sell?=?new?ThreadSynchroniazedMethodSecurity().new?SellTickets();
????????
????????Thread?thread1?=?new?Thread(sell,?"1號窗口");
????????Thread?thread2?=?new?Thread(sell,?"2號窗口");
????????Thread?thread3?=?new?Thread(sell,?"3號窗口");
????????Thread?thread4?=?new?Thread(sell,?"4號窗口");
????????
????????thread1.start();
????????thread2.start();
????????thread3.start();
????????thread4.start();
????????
????}
}
讀者可自行調(diào)試上述代碼的運行結(jié)果
第三種方式:Lock鎖機制
通過創(chuàng)建Lock對象,采用lock()加鎖,unlock()解鎖,來保護指定的代碼塊
package?com.bpan.spring.beans.thread;
import?java.util.concurrent.locks.Lock;
import?java.util.concurrent.locks.ReentrantLock;
public?class?ThreadLockSecurity?{
????
????static?int?tickets?=?10;
????
????class?SellTickets?implements?Runnable{
????????
????????Lock?lock?=?new?ReentrantLock();
????????@Override
????????public?void?run()?{
????????????//?Lock鎖機制
????????????while(tickets?>?0)?{
????????????????
????????????????try?{
????????????????????lock.lock();
????????????????????
????????????????????if?(tickets?<=?0)?{
????????????????????????
????????????????????????return;
????????????????????}
????????????????????????
????????????????????System.out.println(Thread.currentThread().getName()+"--->售出第:??"+tickets+"?票");
????????????????????tickets--;
????????????????}?catch?(Exception?e1)?{
????????????????????//?TODO?Auto-generated?catch?block
????????????????????e1.printStackTrace();
????????????????}finally?{
????????????????????
????????????????????lock.unlock();
????????????????????try?{
????????????????????????Thread.sleep(100);
????????????????????}?catch?(InterruptedException?e)?{
????????????????????????e.printStackTrace();
????????????????????}
????????????????}
????????????}
????????????????
????????????if?(tickets?<=?0)?{
????????????????
????????????????System.out.println(Thread.currentThread().getName()+"--->售票結(jié)束!");
????????????}
????????????
????????}
????}
????
????
????public?static?void?main(String[]?args)?{
????????
????????
????????SellTickets?sell?=?new?ThreadLockSecurity().new?SellTickets();
????????
????????Thread?thread1?=?new?Thread(sell,?"1號窗口");
????????Thread?thread2?=?new?Thread(sell,?"2號窗口");
????????Thread?thread3?=?new?Thread(sell,?"3號窗口");
????????Thread?thread4?=?new?Thread(sell,?"4號窗口");
????????
????????thread1.start();
????????thread2.start();
????????thread3.start();
????????thread4.start();
????????
????????
????}
????
}
最后總結(jié)
由于synchronized是在JVM層面實現(xiàn)的,因此系統(tǒng)可以監(jiān)控鎖的釋放與否;而ReentrantLock是使用代碼實現(xiàn)的,系統(tǒng)無法自動釋放鎖,需要在代碼中的finally子句中顯式釋放鎖lock.unlock()。
另外,在并發(fā)量比較小的情況下,使用synchronized是個不錯的選擇;但是在并發(fā)量比較高的情況下,其性能下降會很嚴(yán)重,此時ReentrantLock是個不錯的方案。
補充
在使用synchronized 代碼塊時,可以與wait()、notify()、nitifyAll()一起使用,從而進一步實現(xiàn)線程的通信。
其中,wait()方法會釋放占有的對象鎖,當(dāng)前線程進入等待池,釋放cpu,而其他正在等待的線程即可搶占此鎖,獲得鎖的線程即可運行程序;
線程的sleep()方法則表示,當(dāng)前線程會休眠一段時間,休眠期間,會暫時釋放cpu,但并不釋放對象鎖,也就是說,在休眠期間,其他線程依然無法進入被同步保護的代碼內(nèi)部,當(dāng)前線程休眠結(jié)束時,會重新獲得cpu執(zhí)行權(quán),從而執(zhí)行被同步保護的代碼。
wait()和sleep()最大的不同在于wait()會釋放對象鎖,而sleep()不會釋放對象鎖。
notify()方法會喚醒因為調(diào)用對象的wait()而處于等待狀態(tài)的線程,從而使得該線程有機會獲取對象鎖。調(diào)用notify()后,當(dāng)前線程并不會立即釋放鎖,而是繼續(xù)執(zhí)行當(dāng)前代碼,直到synchronized中的代碼全部執(zhí)行完畢,才會釋放對象鎖。JVM會在等待的線程中調(diào)度一個線程去獲得對象鎖,執(zhí)行代碼。
需要注意的是,wait()和notify()必須在synchronized代碼塊中調(diào)用。
notifyAll()是喚醒所有等待的線程。
下面是示例代碼,
package?com.bpan.spring.beans.thread;
public?class?ThreadDemo?{
????
????static?final?Object?obj?=?new?Object();
????
????//第一個子線程
????static?class?ThreadA?implements?Runnable{
????????@Override
????????public?void?run()?{
????????????
????????????
????????????int?count?=?10;
????????????while(count?>?0)?{
????????????????
????????????????synchronized?(ThreadDemo.obj)?{
????????????????????
????????????????????System.out.println("A-----"+count);
????????????????????count--;
????????????????????
????????????????????synchronized?(ThreadDemo.obj)?{
????????????????????????
????????????????????????//notify()方法會喚醒因為調(diào)用對象的wait()而處于等待狀態(tài)的線程,從而使得該線程有機會獲取對象鎖。
????????????????????????//調(diào)用notify()后,當(dāng)前線程并不會立即釋放鎖,而是繼續(xù)執(zhí)行當(dāng)前代碼,直到synchronized中的代碼全部執(zhí)行完畢,
????????????????????????ThreadDemo.obj.notify();
????????????????????????
????????????????????????try?{
????????????????????????????ThreadDemo.obj.wait();
????????????????????????}?catch?(InterruptedException?e)?{
????????????????????????????//?TODO?Auto-generated?catch?block
????????????????????????????e.printStackTrace();
????????????????????????}
????????????????????}
????????????????}
????????????}
????????????
????????}
????????
????}
????
????static?class?ThreadB?implements?Runnable{
????????
????????
????????@Override
????????public?void?run()?{
????????????
????????????int?count?=?10;
????????????
????????????while(count?>?0)?{
????????????????
????????????????synchronized?(ThreadDemo.obj)?{
????????????????????System.out.println("B-----"+count);
????????????????????count--;
????????????????????
????????????????????synchronized?(ThreadDemo.obj)?{
????????????????????
????????????????????????//notify()方法會喚醒因為調(diào)用對象的wait()而處于等待狀態(tài)的線程,從而使得該線程有機會獲取對象鎖。
????????????????????????//調(diào)用notify()后,當(dāng)前線程并不會立即釋放鎖,而是繼續(xù)執(zhí)行當(dāng)前代碼,直到synchronized中的代碼全部執(zhí)行完畢,
????????????????????????ThreadDemo.obj.notify();
????????????????????????
????????????????????????try?{
????????????????????????????ThreadDemo.obj.wait();
????????????????????????}?catch?(InterruptedException?e)?{
????????????????????????????//?TODO?Auto-generated?catch?block
????????????????????????????e.printStackTrace();
????????????????????????}
????????????????????}
????????????????????
????????????????}
????????????????
????????????}
????????????
????????}
????????
????}
????
????public?static?void?main(String[]?args)?{
????????
????????
????????new?Thread(new?ThreadA()).start();
????????new?Thread(new?ThreadB()).start();
????????
????}
}
來源:cnblogs.com/revel171226/p/9411131.html
程序汪資料鏈接
堪稱神級的Spring Boot手冊,從基礎(chǔ)入門到實戰(zhàn)進階
臥槽!字節(jié)跳動《算法中文手冊》火了,完整版 PDF 開放下載!
臥槽!阿里大佬總結(jié)的《圖解Java》火了,完整版PDF開放下載!
字節(jié)跳動總結(jié)的設(shè)計模式 PDF 火了,完整版開放下載!
歡迎添加程序汪個人微信 itwang009? 進粉絲群或圍觀朋友
