標籤:
下面是我學習多線程記錄的知識點,並沒詳細講解每個知識點,只是將重要的知識點記錄下來,有時間可以看看,如果有不對的地方,歡迎大家支出,謝謝!
1、多線程的狀態和建立方式:
線程的狀態:
1、新狀態:線程對象已經建立,還沒有在其上調用start()方法。
2、可運行狀態:當線程有資格運行,但發送器還沒有把它選定為運行線程時線程所處的狀態。當start()方法調用時,線程首先進入可運行狀態。線上程運行之後或者從 阻塞、等待或睡眠狀態回來後,也返回到可運行狀態。
3、運行狀態:線程發送器從可運行池中選擇一個線程作為當前線程時線程所處的狀態。這也是線程進入運行狀態的唯一一種方式。
4、等待/阻塞/睡眠狀態:這是線程有資格運行時它所處的狀態。實際上這個三狀態組合為一種,其共同點是:線程仍舊是活的,但是當前沒有條件運行。換句話說,它是可 啟動並執行,但是如果某件事件出現,他可能返回到可運行狀態。
5、死亡態:當線程的run()方法完成時就認為它死去。這個線程對象也許是活的,但是,它已經不是一個單獨執行的線程。線程一旦死亡,就不能複生。如果在一個死去的 線程上調用start()方法,會拋出java.lang.IllegalThreadStateException異常。
線程的2種建立方式:
繼承Thread類和實現Runnable介面類,使用實現介面的方式時,建立線程執行個體需要將介面實作類別的執行個體傳給Thread建構函式。因為java不支援多繼承,所以建議採用實現 runnable介面的方式建立線程。
2、線程同步,關鍵字:synchronized和volatile,及其區別。死結發生的原因
根據jvm運行時刻記憶體的分配。其中有一個記憶體地區是jvm虛擬機器棧,每一個線程運行時都有一個線程棧,線程棧儲存了線程運行時候變數值資訊。當線程訪問某一個對象時候值的時候,首先通過對象的引用找到對應在堆記憶體的變數的值,然後把堆記憶體變數的具體值load到執行緒區域記憶體中,建立一個變數副本,之後線程就不再和對象在堆記憶體變數值有任何關係,而是直接修改副本變數的值,在修改完之後的某一個時刻(線程退出之前),自動把線程變數副本的值回寫到對象在堆中變數。這樣在堆中的對象的值就產生變化了。所以在多個線程共用的變數就有可能出問題。
當一個共用變數被volatile修飾時,它會保證修改的值會立即被更新到主存,當有其他線程需要讀取時,它會去記憶體中讀取新值。所以有時候可以防止線程讀取共用變數時,值是不正確的情況出現,但是volatile不能保證變數的原子性,如果共用變數是單一資料型別,變數的修改不基於其自身的數值時(如x=10可以,x++不行),可以保證其原子性.因為volatile並不能保證原子性,所以慎用volatile。這裡只是做個簡單的筆記,關於bolatitle更詳細的介紹可以看看下面的文章:
http://www.cnblogs.com/dolphin0520/p/3920373.html#commentform
http://www.infoq.com/cn/articles/java-memory-model-4/
http://blog.csdn.net/hupitao/article/details/45227891
http://www.cnblogs.com/aigongsi/archive/2012/04/01/2429166.html
synchronized知識點:
當synchronized修改某個方法,則該方法叫同步方法,如果synchronized作用於代碼塊,則該程式碼程式庫叫同步代碼塊。
java的對象鎖和類鎖:java的對象鎖和類鎖在鎖的概念上基本上和內建鎖是一致的,但是,兩個鎖實際是有很大的區別的,對象鎖是用於對象執行個體方法,或者一個對象執行個體上的,類鎖是用於類的靜態方法或者一個類的class對象上的。我們知道,類的對象執行個體可以有很多個,但是每個類只有一個class對象,所以不同對象執行個體的對象鎖是互不干擾的,但是每個類只有一個類鎖。但是有一點必須注意的是,其實類鎖只是一個概念上的東西,並不是真實存在的,它只是用來協助我們理解鎖定執行個體方法和靜態方法的區別的。
synchronized塊寫法:
synchronized(object){...}
表示線程在執行的時候會將object對象上鎖(注意這個對象可以是任意類的對象,也可以使用this關鍵字)。這樣就可以自行規定上鎖對象。
如果同步代碼塊中synchronized的括弧內是類對象(如Object.class),則表示給類的這個方法上鎖
非靜態同步方法會將對象上鎖,但是靜態方法的鎖不屬於對象,當一個synchronized關鍵字修飾的方法同時又被static修飾時,它的鎖屬於類,它會將這個方法所在的類的Class對象上鎖。
類鎖和對象鎖是兩個不一樣的鎖,控制著不同的地區,它們是互不干擾的。同樣,線程獲得對象鎖的同時,也可以獲得該類鎖,即同時獲得兩個鎖,這是允許的。
如果一個對象有多個synchronized方法(包括同步代碼塊),某一時刻某個線程已經進入到了某個synchronized方法(包括同步代碼塊),那麼在該方法沒有執行完畢前,其他線程是無法訪問該對象的任何synchronized方法的(包括同步代碼塊)。但是可以訪問非同步方法和非同步代碼塊。
關於sychronized的用法可以看看下面的文章:
http://zhh9106.iteye.com/blog/2151791
http://www.cnblogs.com/GnagWang/archive/2011/02/27/1966606.html
http://www.cnblogs.com/mengdd/archive/2013/02/16/2913806.html
synchronized關鍵字是防止多個線程同時執行一段代碼,那麼就會很影響程式執行效率,而volatile關鍵字在某些情況下效能要優於synchronized,但是要注意volatile關鍵字是無法替代synchronized關鍵字的,因為volatile關鍵字無法保證操作的原子性。
3、線程間通訊,常用方法:wait、notify、notifyall、join、yeild
wait()、notify()和notifyAll()是Object類中的方法,並且不能被重寫,可以看jdk源碼;
wait和notify、notifyAll方法必須與synchronized一起使用;
wait會釋放鎖,注意如果在同步方法或代碼塊中使用sleep,雖然sleep也是讓線程休眠,但是sleep不會釋放鎖;yield也不會釋放鎖
如果只有一個線程在等待,則使用notify更好,因為它比notify效率更高。如果不能確定是用notify還是notifyall方法,則選擇使用notifyall,這可能有些浪費資源,但是更安全.notify方法只會喚醒等待中的一個線程(這個線程由jvm決定,是無法具體確定的,是隨機的,與線程優先順序無關)。
注意在調用notify和nofityAll的時候,並不能確切的喚醒某一個等待狀態的線程,而是由JVM確定喚醒哪個線程,而且不是按線程的優先順序。
注意notify的遺漏通知和早期通知導致的問題,典型的同步代碼中出現如下代碼:if(condition){wait()..},這裡容易出現遺漏通知的情況,此時可以將if改成while來避免此類問題。
線程可以使用類ThreadLocal和InheritableThreadLocal讓特定線程變數在不同的線程中為不同的值
join() 方法主要是讓調用該方法的thread完成run方法裡面的東西後, 在執行join()方法後面的代碼,舉個例子:你和朋友去吃飯,中途你有事,你朋友哪怕吃飽了,也會在那裡等你,因為他沒帶錢等你回來結賬後才能一起走。
join方法只有在繼承了Thread類的線程中才能調用
線程必須要start() 後再join才能起作用
Thread.yield()方法作用是:暫停當前正在執行的線程對象,並執行其他線程。
yield()應該做的是讓當前運行線程回到可運行狀態,以允許具有相同優先順序的其他線程獲得運行機會。因此,使用yield()的目的是讓相同優先順序的線程之間能適當的輪轉執行。但是,實際中無法保證yield()達到讓步目的,因為讓步的線程還有可能被線程發送器再次選中。
結論:yield()從未導致線程轉到等待/睡眠/阻塞狀態。在大多數情況下,yield()將導致線程從運行狀態轉到可運行狀態,但有可能沒有效果。
可以看看以下文章:
http://www.cnblogs.com/riskyer/p/3263032.html
4、守護線程及線程優先順序
1、什麼是守護線程(Daemon線程)?
守護線程是一個後台啟動並執行線程,與之對比的是使用者線程(User線程)。當正在啟動並執行線程都是守護線程時,JAVA 虛擬機器退出。不管守護線程執行是否完成,都直接退出,這是特別需要注意的地方。當使用者線程存在使用者線程一直在運行,則守護線程也會一直運行。
2、如何建立守護線程?
建立一個新線程,用setDaemon(boolean on)方法可以方便的設定線程的Daemon模式,true為Daemon模式,false為User模式。setDaemon(boolean on)方法必須線上程啟動之前調用,當線程正在運行時調用會產生異常。isDaemon方法將測試該線程是否為守護線程。值得一提的是,當你在一個守護線程中產生了其他線程,那麼這些新產生的線程不用設定Daemon屬性,都將是守護線程,使用者線程同樣。
3、守護線程的用處?
守護線程使用的情況較少,但並非無用,舉例來說,JVM的記憶體回收、記憶體管理等線程都是守護線程。還有就是在做資料庫應用時候,使用的資料庫連接池,串連池本身也包含著很多後台線程,監控串連個數、逾時時間、狀態等等。守護線程一般是為使用者線程服務的。
可以看看以下文章:
http://blog.csdn.net/ljyy2006/article/details/1901109
http://lavasoft.blog.51cto.com/62575/221845/
http://www.cnblogs.com/super-d2/p/3348183.html
線程優先順序:
線程的優先順序仍然無法保障線程的執行次序。只不過,優先順序高的線程擷取CPU資源的機率較大,優先順序低的並非沒機會執行。
當設計多線程應用程式的時候,一定不要依賴於線程的優先順序。因為線程調度優先順序操作是沒有保障的,只能把線程優先順序作用作為一種提高程式效率的方法,但是要保證程式不依賴這種操作。
線程的優先順序用1-10之間的整數表示,數值越大優先順序越高,預設的優先順序為5。
在一個線程中開啟另外一個新線程,則新開線程稱為該線程的子線程,子線程初始優先順序與父線程相同。
如果該線程已經屬於一個線程組(ThreadGroup),該線程的優先順序不能超過該線程組的優先順序
5、線程組、線程池
Java提供了ThreadGroup類來控制一個線程組,一個線程組可以通過線程對象來建立,也可以由其他線程組來建立,產生一個樹形結構的線程,對線程分組是Java並發API提供的一個有趣功能。我們可以將一組線程看成一個獨立單元,並且可以隨意操縱線程組中的線程對象。比如,可以控制一組線程來運行同樣的任務,無需關心有多少線程還在運行,還可以使用一次中斷調用中斷所有線程的執行。
Java提供了ThreadGroup類來控制一個線程組。一個線程組可以通過線程對象來建立,也可以由其他線程組來建立,產生一個樹形結構的線程。根據《Effective Java》的說明,不再建議使用ThreadGroup。建議使用Executor。
java預設建立的線程都是屬於系統線程組,而同一個線程組的線程是可以相互修改對方的資料的。但如果在不同的線程組中,那麼就不能“跨線程組”修改資料,可以從一定程度上保證資料安全。
1、為什麼需要線程池
網路請求通常有兩種形式:第一種,請求不是很頻繁,而且每次串連後會保持相當一段時間來讀資料或者寫資料,最後斷開,如檔案下載,網路流媒體等。另一種形式是請求頻繁,但是串連上以後讀/寫很少量的數 據就中斷連線。考慮到服務的並發問題,如果每個請求來到以後服務都為它啟動一個線程,那麼這對服務的資源可能會造成很大的浪費,特別是第二種情況。因為通常情況下,建立線程是需要一定的耗時的,設這個時間為T1,而串連後讀/寫服務的時間為T2,當T1>>T2時,我們就應當考慮一種策略或者機制來控制,使得服務對於第二種請求方式也能在較低的功耗下完成。
通常,我們可以用線程池來解決這個問題,首先,在服務啟動的時候,我們可以啟動好幾個線程,並用一個容器(如線程池)來管理這些線程。當請求到來時,可以從池中去一個線程出來,執行任務(通常是對請求的響應),當任務結束後,再將這個線程放入池中備用;如果請求到來而池中沒有閒置線程,該請求需要排隊等候。最後,當服務關閉時銷毀該池即可。
jdk提供線程池,在java.util.concurrent包內,可以看下jdk關於線程池的實現,以下關於線程池的文章可以看看:
http://www.oschina.net/question/565065_86540
http://www.cnblogs.com/dolphin0520/p/3932921.html
6、使用管道線上程間流動資料
Java提供了各種各樣的輸入輸出資料流(stream),使程式員能夠很方便地對資料進行操作。其中,管道(pipe)流是一種特殊的流,用於在不同線程間直接傳送資料。一個線程發送資料到輸出管道,另一個線程從輸入管道中讀出資料。通過使用管道,達到實現多個線程間通訊的目的。那麼,如何建立和使用管道呢?
Java提供了兩個特殊的專門用來處理管道的類,它們就是PipedInputStream類和PipedOutputStream類。
其中,PipedInputStream代表了資料在管道中的輸出端,也就是線程從管道讀出資料的一端;PipedOutputStream代表了資料在管道中的輸入端,也就是線程向管道寫入資料的一端,這兩個類一起使用就可以建立出資料輸入輸出的管道流對象。
一旦建立了管道之後,就可以利用多線程的通訊機制對磁碟中的檔案通過管道進行資料的讀寫,從而使多線程的程式設計在實際應用中發揮更大的作用。
可以看看以下文章:
http://www.zhujiangroad.com/program/JAVA/1942.html
7、stop、suspend、resume方法為什麼被淘汰,以及安全實現相似行為的方式
suspend()方法就是將一個線程掛起(暫停),resume()方法就是將一個掛起線程複活繼續執行。
suspend() 和 resume() 方法:兩個方法配套使用,suspend()使得線程進入阻塞狀態,並且不會自動回復,必須其對應的 resume() 被調用,才能使得線程重新進入可執行狀態。典型地,suspend() 和 resume() 被用在等待另一個線程產生的結果的情 形:測試發現結果還沒有產生後,讓線程阻塞,另一個線程產生了結果後,調用 resume() 使其恢複。如果調用suspend的方法線程試圖取得相同的鎖,程式就會死結。
最謹慎的方法是讓“目標”線程輪詢一個指示所需線程狀態(活動或掛起)的變數。當需要掛起狀態時,線程將使用 Object.wait 進行等待。當線程恢複時,將使用 Object.notify 通知目標線程。
stop這個方法將終止所有未結束的方法,包括run方法。當一個線程停止時候,他會立即釋放所有他鎖住對象上的鎖。這會導致對象處於不一致的狀態。假如一個方法在將錢從一個賬戶轉移到另一個賬戶的過程中,在取款之後存款之前就停止了。那麼現在銀行對象就被破壞了。因為鎖已經被釋放了。當線程想終止另一個線程的時候,它無法知道何時調用stop是安全的,何時會導致對象被破壞。所以這個方法被棄用了。你應該中斷一個線程而不是停止他。我們平常使用windows系統,某個程式會出現無法響應,此時在工作管理員中直接終止掉進程,像類似的情況,是可以直接使用stop的。
大多數使用 stop 的情況都應該用簡單修改一些變數以指示目標線程應停止啟動並執行代碼所代替。目標線程應該定期檢查該變數,並在該變數指示需要它停止運行時以一種合理的方法從其 run 方法中返回(這是 JavaSoft 教程始終推薦的方法)。要確保停止請求的即時通訊,該變數必須是 volatile(迅變)的(或對該變數的訪問必須是同步的)。
可以看看以下文章:
http://blog.csdn.net/linchengzhi/article/details/7468395
http://blog.163.com/feng_welcome/blog/static/17177032420112246191360/
8、interrupt方法:"完美終止線程"
Thread.interrupt()方法不會中斷一個正在啟動並執行線程。這一方法實際上完成的是,線上程受到阻塞時拋出一個中斷訊號,這樣線程就得以退出阻塞的狀態。更確切的說,如果線程被Object.wait, Thread.join和Thread.sleep三種方法之一阻塞,那麼,它將接收到一個中斷異常(InterruptedException),從而提早地終結被阻塞狀態。
因此,如果線程被上述幾種方法阻塞,正確的停止線程方式是設定共用變數,並調用interrupt()(注意變數應該先設定)。如果線程沒有被阻塞,這時調用interrupt()將不起作用;否則,線程就將得到異常(該線程必須事先預備好處理此狀況),接著逃離阻塞狀態。在任何一種情況中,最後線程都將檢查共用變數然後再停止。
9、死結
1、死結產生原因
Java線程死結是一個經典的多線程問題,因為不同的線程都在等待那些根本不可能被釋放的鎖,從而導致所有的工作都無法完成。假設有兩個線程,分別代表兩個饑餓的人,他們必須共用刀叉並輪流吃飯。他們都需要獲得兩個鎖:共用刀和共用叉的鎖。假如線程 “A”獲得了刀,而線程“B”獲得了叉。線程“A”就會進入阻塞狀態來等待獲得叉,而線程“B”則阻塞來等待“A”所擁有的刀。
導致死結的根源在於不適當地運用“synchronized”關鍵詞來管理線程對特定對象的訪問
避免死結的一個通用的經驗法則是:當幾個線程都要訪問共用資源A、B、C時,保證使每個線程都按照同樣的順序去訪問它們,比如都先訪問A,在訪問B和C。
可以看看以下文章:
http://leowzy.iteye.com/blog/740859
2、查看線程死結
http://www.cnblogs.com/ilahsa/archive/2013/06/03/3115410.html
10、自運行活動類技術,推薦使用在匿名內部類中隱藏run方法的設計方案
自運行活動類技術:在Thread子類或實現runnable介面類中的建構函式中啟動線程;
隱藏run方法的設計方案:在普通類的構造方法中使用匿名內部類建立線程內,再啟動線程
java多線程知識點