關于線程與多線程的較詳細的理解可以參考:線程的解釋 和多線程的解釋。
而我們要做的是,對其進行“精煉"。我們每天都在和電腦、手機打交道,每天都在使用各種各樣的利用軟件。
打開上電腦的任務管理器,就能夠看到有1項名為"進程"的欄目,點擊到里面可能就會發現1系列熟習的名稱:QQ,360等等。
所以首先知道了,QQ、360之類的利用軟件在計算機上被稱為1個進程。
而1個利用程序都會有自己的功能,用以履行這些進程當中的個別功能的程序履行流就是所謂的線程。
所以,線程有時候也被稱為輕量級進程,是程序履行流當中的最小單元。
線程的劃分尺度小于進程,其不能夠獨立履行,必須依存在利用程序中,由利用程序提供多個線程履行控制。
進程在履行進程中具有獨立的內存單元,而多個線程同享內存,所以能極大地提高了程序的運行效力。
所以簡而言之的概括的話,就是:1個程序最少有1個進程,1個進程最少有1個線程。
以360殺毒來講,里面的1項功能任務“電腦體檢”就是該利用程序進程中的1個線程任務。
而除此任務以外,我們還可以同時進行多項操作。例如:“木馬查殺”、“電腦清算”等。
那末,以上同時進行的多項任務就是所謂的存活在360利用程序進程中的多線程并發。
多線程的有益的地方,不言而喻。在傳統的程序設計語言中,同1時刻只能履行單任務操作,效力非常低。
假定在某個任務履行的進程中產生梗塞,那末下1個任務就只能1直等待,直至該任務履行完成后,才能接著履行。
而得益于多線程能夠實現的并發操作,即便履行進程中某個線程因某種緣由產生阻塞,也不會影響到其它線程的履行。
也就是說,多線程并發技術帶來的最大好處就是:很大程度上提高了程序的運行效力。
似乎百里而無1害的多線程并發技術,還有弊端嗎?從某種程度上來講,也是存在的:會致使任務履行效力的下降。
之所以這樣講,是由于所謂的“并發”其實不是真正意義上的并發,而是CPU在多個線程之間做著快速切換的操作。
但CPU的運算速度肯定是遠遠高于人類的思惟速度,所以就帶來了1種“并發”的錯覺。
那就不難想象了:假定某1進程中,線程A與線程B并發履行,CPU要做的工作就是:
不斷快速且隨機的在兩個線程之間做著切換,分別處理對應線程上的線程任務,直到兩個線程上的任務都被處理完成。
那末,也就能夠斟酌這樣的情況:CPU履行完本來線程A的線程任務只需要5秒;但如今由于另外一個線程B的并發加入。
CPU則不能不分出1部份時間切換到線程B上進行運算處理。因而可能CPU完成該線程任務A的時間反而延長到了7秒。
所以所謂的效力下降,就是指針對某單個任務的履行效力而言的。
也就是說,如果在多線程并發操作時,如果有某個線程的任務你認為優先級很高。那末則可以:
通過設置線程優先級或通過代碼控制等手段,來保證該線程享有足夠的“特權”。
注:Java中設置線程優先級,實際上也只是設置的優先級越大,該線程被CPU隨機訪問到的幾率會相對高1些。
這個進程可以替換成1些實際生活中的情形來進行思考。快過年了,以家庭團圓為例。
假定你除準備炒1桌子美味的菜肴以外,過年自然還要有1頓熱騰騰的餃子。那末:
傳統單任務的操作進程可以被理解為:先把準備的菜肴都做好;菜都端上桌后便開始煮餃子。
這樣做的壞處就是:如果在炒菜的中途產生1些意外情況,那末隨著炒菜動作的暫停。煮餃子的動作也將被無窮期延后。
而對應于多線程并發的操作就是:1邊炒菜,1邊煮餃子。這時候你就是CPU,你要做的動作多是這樣的:
炒菜的中途你能會抽空去看看鍋里的餃子煮好沒有;發現沒有煮好,又回來繼續炒菜。炒好1道菜后,再去看看餃子能出鍋了沒。
由此你發現,你做的工作與CPU處理多線程并發的工作是1樣的:不斷的在“煮餃子”與“炒菜”兩個任務之間做著切換。
Java中線程的全部生命周期基本可以劃分為以下4種狀態:
以下是Java中1些用于改變線程狀態的方法列表:
開發中通常選擇通過實現Runnbale接口的方式創建線程,好處在于:
1.Java中不支持多繼承,所以使用Runnable接口可以免此問題。
2.實現Runnable接口的創建方式,等因而將線程要履行的任務單獨分離了出來,更符合OO要求的封裝性。
春運將至了,還是先通過1個老話題來看1個多線程并發的例子,來看看多線程可能存在的安全隱患。
而根據程序的輸出結果,我們發現的安全隱患是:有坐位號為0號的車票被售出了,買到這張車票的顧客該找誰說理去呢?
我們來分析1下為何會出現這樣的毛病情況,其構成的緣由多是這樣的:
當線程1履行完“1號售票窗口售出了2號票”以后,根據while循環的規則,再1次開始售票工作。
首先判斷while為true,進入到while循環體;接著判斷if語句,此時余票數為1張(也就是只剩下坐位號為1的車票了)。
1大于0,滿足判斷條件,進入到if語句塊當中。此時履行到"Thread.sleep(10)"語句。
OK,當前線程進入到梗塞狀態,暫時失去了Cpu的履行資格。因而Cpu重新切換,開始履行線程2。
因而線程2開始履行線程任務,又是老模樣:while判斷 - if判斷,由于上次線程1判斷后還沒履行售票工作,就被阻塞了。
所以這次if判斷依然為"1>0",滿足判斷條件,繼續履行,又履行到線程休眠語句,因而線程2也進入阻塞狀態。
此時兩個線程暫時都不具有履行資格,但我們指定線程休眠的時間為10毫秒,因而10毫秒后,可能兩個線程都蘇醒了,恢復了Cpu的履行資格。
面對兩個都處于可履行狀態的線程,Cpu又只好隨機選擇1個先履行了。因而Cpu選擇了線程2,線程2恢復履行。
線程2開始做自己上次沒做完的事,因而履行表達式和輸出語句,因而得到輸出信息"2號售票窗口售出了1號票"。
線程2繼續履行while判斷,沒問題。再履行if判斷"0>0",不滿足判斷條件,因而履行到了break語句。
線程2到此退出循環,完成了所有線程任務,因而自然滅亡進入done狀態。
因而現在Cpu的履行權自然就屬于線程1了,線程1也猶如線程21樣,從美夢中醒來,開始上次沒做完的事。
問題就在這里出現了,雖然這個時候,堆內存中寄存的對象成員變量“ticket_num”的值實際上已是0了。
但是!由于上1次線程1已經過了if判斷進入到了if語句塊以內。所以它將直接開始履行表達式,并輸出。
就構成了我們看到的毛病信息:“1號售票窗口售出了0號票”。并且這個時候實際上余票數的值已是“⑴”了。
所以,實際上之所以我們在處理賣票的代碼之前加上讓線程休眠10毫秒的代碼,目的也就是為了摹擬線程安全隱患的問題。
而根據這個例子我們能夠得到的信息就是:之所以多線程并發存在著安全隱患,正是CPU的實際處理方式是在不同線程之間做著隨機的快速切換。
這意味著它其實不會保證當處理1個線程的任務時,1定會履行完該次線程的所有代碼才做切換。而是可能做到1半就切換了。
所以,我們可以歸納線程安全隱患之所以會出現的緣由就是由于:
既然已了解了線程安全隱患之所以產生,就是由于線程在操作同享數據的途中,其它線程被參與了進來。
那末我們想要解決這1類的安全隱患,自然就是保證在某個線程在履行線程任務的時候,不能讓其余線程來搗亂。
在樣的做法,在Java當中被稱為同步鎖,也就是說給封裝在同步當中的代碼加上1把鎖。
每次只能由1個線程能夠獲得到這把鎖,只有當前持有鎖的線程才能履行同步當中的代碼,其它線程將被拒之門外。
Java中對同步的使用方式通常分為兩種,即:同步代碼塊和同步函數。關鍵字synchronized用以聲明同步。其格式分別為:
這正是由于我們通過同步代碼塊,將希望每次只有有1個線程履行的代碼封裝了起來,為它們加上了1把同步鎖(對象)。
同步最需要注意的地方,就是要保證鎖的1致性。這是由于我們說過了:
同步的原理就是鎖,每次當有線程想要訪問同步當中的代碼的時候,只有獲得到該鎖才能履行。
所以如果鎖不能保證是同1把的話,自然也就實現不了所謂的同步了。
可以試著將定義在TicketOffice的成員變量objLock移動定義到run方法當中,就會發現線程安全問題又出現了。
這正是由于,將對象類型變量objLock定義為成員變量,它會隨著該類的對象存儲在堆內存當中,該變量在內存中獨此1份。
而移動到run方法內,則會存儲在棧內存當中,而每個線程都會在棧內存中,單獨開辟1條方法棧。
這樣就等于每一個線程都有1把獨自的鎖,自然也就不是所謂的同步了。
而同步函數的原理實際上與同步代碼塊是相同的,不同的只是將本來包括在同步代碼塊當中的代碼單獨封裝到1個函數中:
同步代碼塊:可使用任1對象鎖。
同步函數:使用this作為鎖。
靜態同步函數:使用該函數所在類的字節碼文件對象作為鎖。
提到同步,就不能不提到與之相干的1個概念:死鎖。
死鎖是指兩個或兩個以上的進程在履行進程中,因爭取資源而釀成的1種相互等待的現象,若無外力作用,它們都將沒法推動下去。
此時稱系統處于死鎖狀態或系統產生了死鎖,這些永久在相互等待的進程稱為死鎖進程。同理,線程也會出現死鎖現象。
線程1開啟履行后,判斷標記為true,因而先獲得了鎖A,并輸出信息。
此時CPU做切換,線程2開啟履行,判斷標記為false,首先獲得鎖B,并輸出相干信息。
但這時候候不管CPU再怎樣樣切換,程序都已沒法繼續推動了。
由于線程1想要繼續推動必須獲得的資源鎖B現在被線程2持有,反之線程2需要的鎖A被線程1持有。
這正是由于兩個線程由于相互爭取資源而釀成的死鎖現象。
死鎖還是很蛋疼的,1旦出現,程序的調試和查錯修改工作都會變得很麻煩
關于多線程編程,類似于車站賣票的例子是1種常見的使用處徑。
這類利用途徑通常為:多個線程操作同享數據,并且履行的是同1個動作(線程任務)。
車站售票:多個線程都是操作同1組車票,并且都是履行同1個動作:出售車票。
那末在多線程當中的另外一個經典例子:生產者與消費者,就描寫的是另外一種常見的利用途徑。
不同線程間的通訊應當怎樣樣來完成,其手段是通過Object類當中提供的幾個相干方法:
notify()
方法或notifyAll()方法前,致使當前線程等待。首先,我們可能會思考的1點就是:既然是針對線程之間相互通訊的方法,為何沒有被定義在線程類,反而被定義在了Object類當中。
由于這些方法事實上我們可以視作是線程監視器的方法,監視器其實就是鎖。
我們知道同步中的鎖,可以是任意的對象,那末既然是任1對象調用的方法,自然1定被定義在Object類中。
可以將所有使用同1個同步的線程視作被存儲在同1個線程池當中,而該同步的鎖就是該線程池的監視器。
由該監視器來調度對應線程池內的各個線程,從而到達線程通訊的目的。
接下來就來看生產者與消費者的例子:
1.生產者生產商品;
2.消費者購買商品。
3.可能會同時存在多個生產者與多個消費者。
4.多個生產者中某個生產者生產1件商品,就暫停生產,并在多個消費者中通知1個消費者進行消費;
消費者消費掉商品后,停止消費,再通知任逐一個生產者進行新的生產工作。
這就是對線程通訊1個簡單的利用。而需要記住的是:關于線程的停止與喚醒都必須定義在同步中。
由于我們說過了,關于所謂的線程通訊工作。實際上是通過監視器對象(也就是鎖),來完成對線程的停止或喚醒的操作的。
既然使用的是鎖,那末自然必須被定義在同步中。并且,必須確保相互通訊的線程使用的是同1個鎖。
這是10分重要的,試想1下,如果試圖用線程池A的監視器鎖A去喚醒另外一個線程池B內的某1個線程,這自然是辦不到的。
簡單解釋下,你可能已注意到在上面的例子中,我是直接采取"wait()"和"notifyAll()"的方式來喚醒和阻塞線程的。
那末你應當明白這其實對應于隱式的"this.wait()"與"this.notifyAll()",而同時我們已說過了:
在同步方法中,使用的鎖正是this。也就是說,在線程通訊中,你可以將同步鎖this看作是1個線程池的對象監視器。
當某個線程履行到this.wait(),就代表它在該線程池內阻塞了。而通過this.notify()則可以喚醒阻塞在這個線程池上的線程。
而到了這里,另外一值得1提的1點就是:
Thread類的sleep()方法和Object類的wait()方法都可使當前線程掛起,而它們的不同的地方在于:
1:sleep方法必須線程掛起的時間,超過指定時間,線程將自動從掛起中恢復。而wait方法可以指定時間,也能夠不指定。
2:線程調用sleep方法會釋放Cpu的履行資格(也就是進入到non Runnable狀態),但不會釋放鎖;
而通過調用wait方法,線程即會釋放cpu的履行資格,同時也會釋放掉鎖。
與之前說過的賣票用例1樣,對線程通訊的通訊也應當謹慎謹慎,否則也可能會引發相干的毛病。常見的問題例如:
1、使用notify而不是notifyAll喚醒線程可能會出現的問題
我在最初接觸多線程的時候,容易這樣斟酌,既然想要到達的目的是:生產者線程生產1件商品,則喚醒1個消費者線程。消費者進行消費,則喚醒1個生產者線程。
既然notify()方法用于喚醒單個線程,而notifyAll()用于喚醒所有線程,那使用notifyAll不是浪費效力嗎?
后來明白,很惋惜的是,我們要做的是喚醒單個對方線程。而notify沒有這么強大。
它只是隨機的喚醒1個處于阻塞狀態下的線程,所以如果使用notify(),可能會看到以下的毛病情況:
沒錯,操蛋,又出現了坑爹的死鎖。為何出現這樣的情況呢?我們來分析1下:
履行到此,當前處于可履行狀態的線程為:生產者2、消費者1、消費者2
因而,當前處于可履行狀態的線程變成了:消費者1、消費者2
到此,當前處于可履行狀態的線程變成了:生產者2、消費者2
好了,處于可履行狀態的線程只剩下:生產者2。
至此,唯1處于可履行狀態的線程變成了:生產者1
這下好了,4個線程都進入了阻塞狀態,而不是滅亡狀態。自然的,死鎖了。
2、使用if而不是使用while判斷isEmpty可能出現的問題
如果使用if而不是while對isEmpty進行判斷,可能會出現的毛病為:
1、不同的生產者連續生產了多件商品,但消費者只消費掉其中1件。
2、1個生產者生產了1件商品以后,有多個消費者進行連續消費。
出現這樣的安全問題是由于if的判斷機制釀成的:通過if來判斷標記,只會履行1次判斷。
所以可能會致使不該運行的線程運行了,從而出現數據毛病的情況。
這類問題的出現也就是與我們上面說的“售票處售出0號票”的毛病類似。
我們前面已說到了,關于生產者與消費者的問題中。
我們的目的是,每當1個線程履行終了1次任務后,只喚醒單1的對方線程。
而在JDK1.5之前,為了不死鎖的產生,我們不能不使用notifyAll()來喚醒線程。
而這樣做有1個缺點就在于:每次都要喚醒所有處于阻塞的線程,自然就會致使效力下降。
在JDK1.5以后,,Java提供了新的工具用于解決此類問題,就是:Lock和Condition接口。
簡答的說,就是對將本來的同步鎖synchronized與對象監視器進行了封裝,分別對應于于Lock及Condition。
并且,重要的是相對1.5之前,新的工具具有更靈活及更廣泛的操作。
1、Lock的使用及注意事項
1、通過Lock lock = new ReentrantLock();獲得1個Lock對象。
2、通過成員方法lock(),用于對代碼進行同步管理。
3、通過成員方法unlock(),用于同步代碼履行終了后,釋放鎖對象。
4、由于不管在同步代碼的履行進程中是不是出現異常,最后都必須釋放該鎖,否則可能會致使死鎖現象的產生。所以通常在使用lock時,都會遵守以下格式:
lock.lock();
try{
{
// 同步代碼....
}finally{
lock.unlock();
}
}
2、對象監視器Condition的使用及注意事項
1、可以通過Lock對象使用成員方法newCondition()來獲得1個新的監視器對象。
2、Condition分別使用await();signal();signalAll()來替換本來Object類當中的wait();notify();及notifyAll()方法。
3、同1個Lock對象可以具有多個不同的Condition對象。
請注意1個很關鍵的特性:同1個Lock對象可以具有多個不同的Condition對象!
也就是說:通過此特性,我們可以獲得多個Condition對象,將操作不同線程任務的線程分別寄存在不同的Condition對象當中。
例如在前面所說的生產者消費者例子當中,我們就能夠生成兩組監視器,1組監視生產者線程,1組監視消費者線程。
從而到達我們想要的每次只喚醒對方線程而不喚醒本方線程的目的,修改后的例子代碼以下:
最后,看1下1些關于線程的經常使用方法。
1、線程的中斷工作
1、通常使用自然中斷的做法,也就是當某個線程的線程任務履行結束以后,該線程就會自然終結。
2、通過標記控制。如果線程任務中存在循環(通常都有),那末,可以在循環中使用標記,通過標記來控制線程的中斷。
2、interrupt()方法:中斷線程
我們知道sleep及wait等方法都可使線程進入阻塞狀態。所以可能你在程序通過使用標記的方式來控制線程的中斷,但由于進程中線程墮入了
凍結(掛起/阻塞)狀態,這時候通過標記將沒法正常的控制線程中斷。這時候,就能夠通過interrupt方法來中斷線程的凍結狀態,強迫恢復到運行狀態中來,讓線程具有cpu的履行資格。但是由于此方法具有強迫性,所以會引發InterruptedException,所以要記得處理異常。
3、setDaemon()方法:將該線程標記為守護線程或用戶線程。
所謂守護線程,可以理解為后臺線程。對應的,我們在程序中開辟的線程都可以視為前臺線程,在Java中,當所有的前臺線程都履行結束以后,后臺線程也將隨之結束。
例如:你在某個程序中開辟兩個線程,1個用于接收輸入,1個用于控制輸出。由于只有當有輸入存在時,才會存在輸出。這時候就能夠通過setDaemon將輸出線程設置為守護線程。這樣當輸入線程中斷結束時,輸出線程就會隨之自動中斷,而沒必要再人為控制中斷。
4、控制線程優先級
所謂控制線程優先級,是指我們可以通過設置線程的優先級來控制線程被CPU運行到的概率,線程的優先級越高,被CPU運行的幾率越大。
通過setPriority()與getPriority()方法可以分別設置和獲得某個線程的優先級。Java中線程的優先級取值范圍為:1⑴0
Thread類中使用MAX_PRIORITY(10),NORM_PRIORITY(5),MIN_PRIORITY(1)3個常量代表最經常使用的線程優先級值。
5、join()方法
線程使用join方法,意味著該線程申請加入履行,所以通常如果要臨時加入1個線程,可使用join()方法。并且,當履行到join方法以后,其余線程將等待使用該方法的線程履行完線程任務以后,再繼續履行。
6、yiled()方法
暫停正在履行的線程對象,并履行其他線程。
上一篇 灰度圖像--頻域濾波 概論