java中的多線程
一、java線程基本介紹
1、進程與線程的區別
進程是指一個內存中運行的應用程序,每個進程都有一塊獨立的內存空間,一個進程包含一到多個線程。
每個線程都有他所屬的進程,每個線程也就是該進程的一條執行路徑,線程之間是高頻率快速輪換執行的,‘同時’執行只是給人的感覺。
2、Java當中線程一般有5中狀態
創建狀態:生成線程對象,并沒有調用該對象的start方法,這是線程處于創建狀態。
就緒狀態:當調用了線程的start方法,線程就進入就緒狀態,調用start方法后線程不是立即執行的,只是開始排隊等待執行了,具體什么時候執行得看CPU心情,當線程從等待或者休眠狀態回來之后也是進入到就緒狀態。
運行狀態:開始運行run()函數的代碼,這時候就是運行狀態啦。
阻塞狀態:線程正在運行的時候被暫停就是進入到阻塞狀態,sleep,suspend,wait都可以使線程進入阻塞狀態。
死亡狀態:run()方法運行結束或者調用了線程的stop方法后,該線程就會死亡,對于已經死亡的線程無法使用start方法使其進入就緒狀態。
在java中要想實現多線程,有兩種手段,一種是繼續Thread類,另外一種是實現Runable接口。
先看一個簡單的例子:
1 package com.hxw.Threads; 2 class ThreadTest { 3 /** 4 * 觀察直接調用run()和用start()啟動一個線程的差別 5 * @author HaiCheng 6 * @param args 7 * @throws Exception 8 */ 9 public static void main(String[] args){10 Threadr=new ThreadDemo("直接調用run執行");11 r.run();12 Thread t1=new ThreadDemo("線程一");13 t1.start();14 Thread t2=new ThreadDemo("線程二");15 t2.start();16 for(int i=0;i<10;i++){17 System.out.run和start的區別
1)start:
用start方法來啟動線程,真正實現了多線程運行,這時無需等待run方法體代碼執行完畢而直接繼續執行下面的代碼。通過調用Thread類的start()方法來啟動一個線程,這時此線程處于就緒(可運行)狀態,并沒有運行,一旦得到spu時間片,就開始執行run()方法,這里方法run()稱為線程體,它包含了要執行的這個線程的內容,Run方法運行結束,此線程隨即終止。
2)run:
run方法只是類的一個普通方法而已,如果直接調用Run方法,程序中依然只有主線程這一個線程,其程序執行路徑還是只有一條,還是要順序執行,還是要等待run方法體執行完畢后才可繼續執行下面的代碼,這樣就沒有達到寫線程的目的。
總結:調用start方法方可啟動線程,而run方法只是thread的一個普通方法調用,還是在主線程里執行。
class 類名 extends Thread{方法1;方法2;…public void run(){// other code…}屬性1;屬性2;… }
通過實現Runnable接口:
大致框架是:
class 類名 implements Runnable{方法1;方法2;…public void run(){// other code…}屬性1;屬性2;…}
來先看一個小例子吧:
/** * @author Rollen-Holt 實現Runnable接口 * */class hello implements Runnable { public hello() { } public hello(String name){ this.name= name; } public void run() { for (int i= 0; i < 5; i++) { System.out.println(name+ "運行 " + i); } } public static void main(String[]args) { helloh1=new hello("線程A"); Threaddemo= new Thread(h1); helloh2=new hello("線程B"); Threaddemo1=new Thread(h2); demo.start(); demo1.start(); } private Stringname;}
【可能的運行結果】:
線程A運行0
線程B運行 0
線程B運行 1
線程B運行 2
線程B運行 3
線程B運行 4
線程A運行1
線程A運行2
線程A運行3
線程A運行4
關于選擇繼承Thread還是實現Runnable接口?
其實Thread也是實現Runnable接口的:
Thread和Runnable的區別:
- 如果一個類繼承Thread,則不適合資源共享。但是如果實現了Runable接口的話,則很容易的實現資源共享。
/** * @author Rollen-Holt 繼承Thread類,不能資源共享 * */class hello extends Thread { public void run() { for (int i= 0; i < 7; i++) { if (count> 0) { System.out.println("count=" + count--); } } } public static void main(String[]args) { helloh1 = new hello(); helloh2 = new hello(); helloh3 = new hello(); h1.start(); h2.start(); h3.start(); } private int count =5;}
【運行結果】:
count= 5
count= 4
count= 3
count= 2
count= 1
count= 5
count= 4
count= 3
count= 2
count= 1
count= 5
count= 4
count= 3
count= 2
count= 1
大家可以想象,如果這個是一個買票系統的話,如果count表示的是車票的數量的話,說明并沒有實現資源的共享。
我們換為Runnable接口
class MyThread implements Runnable{ private int ticket =5; //5張票 public void run() { for (int i=0;i<=20; i++) { if (this.ticket> 0) { System.out.println(Thread.currentThread().getName()+"正在賣票"+this.ticket--); } } }}public class lzwCode { public static void main(String[] args) { MyThreadmy = new MyThread(); new Thread(my,"1號窗口").start(); new Thread(my,"2號窗口").start(); new Thread(my,"3號窗口").start(); }}【運行結果】:
count= 5
count= 4
count= 3
count= 2
count= 1
總結一下吧:
實現Runnable接口比繼承Thread類所具有的優勢:
1):適合多個相同的程序代碼的線程去處理同一個資源
2):可以避免java中的單繼承的限制
3):增加程序的健壯性,代碼可以被多個線程共享,代碼和數據獨立。
所以,本人建議大家盡量實現接口。
/** * @author Rollen-Holt * 取得線程的名稱 * */class hello implements Runnable { public void run() { for (int i= 0; i < 3; i++) { System.out.println(Thread.currentThread().getName()); } } public static void main(String[]args) { hellohe = new hello(); new Thread(he,"A").start(); new Thread(he,"B").start(); new Thread(he).start(); }}【運行結果】:
A
A
A
B
B
B
Thread-0
Thread-0
Thread-0
說明如果我們沒有指定名字的話,系統自動提供名字。
提醒一下大家:main方法其實也是一個線程。在java中所以的線程都是同時啟動的,至于什么時候,哪個先執行,完全看誰先得到CPU的資源。
在java中,每次程序運行至少啟動2個線程。一個是main線程,一個是垃圾收集線程。因為每當使用java命令執行一個類的時候,實際上都會啟動一個JVM,每一個jVM實際在就是在操作系統中啟動了一個進程。
二、線程常用方法解析
1.判斷線程是否啟動:
方法解析:在這里提一下上面的
currentThread()方法,返回對當前正在執行的線程對象的引用。
isAlive(),測試線程是否處于活動狀態。如果線程已經啟動且尚未終止,則為活動狀態。
1 packagecom.hxw.Threads; 2 /** 3 * @authorRollen-Holt 判斷線程是否啟動 4 * */ 5 class hello implementsRunnable { 6 public voidrun() { 7 for (int i =0; i < 3; i++) { 8 System.out.println(Thread.currentThread().getName()); 9 }10 }11 public static voidmain(String[] args) {12 hello he = newhello();13 Thread demo= new Thread(he);14 System.out.println("線程啟動之前---》" +demo.isAlive());15 demo.start();16 System.out.println("線程啟動之后---》" +demo.isAlive());17 System.out.println("線程啟動之后后---》" +demo.isAlive());18 System.out.println("線程啟動之后后---》" +demo.isAlive());19 System.out.println("線程啟動之后后---》" +demo.isAlive());20 System.out.println("線程啟動之后后---》" +demo.isAlive());21 System.out.println("線程啟動之后后---》" +demo.isAlive());22 System.out.println("線程啟動之后后---》" +demo.isAlive());23 System.out.println("線程啟動之后后---》" +demo.isAlive());24 25 }26 }【運行結果】
線程啟動之前---》false
線程啟動之后---》true
線程啟動之后后 ---》true
Thread-0
線程啟動之后后 ---》true
線程啟動之后后 ---》true
Thread-0
線程啟動之后后 ---》true
Thread-0
線程啟動之后后 ---》true
線程啟動之后后 ---》false
線程啟動之后后 ---》false
從上面的例子來看:確實是有主線程和子線程在運行的,而且主線程也有可能在子線程結束之前結束。并且子線程不受影響,不會因為主線程的結束而結束。這個叫非守護線程。
上面的例子也表示出了在run方法執行完成后,線程就死亡了,只是由于主線程和子線程之間同步問題,如果想原文的那樣在demo.start()后只打印一個alive()是有問題的。
2.線程的強制執行:thread.Join把指定的線程加入到當前線程,可以將兩個交替執行的線程合并為順序執行的線程。比如在線程B中調用了線程A的Join()方法即A.jion(),直到線程A執行完畢后,才會繼續執行線程B。
t.join(); //使調用線程t在此之前執行完畢。
t.join(1000); //等待t線程,等待時間是1000毫秒
1 /** 2 * @author Rollen-Holt 線程的強制執行 3 * */ 4 class helloimplements Runnable { 5 public void run(){ 6 for (int i= 0; i < 3; i++) { 7 System.out.println(Thread.currentThread().getName()); 8 } 9 }10 11 public static void main(String[]args) {12 hellohe = new hello();13 Threaddemo = new Thread(he,"線程");14 demo.start();15 for(int i=0;i<50;++i){16 if(i>10){17 try{18 demo.join(); //強制執行demo()19 }catch (Exceptione) {20 e.printStackTrace();21 }22 }23 System.out.println("main線程執行-->"+i);24 }25 }26 }【運行的結果】:
main線程執行-->0
main線程執行-->1
main線程執行-->2
main線程執行-->3
main線程執行-->4
main線程執行-->5
main線程執行-->6
main線程執行-->7
main線程執行-->8
main線程執行-->9
main線程執行-->10
線程
線程
線程
main線程執行-->11
main線程執行-->12
main線程執行-->13
...
3.線程的休眠:sleep()在指定的毫秒數內讓當前正在執行的線程休眠(暫停執行),
sleep()中指定的時間是線程不會運行的最短時間。因此,sleep()方法不能保證該線程睡眠到期后就開始執行。
1 packagecom.hxw.Threads; 2 importjava.text.SimpleDateFormat; 3 /** 4 * @authorRollen-Holt 線程的休眠 5 * */ 6 class hello implementsRunnable { 7 public voidrun() { 8 for (int i =0; i < 3; i++) { 9 try {10 Thread.sleep(2000);11 } catch(Exception e) {12 e.printStackTrace();13 }14 System.out.println(Thread.currentThread().getName()+ i+" "+(newSimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(newjava.util.Date())));15 }16 }17 18 public static voidmain(String[] args) {19 hello he = newhello();20 Thread demo= new Thread(he, "線程");21 demo.start();22 }23 }【運行結果】:(結果每隔2s輸出一個)
線程0 2014-08-1606:06:21
線程1 2014-08-1606:06:23
線程2 2014-08-1606:06:25
4.線程的優先級:
與線程休眠類似,線程的優先級仍然無法保障線程的執行次序。只不過,優先級高的線程獲取CPU資源的概率較大,優先級低的并非沒機會執行。
線程的優先級用1-10之間的整數表示,數值越大優先級越高,默認的優先級為5,主線程的優先級也是5。
在一個線程中開啟另外一個新線程,則新開線程稱為該線程的子線程,子線程初始優先級與父線程相同。可以用下面方法設置和返回線程的優先級。public final void setPriority(int newPriority)設置線程的優先級。public final int getPriority()返回線程的優先級。newPriority為線程的優先級,其取值為1到10之間的整數,也可以使用Thread類定義的常量來設置線程的優先級,這些常量分別為:Thread.MIN_PRIORITY、Thread.NORM_PRIORITY、Thread.MAX_PRIORITY,它們分別對應于線程優先級的1、5和10,數值越大優先級越高。當創建Java線程時,如果沒有指定它的優先級,則它從創建該線程那里繼承優先級。
1 /** 2 * @author Rollen-Holt 線程的優先級 3 * */ 4 class hello implements Runnable { 5 public void run() { 6 for(int i=0;i<5;++i){ 7 System.out.println(Thread.currentThread().getName()+"運行"+i); 8 } 9 }10 11 public static void main(String[]args) {12 Threadh1=new Thread(new hello(),"A");13 Threadh2=new Thread(new hello(),"B");14 Threadh3=new Thread(new hello(),"C");15 h1.setPriority(8);16 h2.setPriority(2);17 h3.setPriority(6);18 h1.start();19 h2.start();20 h3.start();21 22 }23 } 【運行結果】:
A運行0
A運行1
A運行2
A運行3
A運行4
B運行0
C運行0
C運行1
C運行2
C運行3
C運行4
B運行1
B運行2
B運行3
B運行4
。
5.線程的禮讓:yield()
通過調用yield方法,線程可能自動的轉發控制權給其他等待的線程,一般情況,在等待其他具有相同優先級的線程產生的某個結果是,線程會轉讓控制權。考慮如下情形,多線程情況下,有一個可讀寫文件由多個線程來讀寫操作,多線程情況下,為了保證數據一致性,在讀寫訪問時都將鎖住這個文件,讀寫線程可能都運行在相同的優先級,現在擁有文件鎖的線程可能會周期性的將控制權轉讓給另一個與之競爭的線程。
需要注意的是,yield()方法對JVM來說是“提示”,而不是強制要求,也沒有結束。JVM無法保證線程調度的確定性,這一點我們會在下面的例子展示出來,除此之外也不能確定這個提示是讓更低級的線程獲得控制權還是同級的線程獲得控制權,盡管大多數情況下是同級線程獲得控制權的。這個方法比較不穩定,一般不常用。它所表達的意思口語化一點就是:“我急獲得了足夠的CPU時間,想讓其他線程有機會運行,我將在一段時間后運行剩余的代碼”,這與sleep方法不一樣,sleep的意思是:“在n毫秒的時間內我不想運行,就算沒有其他線程想運行,也別讓我運行”。
1 package com.hxw.Threads; 2 /** 3 * @author Rollen-Holt 線程的優先級 4 * */ 5 class hello2 implements Runnable { 6 synchronized public void run() { 7 for(int i=0;i<12;++i){ 8 System.out.println(Thread.currentThread().getName()+"運行"+i); 9 if(i==3){10 Thread.currentThread().yield();11 System.out.println(Thread.currentThread().getName()+"將自己的線程禮讓出來了");12 }13 }14 }15 16 public static void main(String[] args) {17 Thread h1=new Thread(new hello2(),"A");18 Thread h2=new Thread(new hello2(),"B");19 h1.start();20 h2.start();21 22 }23 }A運行0
B運行0
A運行1
B運行1
A運行2
B運行2
A運行3
A將自己的線程禮讓出來了
A運行4
A運行5
A運行6
A運行7
A運行8
A運行9
A運行10
A運行11
A運行12
B運行3
B將自己的線程禮讓出來了
B運行4
B運行5
B運行6
B運行7
B運行8
B運行9
B運行10
B運行11
B運行12
相信讀者看完上面的結果想“呵呵!”了,A真不客氣,都說將自己的線程禮讓出來了還愣是把自己運行完了才把控制權交給B,這就是yiel()方法的“提示”轉讓。當然有些時候還會準確的讓出控制權的。
6.線程的中斷(打擾):
中斷的原理:Java中斷機制是一種協作機制,也就是說通過中斷并不能直接終止另一個線程,而需要被中斷的線程自己處理中斷。這好比是家里的父母叮囑在外的子女要注意身體,但子女是否注意身體,怎么注意身體則完全取決于自己。
Java中斷模型也是這么簡單,每個線程對象里都有一個boolean類型的標識(不一定就要是Thread類的字段,實際上也的確不是,這幾個方法最終都是通過native方法來完成的),代表著是否有中斷請求(該請求可以來自所有線程,包括被中斷的線程本身)。例如,當線程t1想中斷線程t2,只需要在線程t1中將線程t2對象的中斷標識置為true,然后線程t2可以選擇在合適的時候處理該中斷請求,甚至可以不理會該請求,就像這個線程沒有被中斷一樣。
關于線程的中斷/打擾有三個重要方法,interrupt,isInterrupted,interrupted
interrupt:中斷(打擾)線程。
interrupted:靜態方法,測試當前線程是否已經中斷。線程的中斷狀態 由該方法清除。換句話說,如果連續兩次調用該方法,則第二次調用將返回 false(即線程狀態為非中斷狀態,而其實已經是終端狀態,知識沒有了這個終端標識了而已)。
isInterrupted: 測試線程是否已經中斷。這個方法不是靜態的,調用是需要對象引用,而且這個方法不會清空中斷標志。
當另一個線程通過調用Thread.interrupt()中斷一個線程時,會出現以下兩種情況之一。一種情況正常的話會設置該線程的終端狀態,但是如果那個線程在執行一個低級可中斷阻塞方法,例如Thread.sleep()、Thread.join()或Object.wait(),那么它將取消阻塞并拋出InterruptedException。
注意:j2se 1.2開始,stop,suspend,resume方法就已經不提倡使用了,因為他們容易造成死鎖。
反對使用stop(),是因為它不安全。它會解除由線程獲取的所有鎖定,而且如果對象處于一種不連貫狀態,那么其他線程能在那種狀態下檢查和修改它們。結果很難檢查出真正的問題所在。suspend()方法容易發生死鎖。調用suspend()的時候,目標線程會停下來,但卻仍然持有在這之前獲得的鎖定。此時,其他任何線程都不能訪問鎖定的資源,除非被"掛起"的線程恢復運行。對任何線程來說,如果它們想恢復目標線程,同時又試圖使用任何一個鎖定的資源,就會造成死鎖。所以不應該使用suspend(),而應在自己的Thread類中置入一個標志,指出線程應該活動還是掛起。若標志指出線程應該掛起,便用wait()命其進入等待狀態。若標志指出線程應當恢復,則用一個notify()重新啟動線程。
這個中斷的例子匆忙舉出一個是不太準確的,后面會專門寫一個文章來測試這個用法。
7. 守護線程和非守護線程
JVM中存在兩種線程:用戶線程和守護線程。
所謂的守護線程,是指用戶程序在運行的時候后臺提供的一種通用服務的線程,比如用于垃圾回收的
垃圾回收線程。這類線程并不是用戶線程不可或缺的部分,只是用于提供服務的"服務線程"。
基于這個特點,當虛擬機中的用戶線程全部退出運行時,守護線程沒有服務的對象后,JVM也就退出了,反之還有任意一個用戶線程在,JVM都不會退出。
我們如何開始一個自定義的守護進程呢?正如上述代碼一樣,答案很簡單,就是在Thread.start()方法之前使用setDaemon(true)方法,通過此方法將Thread類中的boolean daemon=true;JVM就會將該線程歸為守護線程
說完了守護線程如何產生和特點,下面簡要的談談使用守護線程應該注意的地方。
1、thread.setDaemon(true)必須在thread.start()之前設置,否則會跑出一個異常。你不能把正在運行
的常規線程設置為守護線程。
2、在守護線程中產生的線程也是守護線程。(這點讀者可結合工具自己驗證)
3、我們自己產生的守護線程應該避免訪問一些類似于文件、數據庫等固有資源,因為由于JVM沒有用戶
線程之后,守護線程會馬上終止。
這一個不做例子,因為在eclipse查看不了jvm退出后其他用戶線程的動作,所以查看起來比較復雜,需要用jvisualvm.exe來看,這一點會再另外寫一篇文章闡述。
以上是線程的基本用法,下一篇是關于線程的同步,因為我寫的比較詳細,也參考了很多資料,希望能把即使比較偏的知識點也囊括進來,如果文章有什么問題和要改進的地方,請提出來大家討論,我會一一回復,我非常想交一些技術上的朋友。
新聞熱點
疑難解答