JAVA多線程,面試者的最愛

線程:是指進程中的一個執行流程。
線程與進程的區別:每個進程都需要操作系統爲其分配獨立的內存地址空間,而同一進程中的所有線程在同一塊地址空間中工作,這些線程可以共享同一塊內存和系統資源。
如何創建一個線程?
創建線程有兩種方式,如下:
1、 擴展java.lang.Thread類
2、 實現Runnable接口
Thread類代表線程類,它的兩個最主要的方法是:
run()——包含線程運行時所執行的代碼
Start()——用於啓動線程
一個線程只能被啓動一次。第二次啓動時將會拋出java.lang.IllegalThreadExcetpion異常
線程間狀態的轉換(如圖示)
新建狀態:用new語句創建的線程對象處於新建狀態,此時它和其它的java對象一樣,僅僅在堆中被分配了內存
就緒狀態:當一個線程創建了以後,其他的線程調用了它的start()方法,該線程就進入了就緒狀態。處於這個狀態的線程位於可運行池中,等待獲得CPU的使用權
運行狀態:處於這個狀態的線程佔用CPU,執行程序的代碼
阻塞狀態:當線程處於阻塞狀態時,java虛擬機不會給線程分配CPU,直到線程重新進入就緒狀態,它纔有機會轉到運行狀態。
阻塞狀態分爲三種情況:
1、 位於對象等待池中的阻塞狀態:當線程運行時,如果執行了某個對象的wait()方法,java虛擬機就回把線程放到這個對象的等待池中
2、 位於對象鎖中的阻塞狀態,當線程處於運行狀態時,試圖獲得某個對象的同步鎖時,如果該對象的同步鎖已經被其他的線程佔用,JVM就會把這個線程放到這個對象的瑣池中。
3、 其它的阻塞狀態:當前線程執行了sleep()方法,或者調用了其它線程的join()方法,或者發出了I/O請求時,就會進入這個狀態中。
死亡狀態:當線程退出了run()方法,就進入了死亡狀態,該線程結束了生命週期。
或者正常退出
或者遇到異常退出
Thread類的isAlive()方法判斷一個線程是否活着,當線程處於死亡狀態或者新建狀態時,該方法返回false,在其餘的狀態下,該方法返回true.
線程調度
線程調度模型:分時調度模型和搶佔式調度模型
JVM採用搶佔式調度模型。
所謂的多線程的併發運行,其實是指宏觀上看,各個線程輪流獲得CPU的使用權,分別執行各自的任務。
(線程的調度不是跨平臺,它不僅取決於java虛擬機,它還依賴於操作系統)
如果希望明確地讓一個線程給另外一個線程運行的機會,可以採取以下的辦法之一
1、 調整各個線程的優先級
2、 讓處於運行狀態的線程調用Thread.sleep()方法
3、 讓處於運行狀態的線程調用Thread.yield()方法
4、 讓處於運行狀態的線程調用另一個線程的join()方法
調整各個線程的優先級
Thread類的setPriority(int)和getPriority()方法分別用來設置優先級和讀取優先級。
如果希望程序能夠移值到各個操作系統中,應該確保在設置線程的優先級時,只使用MAX_PRIORITY、NORM_PRIORITY、MIN_PRIORITY這3個優先級。
線程睡眠:當線程在運行中執行了sleep()方法時,它就會放棄CPU,轉到阻塞狀態。
線程讓步:當線程在運行中執行了Thread類的yield()靜態方法時,如果此時具有相同優先級的其它線程處於就緒狀態,那麼yield()方法將把當前運行的線程放到運行池中並使另一個線程運行。如果沒有相同優先級的可運行線程,則yield()方法什麼也不做。
Sleep()方法和yield()方法都是Thread類的靜態方法,都會使當前處於運行狀態的線程放棄CPU,把運行機會讓給別的線程,兩者的區別在於:
1、sleep()方法會給其他線程運行的機會,而不考慮其他線程的優先級,因此會給較低線程一個運行的機會;yield()方法只會給相同優先級或者更高優先級的線程一個運行的機會。

2、當線程執行了sleep(long millis)方法後,將轉到阻塞狀態,參數millis指定睡眠時間;當線程執行了yield()方法後,將轉到就緒狀態。

