Java較為實用的多線程設計與應用
多線程機制討論
電梯調度算法
請求分配順序
自動化測試方法
- 生成隨機數據
- 驗證輸出數據正確性
- 打包自己的代碼文件
- 運行評測機
線程安全與鎖:https://www.cnblogs.com/zhaojinhui/p/5526988.html
- lock
- condition
- reentrantlock
- Semaohrone信號量
代碼耦合性和內聚性
同一個模塊內的各個元素之間要高度緊密,但是各個模塊之間的相互依存度卻要不那么緊密。 內聚和耦合是密切相關的,同其他模塊存在高耦合的模塊意味着低內聚,而高內聚的模塊意味着該模塊同其他模塊之間是低耦合。在進行軟件設計時,應力爭做到高內聚,低耦合。
降低耦合
一個模塊訪問另一個模塊的內容; 模塊間傳遞數據結構為而不是單一數據
增強內聚
邏輯上獨立模塊; 時間上同時執行的語句可以組合; 模塊的元素只與該模塊功能相關,模塊之間按照順序使用其輸出結果作為輸入結果
多線程介紹
更多1.https://www.cnblogs.com/yjd_hycf_space/p/7526608.html
2.http://www.cnblogs.com/xingele0917/p/3623162.html
Java提供了線程類Thread來創建多線程的程序。其實,創建線程與創建普通的類的對象的操作是一樣的,而線程就是Thread類或其子類的實例對象。每個Thread對象描述了一個單獨的線程。要產生一個線程,有兩種方法:
◆需要從Java.lang.Thread類派生一個新的線程類,重載它的run()方法;
◆實現Runnalbe接口,重載Runnalbe接口中的run()方法。
為什么Java要提供兩種方法來創建線程呢?它們都有哪些區別?相比而言,哪一種方法更好呢?
在Java中,類僅支持單繼承,也就是說,當定義一個新的類的時候,它只能擴展一個外部類.這樣,如果創建自定義線程類的時候是通過擴展 Thread類的方法來實現的,那么這個自定義類就不能再去擴展其他的類,也就無法實現更加復雜的功能。因此,如果自定義類必須擴展其他的類,那么就可以使用實現Runnable接口的方法來定義該類為線程類,這樣就可以避免Java單繼承所帶來的局限性。
還有一點最重要的就是使用實現Runnable接口的方式創建的線程可以處理同一資源,從而實現資源的共享.
線程的狀態:
- 創建:已經有Thread實例了, 但是CPU還有為其分配資源和時間片。
- 就緒:線程已經獲得了運行所需的所有資源,只等CPU進行時間調度。
- 運行:線程位於當前CPU時間片中,正在執行相關邏輯。
- 休眠:一般是調用Thread.sleep后的狀態,這時線程依然持有運行所需的各種資源,但是不會被CPU調度。
- 掛起:一般是調用Thread.suspend后的狀態,和休眠類似,CPU不會調度該線程,不同的是,這種狀態下,線程會釋放所有資源。
- 死亡:線程運行結束或者調用了Thread.stop方法。
創建並運行線程:
- Thread()或者Thread(Runnable):構造線程。
- Thread.start:啟動線程。
- Thread.sleep:將線程切換至休眠狀態。
- Thread.interrupt:中斷線程的執行。
- Thread.join:等待某線程結束。
- Thread.yield:剝奪線程在CPU上的執行時間片,等待下一次調度。
- Object.wait:將Object上所有線程鎖定,直到notify方法才繼續運行。
- Object.notify:隨機喚醒Object上的1個線程。
- Object.notifyAll:喚醒Object上的所有線程。
線程安全與鎖
更多1.https://www.cnblogs.com/zhaojinhui/p/5526988.html
2.https://www.cnblogs.com/lyjblogs/p/7888646.html
3.https://www.cnblogs.com/jalja/p/5887192.html
由於考慮到多個線程同時搶占資源時,會發生沖突,這叫做線程安全問題,即我們需要避免多個線程同時訪問或修改同一資源,其實現機制為同步與互斥。避免沖突的方法就是對臨界資源或者臨界區加鎖。
鎖方法:
1.同步方法
1 在方法聲明上加上synchronized 2
3 public synchronized void method() { 4 可能會產生線程安全問題的代碼 5 }
同步方法中鎖的對象是 this(即調用者對象),並不一定是方法中用到的對象數據!
2.靜態同步方法
1 在方法聲明上加上static synchronized 2 3 public static synchronized void method() { 4 可能會產生線程安全問題的代碼 5 }
靜態同步方法中的鎖對象是 類名.class(因為在加載類文件的時候,靜態同步方法由於是靜態的也被加載進內存了,類名.class的加載優先級高於靜態方法)
3.同步代碼塊
1 在需要同步的代碼外面包上一個synchronized 2 3 synchronized(Object o) { 4 可能會產生線程安全問題的代碼 5 }
同步代碼塊中鎖的對象可以是任意對象(即Object o)
鎖對象:
this指當前調用者對象,一般情況下,普通方法塊中是省略的(在構造方法中沒有省略)。
死鎖:
當前線程正在訪問共享對象時,由於一個確定的條件在某情況下不會發生,則該線程不釋放鎖,那么其余線程無法獲得鎖,也無法運行線程,多線程就會一直陷入等待中,等待這個“永遠不會發生”的條件,這就會造成死鎖。
原語操作和其他鎖:
常用的原語有信號量P/V操作、管程、共享內存等,是進程之間常用的通信的方式。具體參考操作系統或原語、信號量。
synchronized是不錯,但它並不完美。它有一些功能性的限制:
- 它無法中斷一個正在等候獲得鎖的線程;
- 也沒有有權利得到鎖,只能一直陷入等待;即使不想等待,也就沒法得到鎖;
- 同步還要求鎖的釋放只能在與獲得鎖所在的堆棧幀相同的堆棧幀中進行,多數情況下,這沒問題(而且與異常處理交互得很好),但是,確實存在一些非塊結構的鎖定更合適的情況。
獲取鎖的線程釋放鎖只會有兩種情況:
1、獲取鎖的線程執行完了該代碼塊,然后線程釋放對鎖的占有。
2、線程執行發生異常,此時JVM會讓線程自動釋放鎖。
其他鎖:
Lock與synchronized對比:
1、Lock不是Java語言內置的,synchronized是Java語言的關鍵字,因此是內置特性。Lock是一個類,通過這個類可以實現同步訪問。
2、synchronized不需要手動釋放鎖,當synchronized方法或者synchronized代碼塊執行完之后,系統會自動讓線程釋放對鎖的占用;而Lock則必須要用戶去手動釋放鎖,如果沒有主動釋放鎖,就有可能導致出現死鎖現象。
所以lock是可以手動釋放鎖的。
java.util.concurrent.locks包中常用的類和接口:
1 public interface Lock { 2 //用來獲取鎖。如果鎖已被其他線程獲取,則進行等待。 3 void lock(); 4 // 當通過這個方法去獲取鎖時,如果線程正在等待獲取鎖,則這個線程能夠響應中斷,即中斷線程的等待狀態 5 void lockInterruptibly() throws InterruptedException; 6 //它表示用來嘗試獲取鎖,如果獲取成功,則返回true,如果獲取失敗(即鎖已被其他線程獲取),則返回false 7 boolean tryLock(); 8 /*與tryLock()方法是類似的,只不過區別在於這個方法在拿不到鎖時會等待一定的時間,在時間期限之內如果還拿不到鎖,就返回false。
如果如果一開始拿到鎖或者在等待期間內拿到了鎖,則返回true。*/ 9 boolean tryLock(long time, TimeUnit unit) throws InterruptedException; 10 //釋放鎖 11 void unlock(); 12 Condition newCondition(); 13 }
1、Lock與unlock
Lock用於獲取鎖,但lock不會主動釋放鎖(調用unlock釋放),所以需要與unlock()配合使用。一般在使用Lock時必須在try{}catch{}塊中進行,並且將釋放鎖的操作放在finally塊中進行,以保證鎖一定被被釋放,防止死鎖的發生。
PS:同一個線程可以連續獲得同一把鎖,但也必須釋放相同次數的鎖。
2、獲取鎖等待時間tryLock(long time, TimeUnit unit)
如果你和朋友有約,在約定時間內對方未出現,我想你肯定會掃興的離去。對於線程來說也應該時這樣的,因為通常我們是無法判斷一個線程為什么會無法獲得鎖,但我們可以給該線程一個獲取鎖的時間限制,如果到時間還沒有獲取到鎖,則放棄獲取鎖。
ReentrantLock增加了鎖:
1. void lock(); // 無條件的鎖;
2. void lockInterruptibly throws InterruptedException;//可中斷的鎖;
解釋: 使用ReentrantLock如果獲取了鎖立即返回,如果沒有獲取鎖,當前線程處於休眠狀態,直到獲得鎖或者當前線程可以被別的線程中斷去做其他的事情;但是如果是synchronized的話,如果沒有獲取到鎖,則會一直等待下去;
3. boolean tryLock();//如果獲取了鎖立即返回true,如果別的線程正持有,立即返回false,不會等待;
4. boolean tryLock(long timeout,TimeUnit unit);//如果獲取了鎖立即返回true,如果別的線程正持有鎖,會等待參數給的時間,在等待的過程中,如果獲取鎖,則返回true,如果等待超時,返回false。
Condition的特性:
1.Condition中的await()方法相當於Object的wait()方法,Condition中的signal()方法相當於Object的notify()方法,Condition中的signalAll()相當於Object的notifyAll()方法。不同的是,Object中的這些方法是和同步鎖捆綁使用的;而Condition是需要與互斥鎖/共享鎖捆綁使用的。
2.Condition它更強大的地方在於:能夠更加精細的控制多線程的休眠與喚醒。對於同一個鎖,我們可以創建多個Condition,在不同的情況下使用不同的Condition。
例如,假如多線程讀/寫同一個緩沖區:當向緩沖區中寫入數據之后,喚醒"讀線程";當從緩沖區讀出數據之后,喚醒"寫線程";並且當緩沖區滿的時候,"寫線程"需要等待;當緩沖區為空時,"讀線程"需要等待。
如果采用Object類中的wait(), notify(), notifyAll()實現該緩沖區,當向緩沖區寫入數據之后需要喚醒"讀線程"時,不可能通過notify()或notifyAll()明確的指定喚醒"讀線程",而只能通過notifyAll喚醒所有線程(但是notifyAll無法區分喚醒的線程是讀線程,還是寫線程)。 但是,通過Condition,就能明確的指定喚醒讀線程。
線程調度
更多1.https://www.cnblogs.com/xingele0917/p/3979494.html
2.https://www.cnblogs.com/yjd_hycf_space/p/7526608.html
線程創建后,就要將它們運行起來,然而怎樣調度怎樣分配CPU給線程是一個問題。
調度策略
一般線程調度模式分為兩種——搶占式調度和協同式調度。搶占式調度指的是每條線程執行的時間、線程的切換都由系統控制,系統控制指的是在系統某種運行機制下,可能每條線程都分同樣的執行時間片,也可能是某些線程執行的時間片較長,甚至某些線程得不到執行的時間片。在這種機制下,一個線程的堵塞不會導致整個進程堵塞。協同式調度指某一線程執行完后主動通知系統切換到另一線程上執行,這種模式就像接力賽一樣,一個人跑完自己的路程就把接力棒交接給下一個人,下個人繼續往下跑。線程的執行時間由線程本身控制,線程切換可以預知,不存在多線程同步問題,但它有一個致命弱點:如果一個線程編寫有問題,運行到一半就一直堵塞,那么可能導致整個系統崩潰。
---------------------
作者:超人汪小建(seaboat)
來源:CSDN
原文:https://blog.csdn.net/wangyangzhizhou/article/details/41122385
但實際上我們有時需要手動進行調整,就是按照自我的意願去改變線程調度方法,讓某一個線程具有優先權,每當該線程的條件滿足就停止其它線程,而運行該線程。
調度優化及線程優先級
優先級在各種常見算法中並不少見,我們調度多線程可以利用優先級來確定誰先誰后(操作系統中進程調度也有優先級調度方法)。理所當然,優先級高的線程會搶占CPU運行。
1 Java線程的優先級用整數表示,取值范圍是1~10,Thread類有以下三個靜態常量: 2 static int MAX_PRIORITY 3 線程可以具有的最高優先級,取值為10。 4 static int MIN_PRIORITY 5 線程可以具有的最低優先級,取值為1。 6 static int NORM_PRIORITY 7 分配給線程的默認優先級,取值為5。
線程切換
線程睡眠:Thread.sleep(long millis)方法,使線程轉到阻塞狀態。millis參數設定睡眠的時間,以毫秒為單位。當睡眠結束后,就轉為就緒(Runnable)狀態。sleep()平台移植性好。
線程等待與喚醒
更多1.https://www.cnblogs.com/xingele0917/p/3979494.html
2.https://www.cnblogs.com/yjd_hycf_space/p/7526608.html
wait與sleep
共同點:
1. 他們都是在多線程的環境下,都可以在程序的調用處阻塞指定的毫秒數,並返回。
2. wait()和sleep()都可以通過interrupt()方法 打斷線程的暫停狀態 ,從而使線程立刻拋出InterruptedException。
如果線程A希望立即結束線程B,則可以對線程B對應的Thread實例調用interrupt方法。如果此刻線程B正在wait/sleep /join,則線程B會立刻拋出InterruptedException,在catch() {} 中直接return即可安全地結束線程。
需要注意的是,InterruptedException是線程自己從內部拋出的,並不是interrupt()方法拋出的。對某一線程調用 interrupt()時,如果該線程正在執行普通的代碼,那么該線程根本就不會拋出InterruptedException。但是,一旦該線程進入到 wait()/sleep()/join()后,就會立刻拋出InterruptedException 。
不同點:
1.每個對象都有一個鎖來控制同步訪問。Synchronized關鍵字可以和對象的鎖交互,來實現線程的同步。sleep方法沒有釋放鎖,而wait方法釋放了鎖,使得其他線程可以使用同步控制塊或者方法。
2. wait,notify和notifyAll只能在同步控制方法或者同步控制塊里面使用,而sleep可以在任何地方使用
3. sleep必須捕獲異常,而wait,notify和notifyAll不需要捕獲異常
所以sleep()和wait()方法的最大區別是:
sleep()睡眠時,保持對象鎖,仍然占有該鎖;
而wait()睡眠時,釋放對象鎖。
sleep()使當前線程進入停滯狀態(阻塞當前線程),讓出CUP的使用、目的是不讓當前線程獨自霸占該進程所獲的CPU資源,以留一定時間給其他線程執行的機會;sleep()是Thread類的Static(靜態)的方法;因此他不能改變對象的機鎖,所以當在一個Synchronized塊中調用Sleep()方法是,線程雖然休眠了,但是對象的機鎖並木有被釋放,其他線程無法訪問這個對象(即使睡着也持有對象鎖)。
在sleep()休眠時間期滿后,該線程不一定會立即執行,這是因為其它線程可能正在運行而且沒有被調度為放棄執行,除非此線程具有更高的優先級。
wait()方法是Object類里的方法;當一個線程執行到wait()方法時,它就進入到一個和該對象相關的等待池中,同時失去(釋放)了對象的機鎖(暫時失去機鎖,wait(long timeout)超時時間到后還需要返還對象鎖);其他線程可以訪問;
wait()使用notify或者notifyAlll或者指定睡眠時間來喚醒當前等待池中的線程。
wiat()必須放在synchronized block中,否則會在program runtime時扔出”java.lang.IllegalMonitorStateException“異常。
notify與notifyAll
notify()只能喚醒一個在等待該對象(鎖住的對象)線程,而notifyAll()喚醒所有在等待該對象的線程。
Obj.wait(),與Obj.notify()/Obj.notifyAll()必須要與synchronized(Obj)一起使用,也就是wait,與notify是針對已經獲取了Obj鎖進行操作,從語法角度來說就是Obj.wait(),Obj.notify必須在synchronized(Obj){...}語句塊內。從功能上來說wait就是說線程在獲取對象鎖后,主動釋放對象鎖,同時本線程休眠。直到有其它線程調用對象的notify()喚醒該線程,才能繼續獲取對象鎖,並繼續執行。相應的notify()就是對對象鎖的喚醒操作。但有一點需要注意的是notify()調用后,並不是馬上就釋放對象鎖的,而是在相應的synchronized(){}語句塊執行結束,自動釋放鎖后,JVM會在wait()對象鎖的線程中隨機選取一線程,賦予其對象鎖,喚醒線程,繼續執行。這樣就提供了在線程間同步、喚醒的操作。