Java程序员从笨鸟到菜鸟之(十一)多线程讲解
多線程是Java應用程序的一個特點,掌握java的多線程也是作為一java程序員必備的知識。多線程指的是在單個程序中可以同時運行多個同的線程執(zhí)行不同的任務.線程是程序內的順序控制流,只能使用分配給序的資源和環(huán)境。還記得剛開始學習的時候總是和進程分不清,總是對這兩個名詞所迷惑。
下面就首先對這兩個名詞區(qū)分來作為本篇博客的開始:
????一、線程與進程的區(qū)別
多個進程的內部數(shù)據(jù)和狀態(tài)都是完全獨立的,而多線程是共享一塊內存空間和一組系統(tǒng)資源,有可能互相影響.??線程本身的數(shù)據(jù)通常只有寄存器數(shù)據(jù),以及一個程序執(zhí)行時使用的堆棧,所以線程的切換比進程切換的負擔要小。
?多線程編程的目的,就是"最大限度地利用CPU資源",當某一線程的處理不需要占用CPU而只和I/O等資源打交道時,讓需要占用CPU資源的其它線程有機會獲得CPU資源。從根本上說,這就是多線程編程的最終目的。
二、了解一下java在多線程中的基礎知識
1.Java中如果我們自己沒有產(chǎn)生線程,那么系統(tǒng)就會給我們產(chǎn)生一個線程(主線程,main方法就在主線程上運行),我們的程序都是由線程來執(zhí)行的。
?2.?進程:執(zhí)行中的程序(程序是靜態(tài)的概念,進程是動態(tài)的概念)。?
3.?線程的實現(xiàn)有兩種方式,第一種方式是繼承Thread類,然后重寫run方法;第二種是實現(xiàn)Runnable接口,然后實現(xiàn)其run方法。?
4.?將我們希望線程執(zhí)行的代碼放到run方法中,然后通過start方法來啟動線程,start方法首先為線程的執(zhí)行準備好系統(tǒng)資源,然后再去調用run方法。當某個類繼承了Thread類之后,該類就叫做一個線程類。?
5.?一個進程至少要包含一個線程。?
6.?對于單核CPU來說,某一時刻只能有一個線程在執(zhí)行(微觀串行),從宏觀角度來看,多個線程在同時執(zhí)行(宏觀并行)。?
7.?對于雙核或雙核以上的CPU來說,可以真正做到微觀并行。
三、Thread源碼研究:?
1)?Thread類也實現(xiàn)了Runnable接口,因此實現(xiàn)了Runnable接口中的run方法;?
2)?當生成一個線程對象時,如果沒有為其設定名字,那么線程對象的名字將使用如下形式:Thread-number,該number將是自動增加的,并被所有的Thread對象所共享(因為它是static的成員變量)。?
3)?當使用第一種方式來生成線程對象時,我們需要重寫run方法,因為Thread類的run方法此時什么事情也不做。
4)當使用第二種方式生成線程對象時,我們需要實現(xiàn)Runnable接口的run方法,然后使用new?Thread(new?MyThread())(假如MyThread已經(jīng)實現(xiàn)了Runnable接口)來生成線程對象,這時的線程對象的run方法或調就會MyThread類的run方法,這樣我們自己編寫的run方法就執(zhí)行了。
說明:?
Public?void?run(){
If(target!=null){
Target.run();
}}
?
當使用繼承Thread生成線程對象時,target為空,什么也不執(zhí)行,當使用第二種方式生成時,執(zhí)行target.run(),target為runnable的實例對象,即為執(zhí)行重寫后的方法
總結:兩種生成線程對象的區(qū)別:
1.兩種方法均需執(zhí)行線程的start方法為線程分配必須的系統(tǒng)資源、調度線程運行并執(zhí)行線程的run方法。?
2.在具體應用中,采用哪種方法來構造線程體要視情況而定。通常,當一個線程已繼承了另一個類時,就應該用第二種方法來構造,即實現(xiàn)Runnable接口。?
四:線程的生命周期:
由上圖可以看出,一個線程由出生到死亡分為五個階段:
1).創(chuàng)建狀態(tài)?
?當用new操作符創(chuàng)建一個新的線程對象時,該線程處于創(chuàng)建狀態(tài)。?
?處于創(chuàng)建狀態(tài)的線程只是一個空的線程對象,系統(tǒng)不為它分配資源?
2).?可運行狀態(tài)?
?執(zhí)行線程的start()方法將為線程分配必須的系統(tǒng)資源,安排其運行,并調用線程體—run()方法,這樣就使得該線程處于可運行(?Runnable?)狀態(tài)。?
?這一狀態(tài)并不是運行中狀態(tài)(Running?),因為線程也許實際上并未真正運行。?
3).不可運行狀態(tài)?
.當發(fā)生下列事件時,處于運行狀態(tài)的線程會轉入到不可運行狀態(tài)。?
調用了sleep()方法;?
?線程調用wait方法等待特定條件的滿足?
?線程輸入/輸出阻塞?
4)返回可運行狀態(tài):?
?處于睡眠狀態(tài)的線程在指定的時間過去后?
?如果線程在等待某一條件,另一個對象必須通過notify()或notifyAll()方法通知等待線程條件的改變?
?如果線程是因為輸入/輸出阻塞,等待輸入/輸出完成?
5).?消亡狀態(tài)?
當線程的run方法執(zhí)行結束后,該線程自然消亡。?
注意:
1.停止線程的方式:不能使用Thread類的stop方法來終止線程的執(zhí)行。一般要設定一個變量,在run方法中是一個循環(huán),循環(huán)每次檢查該變量,如果滿足條件則繼續(xù)執(zhí)行,否則跳出循環(huán),線程結束。?
2.不能依靠線程的優(yōu)先級來決定線程的執(zhí)行順序。?
五:多線程并發(fā)
多線程并發(fā)是線程同步中比較常見的現(xiàn)象,java多線程為了避免多線程并發(fā)解決多線程共享數(shù)據(jù)同步問題提供了synchronized關鍵字
synchronized關鍵字:當synchronized關鍵字修飾一個方法的時候,該方法叫做同步方法。?
1.Java中的每個對象都有一個鎖(lock)或者叫做監(jiān)視器(monitor),當訪問某個對象的synchronized方法時,表示將該對象上鎖,此時其他任何線程都無法再去訪問該synchronized方法了,直到之前的那個線程執(zhí)行方法完畢后(或者是拋出了異常),那么將該對象的鎖釋放掉,其他線程才有可能再去訪問該synchronized方法。?
2.?如果一個對象有多個synchronized方法,某一時刻某個線程已經(jīng)進入到了某個synchronized方法,那么在該方法沒有執(zhí)行完畢前,其他線程是無法訪問該對象的任何synchronized方法的。?
?
3.如果某個synchronized方法是static的,那么當線程訪問該方法時,它鎖的并不是synchronized方法所在的對象,而是synchronized方法所在的對象所對應的Class對象,因為Java中無論一個類有多少個對象,這些對象會對應唯一一個Class對象,因此當線程分別訪問同一個類的兩個對象的兩個static,synchronized方法時,他們的執(zhí)行順序也是順序的,也就是說一個線程先去執(zhí)行方法,執(zhí)行完畢后另一個線程才開始執(zhí)行。?
4.?synchronized塊,寫法:?
synchronized(object)?
{?
}?
表示線程在執(zhí)行的時候會對object對象上鎖。?
5.synchronized方法是一種粗粒度的并發(fā)控制,某一時刻,只能有一個線程執(zhí)行該synchronized方法;synchronized塊則是一種細粒度的并發(fā)控制,只會將塊中的代碼同步,位于方法內、synchronized塊之外的代碼是可以被多個線程同時訪問到的。?
同步的線程狀態(tài)圖:
六:wait與notify
1.wait與notify方法都是定義在Object類中,而且是final的,因此會被所有的Java類所繼承并且無法重寫。這兩個方法要求在調用時線程應該已經(jīng)獲得了對象的鎖,因此對這兩個方法的調用需要放在synchronized方法或塊當中。當線程執(zhí)行了wait方法時,它會釋放掉對象的鎖。?
2.?另一個會導致線程暫停的方法就是Thread類的sleep方法,它會導致線程睡眠指定的毫秒數(shù),但線程在睡眠的過程中是不會釋放掉對象的鎖的。?
3.notify():喚醒在此對象監(jiān)視器上等待的單個線程。如果所有線程都在此對象上等待,則會選擇喚醒其中一個線程。選擇是任意性的,并在對實現(xiàn)做出決定時發(fā)生。線程通過調用其中一個?wait?方法,在對象的監(jiān)視器上等待。?
直到當前線程放棄此對象上的鎖定,才能繼續(xù)執(zhí)行被喚醒的線程。被喚醒的線程將以常規(guī)方式與在該對象上主動同步的其他所有線程進行競爭;例如,喚醒的線程在作為鎖定此對象的下一個線程方面沒有可靠的特權或劣勢。
此方法只應由作為此對象監(jiān)視器的所有者的線程來調用。通過以下三種方法之一,線程可以成為此對象監(jiān)視器的所有者:
o?通過執(zhí)行此對象的同步實例方法。
o?通過執(zhí)行在此對象上進行同步的?synchronized?語句的正文。
o?對于?Class?類型的對象,可以通過執(zhí)行該類的同步靜態(tài)方法。
一次只能有一個線程擁有對象的監(jiān)視器。
?關于成員變量與局部變量:如果一個變量是成員變量,那么多個線程對同一個對象的成員變量進行操作時,他們對該成員變量是彼此影響的(也就是說一個線程對成員變量的改變會影響到另一個線程)。??如果一個變量是局部變量,那么每個線程都會有一個該局部變量的拷貝,一個線程對該局部變量的改變不會影響到其他的線程。
七:死鎖的問題:?
定義:線程1鎖住了對象A的監(jiān)視器,等待對象B的監(jiān)視器,線程2鎖住了對象B的監(jiān)視器,等待對象A的監(jiān)視器,就造成了死鎖。
?????導致死鎖的根源在于不適當?shù)剡\用“synchronized”關鍵詞來管理線程對特定對象的訪問。“synchronized”關鍵詞的作用是,確保在某個時刻只有一個線程被允許執(zhí)行特定的代碼塊,因此,被允許執(zhí)行的線程首先必須擁有對變量或對象的排他性訪問權。當線程訪問對象時,線程會給對象加鎖
Java中每個對象都有一把鎖與之對應。但Java不提供單獨的lock和unlock操作。下面筆者分析死鎖的兩個過程“上鎖”和“鎖死”?。
(1)?上鎖
?????許多線程在執(zhí)行中必須考慮與其他線程之間共享數(shù)據(jù)或協(xié)調執(zhí)行狀態(tài),就需要同步機制。因此大多數(shù)應用程序要求線程互相通信來同步它們的動作,在?Java?程序中最簡單實現(xiàn)同步的方法就是上鎖。在?Java?編程中,所有的對象都有鎖。線程可以使用?synchronized?關鍵字來獲得鎖。在任一時刻對于給定的類的實例,方法或同步的代碼塊只能被一個線程執(zhí)行。這是因為代碼在執(zhí)行之前要求獲得對象的鎖。
????為了防止同時訪問共享資源,線程在使用資源的前后可以給該資源上鎖和開鎖。給共享變量上鎖就使得?Java?線程能夠快速方便地通信和同步。某個線程若給一個對象上了鎖,就可以知道沒有其他線程能夠訪問該對象。即使在搶占式模型中,其他線程也不能夠訪問此對象,直到上鎖的線程被喚醒、完成工作并開鎖。那些試圖訪問一個上鎖對象的線程通常會進入睡眠狀態(tài),直到上鎖的線程開鎖。一旦鎖被打開,這些睡眠進程就會被喚醒并移到準備就緒隊列中。
(2)鎖死
?????如果程序中有幾個競爭資源的并發(fā)線程,那么保證均衡是很重要的。系統(tǒng)均衡是指每個線程在執(zhí)行過程中都能充分訪問有限的資源,系統(tǒng)中沒有餓死和死鎖的線程。當多個并發(fā)的線程分別試圖同時占有兩個鎖時,會出現(xiàn)加鎖沖突的情形。如果一個線程占有了另一個線程必需的鎖,互相等待時被阻塞就有可能出現(xiàn)死鎖。
????在編寫多線程代碼時,筆者認為死鎖是最難處理的問題之一。因為死鎖可能在最意想不到的地方發(fā)生,所以查找和修正它既費時又費力。例如,常見的例子如下面這段程序。print?
1?public?int?sumArrays(int[]?a1,?int[]?a2){??
2???int?value?=?0;??
3???int?size?=?a1.length;??
4???if?(size?==?a2.length)?{??
5??????synchronized(a1)?{?//1????????
6????????synchronized(a2)?{?//2??????????
7??????????for?(int?i=0;?i<size;?i++)??
8?????????????value?+=?a1[i]?+?a2[i];??
9????????}????
10??????}????
11???}?return?value;??
12?}???
這段代碼在求和操作中訪問兩個數(shù)組對象之前鎖定了這兩個數(shù)組對象。它形式簡短,編寫也適合所要執(zhí)行的任務;但不幸的是,它有一個潛在的問題。這個問題就是它埋下了死鎖的種子。
ThreadLocal類(這個類本人沒用過,占時不太懂)
首先,ThreadLocal?不是用來解決共享對象的多線程訪問問題的,一般情況下,通過ThreadLocal.set()?到線程中的對象是該線程自己使用的對象,其他線程是不需要訪問的,也訪問不到的。各個線程中訪問的是不同的對象。
另外,說ThreadLocal使得各線程能夠保持各自獨立的一個對象,并不是通過ThreadLocal.set()來實現(xiàn)的,而是通過每個線程中的new?對象?的操作來創(chuàng)建的對象,每個線程創(chuàng)建一個,不是什么對象的拷貝或副本。通過ThreadLocal.set()將這個新創(chuàng)建的對象的引用保存到各線程的自己的一個map中,每個線程都有這樣一個map,執(zhí)行ThreadLocal.get()時,各線程從自己的map中取出放進去的對象,因此取出來的是各自自己線程中的對象,ThreadLocal實例是作為map的key來使用的。
如果ThreadLocal.set()進去的東西本來就是多個線程共享的同一個對象,那么多個線程的ThreadLocal.get()取得的還是這個共享對象本身,還是有并發(fā)訪問問題。
?本文來自:曹勝歡博客專欄。轉載請注明出處:http://blog.csdn.Net/csh624366188
總結
以上是生活随笔為你收集整理的Java程序员从笨鸟到菜鸟之(十一)多线程讲解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java程序员从笨鸟到菜鸟之(十)枚举,
- 下一篇: Java程序员从笨鸟到菜鸟之(十二)ja