在Java中引入多線程的目的顯而易見,當程序中有多部分代碼需要同時執行,這時便需要引入多線程,將需要同時執行的代碼作為線程任務(并發任務),來達到目的。
同時在這里,還有必要搞清一個問題,什么是同時執行?從字面意思來理解,就是同時執行!而事實上,是CPU瞬間在各個線程之間做著快速切換,這種切換在引入時間片技術的OS系統中,是按照時間片的技術來完成的。
在這行main函數中內容的同時,垃圾回收器的線程,同時執行!
線程創建的方式有兩種
方式一:繼承Thread類,覆蓋run()方法
步驟:
1.定義類覆蓋Thread類;
2.覆蓋Thread類中的run方法;
3.創建Thread類的子類對象創建線程對象;
4.調用線程對象的start()方法,開啟線程。
方式二:實現Runnable接口,重寫run()方法
1.定義一個類實現Runnable接口;
2.覆蓋Runnable接口中的run()方法,將線程要運行的代碼存儲到run()方法中;
3.創建該接口的子類對象;
4.通過Thread類進行線程的創建,并將Runnable接口的子類作為Thread構造參數的實參進行傳遞;
5.調用Thread類的start()開啟線程。
兩種創建方式的區別:
方式一:繼承自Thread類,這就會收到Java單繼承這種局限性的影響,當我們如果想擴展這個線程子類的功能時就會收到很大的
限制
方式二:實現Runnable接口,則可以避免方式一的局限性,極大的降低了任務對象和Thread對象的耦合,更加符合Java面向對象
編程的思想
線程安全問題
說到線程安全,我會想到售票窗口售票的情況,四個售票員同時出售這100張票,用Java程序來表示的話,就是四個線程共享一個數據,當其中一個線程在處理多條操作共享數據的過程中,其他線程參與了運算,這時就會發生線程安全問題。
線程安全的解決辦法?
只要保證一個線程在執行多條操作共享數據的語句時,其他線程不能參與運算即可,當該線程都執行完后,其他線程才可執行這些語句。
鎖--同步代碼塊,同步函數,靜態同步函數使用的鎖:
1.同步代碼塊使用的鎖是:任意的對象;
2.同步函數使用的鎖是:this,this代表當前對象的引用
3.靜態--當一個類被加載進內存以后,在我們還沒有創建對象的時候,已經有了xxx.class
靜態隨著類的加載而加載,這時內存中存儲的對象至少有一個就是該類字節碼文件對象
這個對象的表示方式:類名.class
靜態同步函數的應用場景:單例模式
單例設計模式:保證一個類在內存中只能有一個對象
怎樣才能保證對象是唯一的呢?
1.其他程序隨時用new創建該類對象,無法控制個數;
2.不讓其他程序創建,該類在本類中自己創建一個對象;
3.該類將創建的對象對位提供,讓其他程序獲取并使用。
步驟:
1.怎么實現不讓其他程序創建該類對象呢?將該類中的構造函數私有化
2.在本類中創建一個本類對象,并私有化
3.定義一個方法,返回值類型是本類類型,讓其他程序通過該方法就可以獲取本類對象
懶漢式:
class Single{
private static Single s=null;
private Single(){}
public static Single getInstance(){
if(s==null){
s=new Single();
return s;
}
}
}
在并發訪問單例的時候,懶漢式單例會被破壞,有可能不能保證對象的唯一性
例如:1線程進來后經過判斷發現為null,正在準備創建對象時,CPU切換到了2線程;
2線程進來后,經過判斷,發現也為null,正在準備創建對象時;
CPU又切換到了1線程,new Single();
此時CPU又切換到了2線程,也new Single();
這就不能保證了對象的唯一性。
解決方案?同步
class Single{
private static Single s=null;
private Single(){}
public static synchronized Single getInstance(){
if(s==null){
s=new Single();
}
return s;
}
}
但是,這樣又帶來一個新的問題:當程序中線程增多后,對鎖的判斷次數就會增多,從而影響程序的性能
那么,如何做到既保證了多線程的安全性,又可以提高程序的性能呢?那就要減少后續對鎖的判斷次數。
class Single{
private static Single s=null;
private Single(){}
private static Single getInstance(){
if(s==null){
synchronized(Single.class){
if(s=null){
s=new Single();
}
}
}
returns;
}
}
死鎖?
指多個進程or線程在運行過程中因爭奪資源而造成的一種僵局,當線程處于這種僵局時,若無外力作用,他們
無法繼續往前執行。
最常見的死鎖的情況:同步的嵌套
我們應當,盡量避免同步的嵌套情況
Thread類的方法:
run()線程中真正起作用的語句放在run()的方法體內,該方法在Thread的子類中覆蓋或在Runnable對象覆蓋
start()程序通過調用線程的start()方法執行線程,而start()則調用run()方法
sleep(long)它的一個參數指出當前執行的線程應休眠多長時間
interrupt()用于中斷一個線程
線程的生命周期
出生:新創建的線程處于出生born狀態,在調用線程的start方法之前,該線程一直處于出生狀態;
就緒:當調用start方法后,線程便進入了就緒狀態ready
運行:當系統給線程分配處理器資源時,處于就緒狀態的最高優先級線程便進入了運行狀態
死亡:當線程的run方法結束或拋出一個未捕獲的異常,那么線程便進入死亡狀態
阻塞:如果處于運行狀態的線程發出IO請求,它便進入了阻塞狀態,當其等待的IO操作結束后,阻塞的線程便進入了就緒狀態
等待:當處于運行狀態的線程調用wait方式,線程便進入等待狀態。它將按次序排在等待隊列中,隊列中的線程均是由于某個對象掉哦那個了wait方法才進入等待狀態的當與某對象相關的另一個線程調用了notify方法是,那么等待該對象所有線程都回到就緒狀態。
休眠:當程序調用一個正在運行的線程的sleep方法時,該線程便會進入休眠狀態
多線程之間的通信
多個線程在處理同一個資源,但是處理的動作(線程的任務)不同,通過一定的手段使各個線程能有效的利用資源,而這種手段就是等待喚醒機制。
等待喚醒機制所涉及到的方法:
Wait():等待,將正在執行的線程釋放其執行資格和執行權,并存儲到線程池中
Notify():喚醒,喚醒線程池中被wait()的線程,一次只能喚醒其中的任意一個
NotifyAll():喚醒全部,可以將線程池中的所有wait()線程都喚醒
其實,所謂喚醒的意思就是讓線程池中的線程具備執行資格,必須注意的是,這些方法都是在同步中有效的,同時這些方法在使用時必須標明所屬鎖,這樣才可以明確出這些方法操作的到底是哪個鎖上的線程
案例:生產者和消費者的問題
生產者生產饅頭,消費者消費饅頭….
當生產者發現沒有饅頭時,就會開始生產,生產完成后,叫消費者消費,如果發現有饅頭,就會wait();
當消費者發現沒有饅頭時,就會wait(),當發現有饅頭時,就消費,然后叫生產者繼續生產。
JDK1.5后出現的新的接口和類:Lock:比同步函數和同步代碼塊要好一些,同步函數還是同步代碼塊所做的都是隱式的操作。并且,同步函數或者同步代碼塊使用的鎖和監視器是同一個。
Lock接口:是將鎖進行單獨對象的封裝,而且提供了對鎖對象很多功能,比如:lock()獲取鎖,unlock()釋放鎖。Lock對鎖的操作都是顯示操作。所以它的出線要比同步函數或者同步代碼塊明確的多,更符合面向對象的思想