3、sleep()方法聲明拋出InterruptedException異常,而yield()方法沒有聲明拋出任何異常
4、sleep()方法比yield()方法具有更好的移植性
等待其它線程的結束:join()
當前運行的線程可以調用另一個線程的 join()方法,當前運行的線程將轉到阻塞狀態,直到另一個線程運行結束,它才恢復運行。
定時器Timer:在JDK的java.util包中提供了一個實用類Timer, 它能夠定時執行特定的任務。
線程的同步
原子操作:根據Java規範,對於基本類型的賦值或者返回值操作,是原子操作。但這裏的基本數據類型不包括long和double, 因爲JVM看到的基本存儲單位是32位,而long 和double都要用64位來表示。所以無法在一個時鐘週期內完成。
自增操作(++)不是原子操作,因爲它涉及到一次讀和一次寫。
原子操作:由一組相關的操作完成,這些操作可能會操縱與其它的線程共享的資源,爲了保證得到正確的運算結果,一個線程在執行原子操作其間,應該採取其他的措施使得其他的線程不能操縱共享資源。
同步代碼塊:爲了保證每個線程能夠正常執行原子操作,Java引入了同步機制,具體的做法是在代表原子操作的程序代碼前加上synchronized標記,這樣的代碼被稱爲同步代碼塊。
同步鎖:每個JAVA對象都有且只有一個同步鎖,在任何時刻,最多隻允許一個線程擁有這把鎖。
當一個線程試圖訪問帶有synchronized(this)標記的代碼塊時,必須獲得 this關鍵字引用的對象的鎖,在以下的兩種情況下,本線程有着不同的命運。
1、 假如這個鎖已經被其它的線程佔用,JVM就會把這個線程放到本對象的鎖池中。本線程進入阻塞狀態。鎖池中可能有很多的線程,等到其他的線程釋放了鎖,JVM就會從鎖池中隨機取出一個線程,使這個線程擁有鎖,並且轉到就緒狀態。
2、 假如這個鎖沒有被其他線程佔用,本線程會獲得這把鎖,開始執行同步代碼塊。
(一般情況下在執行同步代碼塊時不會釋放同步鎖,但也有特殊情況會釋放對象鎖
如在執行同步代碼塊時,遇到異常而導致線程終止,鎖會被釋放;在執行代碼塊時,執行了鎖所屬對象的wait()方法,這個線程會釋放對象鎖,進入對象的等待池中)
線程同步的特徵:
1、如果一個同步代碼塊和非同步代碼塊同時操作共享資源,仍然會造成對共享資源的競爭。因爲當一個線程執行一個對象的同步代碼塊時,其他的線程仍然可以執行對象的非同步代碼塊。(所謂的線程之間保持同步,是指不同的線程在執行同一個對象的同步代碼塊時,因爲要獲得對象的同步鎖而互相牽制)
2、 每個對象都有唯一的同步鎖
3、 在靜態方法前面可以使用synchronized修飾符。
4、 當一個線程開始執行同步代碼塊時,並不意味着必須以不間斷的方式運行,進入同步代碼塊的線程可以執行Thread.sleep()或者執行Thread.yield()方法,此時它並不釋放對象鎖,只是把運行的機會讓給其他的線程。
5、 Synchronized聲明不會被繼承,如果一個用synchronized修飾的方法被子類覆蓋,那麼子類中這個方法不在保持同步,除非用synchronized修飾。
線程安全的類:
1、 這個類的對象可以同時被多個線程安全的訪問。
2、 每個線程都能正常的執行原子操作,得到正確的結果。
3、 在每個線程的原子操作都完成後,對象處於邏輯上合理的狀態。
釋放對象的鎖:
1、 執行完同步代碼塊就會釋放對象的鎖
2、 在執行同步代碼塊的過程中,遇到異常而導致線程終止,鎖也會被釋放
3、 在執行同步代碼塊的過程中,執行了鎖所屬對象的wait()方法,這個線程會釋放對象鎖,進入對象的等待池。
死鎖
當一個線程等待由另一個線程持有的鎖,而後者正在等待已被第一個線程持有的鎖時,就會發生死鎖。JVM不監測也不試圖避免這種情況,因此保證不發生死鎖就成了程序員的責任。
如何避免死鎖
一個通用的經驗法則是:當幾個線程都要訪問共享資源A、B、C 時,保證每個線程都按照同樣的順序去訪問他們。
線程通信
Java.lang.Object類中提供了兩個用於線程通信的方法
1、 wait():執行了該方法的線程釋放對象的鎖,JVM會把該線程放到對象的等待池中。該線程等待其它線程喚醒
2、 notify():執行該方法的線程喚醒在對象的等待池中等待的一個線程,JVM從對象的等待池中隨機選擇一個線程,把它轉到對象的鎖池中。

發佈了30 篇原創文章 · 獲贊 4 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章