三道java新手入門面試題,通往自由的道路--多線程
1. 你知道線程安全問(wèn)題嗎?
線程安全問(wèn)題:一般指在多線程模式下,多個(gè)線程對(duì)同一個(gè)共享數(shù)據(jù)進(jìn)行操作時(shí),第一個(gè)線程還沒(méi)來(lái)得及更新共享數(shù)據(jù),從而導(dǎo)致另外一個(gè)線程沒(méi)得到最新的數(shù)據(jù),并更新數(shù)據(jù),從而產(chǎn)生線程安全問(wèn)題。比較常見(jiàn)的場(chǎng)景有買票。
我舉個(gè)例子吧:
需求:比如買周杰倫演唱會(huì)的門票,此時(shí)有三個(gè)窗口同時(shí)賣總共100張票。窗口就是線程對(duì)象,而100張票的資源,此時(shí)就相當(dāng)于多個(gè)線程去搶占cpu的資源去搶對(duì)票的使用權(quán)。
嘿嘿,這是在大學(xué)時(shí)期和MyGirl去看滴,現(xiàn)在想去看也沒(méi)辦法了。話不多說(shuō),我們還是來(lái)看看代碼吧:
public class SellTicketDemo { public static void main(String[] args) { // 創(chuàng)建線程任務(wù)對(duì)象 Ticket ticket = new Ticket(); //創(chuàng)建三個(gè)窗口對(duì)象 Thread thread = new Thread(ticket, "窗口1"); Thread thread2 = new Thread(ticket, "窗口2"); Thread thread3 = new Thread(ticket, "窗口3"); //同時(shí)賣票 thread.start(); thread2.start(); thread3.start(); } } // 創(chuàng)建Ticket實(shí)現(xiàn)Runnale class Ticket implements Runnable { private int ticket = 100; // 100張周杰倫演唱會(huì)門票 // 執(zhí)行買票的邏輯 @Override public void run() { // 注意每個(gè)窗口都有賣票的權(quán)利 while (true) { if (ticket > 0) { // 有票 可以賣 // 出票: 因?yàn)檫M(jìn)來(lái)買票,總有出票,總會(huì)慢慢沒(méi)票的吧 try { // 這里采用sleep稍微等待下,模擬一下出票時(shí)間 。 Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } // 獲取當(dāng)前線程對(duì)象的名字 System.out.println(Thread.currentThread().getName() + "正在賣:" + ticket--); } } } }
這里講下: 我們創(chuàng)建了Ticket實(shí)現(xiàn)Runnable接口,并重寫(xiě)里面的run方法實(shí)現(xiàn)買票的功能,并定義了一個(gè)共享的變量。并在main方法中創(chuàng)建了三個(gè)線程去實(shí)現(xiàn)三個(gè)窗口買票的功能。最后我們來(lái)看看結(jié)果吧:
在你多運(yùn)行幾次,可以看到這樣的現(xiàn)象:
- 賣出了不存在的票,比如0票與-1票,是不存在滴。而且這種情況根本不允許發(fā)生呀,誰(shuí)會(huì)賣0張甚至-1張票呢。
- 出現(xiàn)多賣相同的票數(shù),比如8和1這張票被賣了三回。那這就很過(guò)分了呦,一張票還可以賣三個(gè)人哈哈。
這些問(wèn)題的發(fā)生就代表我們剛才是線程不安全的了。那線程安全問(wèn)題具體是什么呢?我們可以總結(jié)得到:
是由全局變量及靜態(tài)變量引起的。若每個(gè)線程中對(duì)全局變量、靜態(tài)變量只有讀操作,而無(wú)寫(xiě)操作,一般來(lái)說(shuō),這個(gè)全局變量是線程安全的;若有多個(gè)線程同時(shí)執(zhí)行寫(xiě)操作,一般都需要考慮線程同步,否則的話就可能影響線程安全。
那可能發(fā)生線程安全問(wèn)題的條件:
- 是否多線程環(huán)境下,單線程情況去對(duì)數(shù)據(jù)操作,當(dāng)然是沒(méi)什么問(wèn)題滴啦!
- 是否存在共享變量,如上面的代碼中,定義了一個(gè)全局變量ticket的演唱會(huì)門票,多個(gè)線程會(huì)共享這一變量。
- 是否存在多條語(yǔ)句操作共享數(shù)據(jù),如上面的代碼中,你在賣出票后,ticket變量肯定需要減少滴呀,所以對(duì)ticket進(jìn)行了減的操作了。
2. 那如何解決線程安全問(wèn)題呢?
我們可以引入線程同步解決線程安全問(wèn)題。在上面賣票問(wèn)題中,多線程并發(fā)訪問(wèn)一個(gè)資源的安全性問(wèn)題:也就是解決重復(fù)票與不存在票問(wèn)題,Java中提供了同步機(jī)制(synchronized
)來(lái)解決。
即具體的解決思路是這樣的:
1.窗口1線程進(jìn)入買票的時(shí)候,窗口2和窗口3線程只能在外等著,此時(shí)是不能進(jìn)行買票的操作的,只能等待窗口1買票完畢后,就進(jìn)入到窗口1和窗口2和窗口3再次搶占cpu的資源去執(zhí)行賣票功能。
2.也就是說(shuō)在某個(gè)線程修改共享資源的時(shí)候,其他線程不能修改該資源,等待修改完畢同步之后,才能去搶奪CPU資源,完成對(duì)應(yīng)的操作,保證了數(shù)據(jù)的同步性,解決了線程不安全的現(xiàn)象。
而synchronized
關(guān)鍵字又給我們提供幾種方法呢:
1.實(shí)現(xiàn)同步代碼塊:synchronized
關(guān)鍵字可以用于方法中的某個(gè)區(qū)塊中,表示只對(duì)這個(gè)區(qū)塊的資源實(shí)行互斥訪問(wèn)。
2.實(shí)現(xiàn)同步方法:使用synchronized
修飾的方法,就叫做同步方法,保證一個(gè)線程執(zhí)行該方法的時(shí)候,其他線程只能在方法外等著。
public class SynchronizedDemo { // 加在方法上 實(shí)際是對(duì)this對(duì)象加鎖 private synchronized void synchronizedTest() { } // 同步代碼塊,鎖對(duì)象可以是任意的,可以使用this,或者類.class對(duì)象,或者任意對(duì)象都可以 private void synchronizedTest2(){ synchronized (this){ } } // 加在靜態(tài)方法上 實(shí)際是對(duì)類對(duì)象加鎖 private synchronized static void synchronizedTest3() { } }
除了synchronized 關(guān)鍵字,還有Lock 鎖,與此種方法需要自己定義鎖的釋放位置。
Lock lock = new ReentrantLock(); lock.lock(); // 自己定義開(kāi)啟鎖位置 try { System. out. println("我們獲得了鎖"); } catch (Exception e) { } finally { System. out. println("我們釋放了鎖"); lock.unlock();// 需要自己定義釋放鎖位置,不然會(huì)存在死鎖問(wèn)題。 }
那我們總結(jié)下:
我們?cè)趺幢WC安全問(wèn)題:
1.使用synchronized關(guān)鍵字,實(shí)現(xiàn)同步代碼塊或者同步方法。達(dá)到保證一個(gè)線程對(duì)資源操作的時(shí)候,其他線程只能等待。
2.用手動(dòng)鎖 Lock,使用Lock鎖出現(xiàn)的位置可以相對(duì)比較靈活,但是必須有釋放鎖的配合動(dòng)作。
兩者的區(qū)別:
- synchronized 它是Java中的關(guān)鍵字,而Lock是一個(gè)類,它是個(gè)接口一般我們會(huì)使用ReentrantLock來(lái)創(chuàng)建實(shí)例對(duì)象。synchronized 它可以修飾在類、方法、變量中,可以實(shí)現(xiàn)同步代碼塊,而ReentrantLock只適用于代碼塊鎖。
- synchronized 操作的應(yīng)該是對(duì)象頭中 mark word,而ReentrantLock 底層調(diào)用的是 Unsafe 的park 方法加鎖。
- ReentrantLock 必須手動(dòng)獲取與釋放鎖,而synchronized 不需要手動(dòng)釋放和開(kāi)啟鎖。
- 兩者都是可重入鎖??芍厝腈i就是允許同一個(gè)線程多次獲取同一把鎖,如果某個(gè)線程已經(jīng)獲得某個(gè)鎖,自己可以再次獲取鎖而不會(huì)出現(xiàn)死鎖。而如果是不可鎖重入的話,就會(huì)造成死鎖。
3. 那你講下死鎖是什么吧?
死鎖是指兩個(gè)或兩個(gè)以上的進(jìn)程(線程)在執(zhí)行過(guò)程中,由于競(jìng)爭(zhēng)資源或者由于彼此通信而造成的一種阻塞的現(xiàn)象,若無(wú)外力作用,它們都將無(wú)法推進(jìn)下去。此時(shí)稱系統(tǒng)處于死鎖狀態(tài)或系統(tǒng)產(chǎn)生了死鎖,這些永遠(yuǎn)在互相等待的進(jìn)程(線程)稱為死鎖進(jìn)程(線程)。
舉個(gè)例子:你和你女朋友吵架,你們開(kāi)始互掐頭發(fā),你揪著她秀長(zhǎng)的長(zhǎng)發(fā),而她揪著你寸頭,你們倆疼痛不比,但是彼此都是暴脾氣,彼此發(fā)狠的說(shuō)你放不放,不放我也不放,看先疼死了!此時(shí)你們互相觀望著對(duì)方,喊著誓不放手。現(xiàn)在就相當(dāng)產(chǎn)生了死鎖現(xiàn)象,互相等待。
我們來(lái)簡(jiǎn)單演示下線程死鎖的代碼吧:
public class DeadlockDemo { public static void main(String[] args) { MyRunnable myRunnable = new MyRunnable(); new Thread(myRunnable, "看戲觀眾1看到").start(); new Thread(myRunnable, "看戲觀眾2看到").start(); } } class MyRunnable implements Runnable { Object me = new Object(); Object myGirl = new Object(); @Override public void run() { synchronized (me) { System.out.println(Thread.currentThread().getName() + "me:我要掐死你!你放不放呀"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "me:你放手我就放手!"); synchronized (myGirl) { System.out.println(Thread.currentThread().getName() + "me:你倒是快放手呀!你不疼嗎?"); } } synchronized (myGirl) { System.out.println(Thread.currentThread().getName() + "myGirl:老娘我才要掐死你!你還不放?你今晚誰(shuí)地板吧!"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "myGirl:是你放手才對(duì)!"); synchronized (me) {// t1 , objB, 拿不到A鎖,等待 System.out.println(Thread.currentThread().getName() + "myGirl:看來(lái)你今晚要睡地板啊"); } } } }
先看看結(jié)果:
首先可以看到我們的進(jìn)程還在運(yùn)行狀態(tài),但是都不往下運(yùn)行代碼了,為什么呢,我們來(lái)分析來(lái):
此時(shí)的狀態(tài)是線程1在執(zhí)行第二個(gè)synchronized (myGirl) 這個(gè)同步代碼塊里中,還在等待me放手的時(shí)候,此時(shí)線程2又進(jìn)來(lái)了,執(zhí)行了第一個(gè)synchronized (me)這一個(gè)同步代碼塊中,相當(dāng)于把me這個(gè)鎖鎖住了。
而線程1此時(shí)想進(jìn)入第二個(gè)synchronized (me)的時(shí)候,發(fā)現(xiàn)這個(gè)me的鎖被人拿了就陷入等待狀態(tài),而線程2發(fā)現(xiàn)要進(jìn)入到第一個(gè)synchronized (myGirl) 中,myGirl又被線程1拿住了,也陷入了等待狀態(tài)。此時(shí)狀態(tài)就是兩個(gè)人在互相等待對(duì)方結(jié)束釋放鎖,陷入了無(wú)限等待的狀態(tài)。
而產(chǎn)生死鎖的必要條件有:
兩個(gè)或兩個(gè)以上的線程在執(zhí)行過(guò)程中,因爭(zhēng)奪資源而造成了互相等待的狀態(tài)。
- 互斥條件:線程對(duì)于所分配到的資源具有排它性,即一個(gè)資源只能被一個(gè)線程占用,直到被該線程(進(jìn)程)釋放。
- 請(qǐng)求與保持條件:一個(gè)線程因請(qǐng)求被占用資源而發(fā)生阻塞時(shí),對(duì)已獲得的資源保持不放,即一個(gè)線程在已經(jīng)有一個(gè)資源的資格后,又提出了新的資源請(qǐng)求。
- 不剝奪條件:線程已獲得的資源在末使用完之前不能被其他線程強(qiáng)行剝奪,只有自己使用完畢后才釋放資源。
- 循環(huán)等待條件:當(dāng)發(fā)生死鎖時(shí),所等待的線程必定會(huì)形成一個(gè)資源的環(huán)形鏈(類似于死循環(huán)),造成永久阻塞。
總結(jié)
這篇文章就到這里了,如果這篇文章對(duì)你也有所幫助,希望您能多多關(guān)注腳本之家的更多內(nèi)容!
相關(guān)文章
SpringSecurity實(shí)現(xiàn)前后端分離登錄token認(rèn)證詳解
目前市面上比較流行的權(quán)限框架主要實(shí)Shiro和Spring Security,這兩個(gè)框架各自側(cè)重點(diǎn)不同,各有各的優(yōu)劣,本文將給大家詳細(xì)介紹SpringSecurity如何實(shí)現(xiàn)前后端分離登錄token認(rèn)證2023-06-06IntelliJ IDEA報(bào)錯(cuò)Error:java: Compilation failed: internal java
今天小編就為大家分享一篇關(guān)于IntelliJ IDEA報(bào)錯(cuò)Error:java: Compilation failed: internal java compiler error的解決辦法,小編覺(jué)得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2018-10-10SpringCloud使用Kafka Streams實(shí)現(xiàn)實(shí)時(shí)數(shù)據(jù)處理
使用Kafka Streams在Spring Cloud中實(shí)現(xiàn)實(shí)時(shí)數(shù)據(jù)處理可以幫助我們構(gòu)建可擴(kuò)展、高性能的實(shí)時(shí)數(shù)據(jù)處理應(yīng)用,Kafka Streams是一個(gè)基于Kafka的流處理庫(kù),本文介紹了如何在SpringCloud中使用Kafka Streams實(shí)現(xiàn)實(shí)時(shí)數(shù)據(jù)處理,需要的朋友可以參考下2024-07-07全網(wǎng)最全最細(xì)的jmeter接口測(cè)試教程以及接口測(cè)試流程(入門教程)
本文主要介紹了全網(wǎng)最全最細(xì)的jmeter接口測(cè)試教程以及接口測(cè)試流程,文中介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-11-11Java操作redis實(shí)現(xiàn)增刪查改功能的方法示例
這篇文章主要介紹了Java操作redis實(shí)現(xiàn)增刪查改功能的方法,涉及java操作redis數(shù)據(jù)庫(kù)的連接、設(shè)置、增刪改查、釋放資源等相關(guān)操作技巧,需要的朋友可以參考下2017-08-08如何使用Maven管理項(xiàng)目?Maven管理項(xiàng)目實(shí)例
下面小編就為大家?guī)?lái)一篇如何使用Maven管理項(xiàng)目?Maven管理項(xiàng)目實(shí)例。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-06-06淺談SpringBoot 中關(guān)于自定義異常處理的套路
這篇文章主要介紹了淺談SpringBoot 中關(guān)于自定義異常處理的套路,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04Maven在Windows中的配置以及IDE中的項(xiàng)目創(chuàng)建(圖文教程)
這篇文章主要介紹了Maven在Windows中的配置以及IDE中的項(xiàng)目創(chuàng)建(圖文教程),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-09-09