標籤:java 多線程
什麼叫做進程?
進程是作業系統中的一個任務,他是包含了某些資源的記憶體地區。一個進程可以包含了一個或多個執行單元稱作線程,這些線程可以被看做是同時執行的(實際是輪流佔用CPU資源,快速切換,達到看似同時執行)。每個進程還有一個私人虛擬地址空間,該空間只能被包含的線程所訪問。當作業系統建立一個進程之後,該進程會自動申請一個名為主線程的線程。
什麼叫做線程?
一個線程是進程的一個順序執行流,同類的線程可以共用一塊記憶體空間和一組系統資源。線程在切換的時候負荷很小,因此,線程也被成為輕進程。一個進程可以包含多組線程。
進程和線程的區別?
一個進程最少有一個線程,線程的劃分尺度要比進程小,為什麼這麼誰呢。因為每個進程在執行過程中都有一塊獨立的記憶體單元,進程在切換時候需要消耗大量資源。而多個線程是共用這塊記憶體的,線上程切換過程中並不需要切換記憶體單元,所以極大的提高了運行效率。多線程的意義在於一個應用程式中,有多個執行部分可以同時執行。但作業系統並沒有將這些線程看做獨立啟動並執行程式來進行進程調度和資源分派。
並發與並行?
多個線程同時執行,這隻是在宏觀上看的。事實上是OS將CPU已耗用時間劃分為許多時間片,獲得時間片的線程執行,其他的線程等待。由於切換速度非常快,所以在宏觀上是同時執行的,但微觀上並不是,這種叫做並發。而並行是無論微觀還是宏觀都是同時執行的,這裡通過多個CPU可以實現並存執行程式。
建立線程使用Thread建立線程並啟動線程
通過繼承Thread類,並且重寫Thread類中run方法來定義一個具體的線程。重寫run方法的目的在於定義線程要執行的邏輯。啟動線程不是調用run方法,而是調用線程的start()方法。start()方法將線程納入線程調度序列,是當前線程擷取時間片段後可以執行。Thread類是一個線程類,每個執行個體表示一個可以並發啟動並執行線程。
class SampleThread extends Thread {public void run() {System.out.println("通過繼承Thread類重寫run方法實現");}public static void main(String[] args) {SampleThread thread = new SampleThread();thread.start();}}
使用Runnable建立並啟動線程
實現Runnable介面並且重寫run方法定義線程體,然後建立線程的時候將Runnable執行個體傳入並啟動線程。這樣做的好處是,將定義線程與線程要執行的任務分開來,來減少耦合。並且,java是單繼承的,如果一個類繼承了Thread類,就無法繼承其他類了。(Thread類也是實現Runnable介面的)
在Runnable介面中,只定義了抽象run方法:
public abstract void run();
class SampleThread implements Runnable {public void run() {System.out.println("通過繼承Thread類重寫run方法實現");}public static void main(String[] args) {SampleThread sample = new SampleThread();Thread thread = new Thread(sample);thread.start();}}使用匿名內部類建立線程
Thread thread = new Thread(){public void run() {};};
中斷線程
當執行完run方法,或者沒有捕獲異常而使線程終止。但是如果想強制終止線程,可以使用interrupt方法來請求終止線程。
線程中斷是改變線程的中斷狀態,中斷之後可能是死亡、可能是等待新的任務、可能是繼續執行到下一步,這個要看程式本身。線程會不斷檢測這個中斷標識位,以判斷是否應該被中斷。但是如果線程被阻塞,在使用interrupt終止線程,會拋出InterruptedException異常。
使用isInterrupted來檢測線程是否被中斷
boolean isInterrupted()
boolean interrupted():測試當前線程是否被中斷,但是這一調用會產生副作用,他將當前線程中斷狀態設定為false
void interrupt():發送插斷要求,中斷狀態被設定為true
線程狀態
線程可以有6中狀態:
New:新建立
Runnable:可運行
Blocked:被阻塞
Waiting:等待
Timed waiting:計時等待
Terminated:被終止
通過getState()方法檢測當前線程狀態
新建立線程
使用new操作符建立一個線程,如new Thread(t),該線程還沒有開始運行。當一個線程處於新建立狀態時,說明程式還沒有運行線程中的代碼。
可運行線程
一旦調用start()方法,線程處於runnable狀態,運行狀態中的線程可能正在運行,也可能沒有運行,這個要看作業系統提供的已耗用時間。其實通過中斷就是為了讓其他線程獲得啟動並執行時間。
被阻塞線程和等待線程
被阻塞線程和等待線程,他們都是暫時不活動,不運行任何代碼且消耗最少的資源。直到線程調度器重新啟用它。
當一個線程試圖擷取一個內部的對象鎖,而該鎖被其他線程持有,則該線程進入阻塞狀態。當所有其他線程釋放該鎖,並且調度器允許線程持有他的時候,該線程變為非阻塞狀態。
當線程等待另一個線程通知調度器一個條件時,它自己進入等待狀態。如調用Object.wait方法或Thread.join方法,或者等待Lock或Condition是,出現這種情況。
計時狀態
有幾個方法有一個逾時參數,調用他們導致線程進入計時狀態,這一狀態將一直到逾時期滿或者接到適當的通知。帶計時的方法Thread.sleep()、Object.wait()、Thread.join()、Lock.tryLock()、Condition.await
被終止線程
有兩個原因導致線程終止:
run方法正常退出而自然死亡
因為沒有捕獲一個異常而終止了run方法而意外死亡
線程操作API
擷取線程資訊
Thread Thread.currentThread()方法:擷取當前運行程式碼片段的線程
Thread thread = Thread.currentThread();
boolean isAlive():測試線程是否處於活動狀態
boolean isDaemon():測試線程是否是守護線程
boolean isInterrupted():測試線程是否已經被中斷
long getId():擷取線程標識符
String getName():返回線程名稱
int getPriority():返回線程優先順序
Thread.state getState():擷取線程狀態
線程優先順序
線程的切換是由線程調度控制的,我們無法通過代碼無法幹涉,但是我們可以通過提高線程的優先順序來最大程度的改善線程擷取時間片的幾率
線程優先順序被分為10個等級,分別為1-10,其中1最小,10最大。線程提供了三個常量值表示最低、最高以及預設優先順序
Thread.MIN_PRIORIITY
Thread.MAX_PRIORITY
Thread.NORMAL_PRIORITY
通過setPriority()方法進行設定
守護線程
java中線程分為兩類:User Thread(使用者線程)和Daemon Thread(守護線程)。Daemon線程是為其他線程運行提供便利的,如記憶體回收行程就是一個守護線程。Daemon與User Thread沒有什麼不同,主要的不同就是虛擬機器的離開:當所有User Thread全部運行退出後,只剩下Daemon Thread,這時候虛擬機器也就退出了。守護進程並非只有虛擬機器可以提供,使用者也可以設定通過:setDaemon(boolean on)
不要輕易設定守護線程,因為你不知道User Thread什麼時候完成,如果當User Thread運行完畢後,你定義的這個Daemon Thread還沒運行完畢,也會被強制退出
sleep()方法
sleep(long mills)方法是使當前進程阻塞狀態,進入阻塞的時間為指定的毫秒數,阻塞時間過去後進入Runable狀態,等待分配時間片。這個方法在聲明的時候拋出了InterruptedException,所以在使用該方法的時候也要捕獲這個異常。
yield()方法
static void yield():主動讓出當次時間片回到Runnable狀態,等待分配時間片。
join()方法
用於等待當前線程結束,該方法會拋出InterruptedException。
線程同步
當多個線程同時讀寫同一臨界資源的時候會發生線程並發安全問題。比如:當兩個線程讀取相同的對象的時候,並且每個一個線程都修改了該對象的狀態,這時候錯誤就出現了,這樣的情況稱為競爭條件。
如果想解決安全執行緒問題,需要將非同步作業變為同步操作:
非同步作業:多線程並行作業,各自執行各自的
同步操作:有先後順序的操作,你執行我不執行
鎖對象
java提供了兩種機制防止代碼塊受並發訪問的幹擾,一種是都熟悉的使用synchronized關鍵字,另一種是使用ReentrantLock類。
ReentrantLock:
Lock lock = new ReentrantLock();lock.lock();try{//臨界區代碼塊,這樣能夠保證任何時刻只有一個線程進入臨界區,一旦一個線程封鎖了鎖對象,其他任何線程都無法通過lock語句//當調用lock時,他們被堵塞,直到第一個線程釋放鎖對象}finally{lock.unlock();}
unlock釋放鎖對象一定要放到finally中,如果臨界區代碼拋出異常,finally中能夠釋放鎖對象,否則其他線程永遠都將阻塞。
每個該類對象都有自己的ReentrantLock()對象,如果兩個線程訪問同一個該類對象,則提供串列服務。但當訪問不同的對象的時候,每個線程都有不同的鎖對象,兩個線程不會發生堵塞。操作不同的執行個體,線程間是不會受影響的。
條件對象
考慮,如果當一個線程進入臨界區後發現需要某個條件才能夠運行,這時候他已經獲得了鎖對象,但是無法向下執行,java中通過條件對象(條件變數)來解決。
一個鎖對象可以有多個條件對象,使用newCondition()方法獲得條件對象,當到了可能需要某種條件才能向下執行的時候(可以通過判斷,來確定是否條件滿足)使用await()方法,這時候當前線程被阻塞,並放棄了鎖。當另一線程執行完畢後,條件滿足時調用signalAll()方法,這樣才能啟用因為一個條件而等待的所有線程。
Lock lock = new ReentrantLock();Condition condition = lock.newCondition(); lock.lock();try{ if(//判斷某一個條件是否滿足,如果不滿足) conditon.await(); //臨界區代碼塊,這樣能夠保證任何時刻只有一個線程進入臨界區,一旦一個線程封鎖了鎖對象,其他任何線程都無法通過lock語句//當調用lock時,他們被堵塞,直到第一個線程釋放鎖對象 ... //其他線程執行完之後,可能滿足當前條件,重新啟用 condition.signalAll(); }finally{lock.unlock();}
使用synchronized關鍵字
將方法用synchronized關鍵字聲明。從jdk1.0,java就為每個對象提供了一個內部鎖,如果一個方法使用synchronized關鍵字修飾,那麼對象鎖將保護整個方法,即要調用該方法,線程必須獲得內部鎖對象。內部鎖對象也有一個相關條件,wait()方法添加一個線程到等待集中,notify/notofyAll方法接觸等待線程的阻塞狀態。
即wait()方法相當於上面的await()方法,notify/notifyAll相當於上面的signal/signalAll方法。
public synchronized void method(){if(//條件狀態)wait(); .... notifyAll();}
這種直接給方法進行加鎖是當前方法都需要加鎖,還有一種是通過同步阻塞。
synchronized (同步監視器—鎖對象){ //代碼塊}
選擇合適的鎖對象:
synchronized需要對一個對象上鎖以保證線程同步,那麼這個對象應該保證,當多個需要同步的線程在訪問該同步塊時,看到的應該是同一個鎖對象,否則達不到同步效果,通常使用this作為鎖對象。
那麼什麼情況下使用顯示的線程鎖lock/unlock,什麼時候使用synchronized關鍵字?
最好的情況下是都不使用,java.util.concurrnt包中的一種機制,為你所處理代碼加鎖。例如可以使用阻塞隊列來完成
如果使用synchronized關鍵字適合你的程式,那麼盡量使用它,可以減少代碼量,防止出錯。
ExecutorService實現線程池
線程池作用:控制線程數量、重用線程
當一個程式中若建立大量線程,並在任務結束後銷毀,會給系統帶來過度消耗資源,以及切換線程帶來的危險,從而導致系統崩潰,可以使用線程池解決這個問題。
原理:首先建立一些線程,他們的集合成為線程池,當伺服器從用戶端接受一個請求後,從線程池取出一個空閑線程,服務完成後不關閉線程而是還回到線程池中。線上程池編程模型下,任務是提交給線程池的,不是提交給某個線程,線程池拿到任務後,線上程池中找有無空閑線程,將任務交給空閑線程。一個線程只能執行一個任務,但可以同時向線程池提交多個任務。
實現方式:
1、
ExecutorService pool = Executors.newCachedThreadPool();
建立一個可根據需要建立新線程的線程池,但是在以前構造的線程可用時將重用他們。
2、
ExecutorService pool = Executors.newFixedThreadPool(nThreads);
建立一個固定集合的線程池,共用無界隊列方式來運行這些線程。
3、
ExecutorService pool = Executors.newScheduledThreadPool(corePoolSize)
建立一個線程池,安排在給定延遲後運行命令或者定期執行。
4、
ExecutorService pool = Executors.newSingleThreadExecutor();
建立一個使用單個work線程的Executor,以無界序列方式來運行該線程。
class SampleThread {public static void main(String[] args) {ExecutorService pool = Executors.newFixedThreadPool(2);Handler handler = new Handler();for(int i =0 ;i<5;i++){pool.execute(handler);}}}class Handler implements Runnable{@Overridepublic void run() {String name = Thread.currentThread().getName();for(int i=0;i<10;i++){System.out.println("當前執行的線程為:"+name);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("任務執行完畢");}}
BlockingQueue
BlockingQueue是雙緩衝序列,在多線程並發時,若需要使用隊列,我們可以使用Query 。但是同步操作會降低並發對Query的操作,可以使用BlockingQuery。
BlockingQueue內部使用兩條隊列,可允許兩個兩個線程同時向隊列一個做儲存,一個做讀操作。在保證並發安全的同時提高了隊列的存取效率
當BlockingQueue為空白的時候向其隊列取東西,則線程將被中斷進度等待狀態,直到BlockingQueue有東西,該線程才會被喚醒繼續操作。如果BlockingQueue隊列是滿的,再向裡面放入東西的時候,該線程也會被中斷進入等待狀態,直到BlockingQueue中有空間,才會被喚醒繼續操作。
| |
拋出異常 |
特殊值 |
阻塞 |
逾時 |
| 插入 |
add(e) |
offer(e) |
put(e) |
offer(e,time,unit) |
| 移除 |
remove() |
poll() |
take() |
poll(time,unit) |
| 檢查 |
element() |
peek() |
不可用 |
不可用 |
add(e):向隊列中加入元素,如果BlockingQueue可以容納,則返回true,否則拋出異常
offer(e):向隊列中加入元素,如果BlockingQueue可以容納,則返回true,否則返回false
put(e):向隊列中加入元素,如果BlockingQueue可以容納,則插入,如果不能插入則該線程進入阻塞狀態,直到能夠被插入
下面的方法也是這樣的原理
BlockingQueue的四個實作類別
ArrayBlockingQueue:規定大小的BlockingQueue,需要指定一個int型參數指定其大小,所含的對象是FIFO順序排序的。
LinkedBlockingQueue:大小不定的BlockingQueue,如果建構函式指定一個大小,則其為BlockingQueue隊列大小,如果不指定則為Integer.MAX_VALUE大小,所含對象是FIFO順序排序的。
PriorityBlockingQueue:與LinkedBlockQueue類似,但是不是FIFO順序排序的。而是依據對象的自然順序或者建構函式指定的Comparator決定的順序。
SynchronousBlockingQueue:是一種特殊的BlockingQueue,對其操作需要方和取交替執行。
其中LinkedBlockingQueue和ArrayBlockingQueue比較起來,它們背後所用的資料結構不一樣,導致LinkedBlockingQueue的資料輸送量要大於ArrayBlockingQueue,但線上程數量很大時其效能的可預見性低於ArrayBlockingQueue。
查看其源碼會發現,對隊列每個操作都加上了ReentrantLock鎖對象。
測試offer方法,如果插入超過兩秒沒有進入序列返回false
BlockingQueue<Integer> queue = new ArrayBlockingQueue<Integer>(10);for(int i =0;i<20;i++){try {boolean result = queue.offer(i, 2, TimeUnit.SECONDS);//插入2秒後如果沒有成功返回falseSystem.out.println("存入是否成功:"+ result);} catch (InterruptedException e) {e.printStackTrace();}}
測試poll()方法,如果超過2秒沒有取出元素,則返回false
BlockingQueue<Integer> queue = new ArrayBlockingQueue<Integer>(10);for(int i =0;i<10;i++){queue.offer(i);}for(int i=0;i<20;i++){try {Integer result = queue.poll(2, TimeUnit.SECONDS); //如果兩秒後取不出元素,則返回nullSystem.out.println("結果:"+result);} catch (InterruptedException e) {e.printStackTrace();}}
著作權聲明:本文為博主原創文章,未經博主允許不得轉載。
java之多線程