java多線程功力,java多線程
一、操作系統中線程和進程的概念
現在的操作系統是多任務操作系統。多線程是實現多任務的一種方式。多線程編程可以使程序具有兩條或兩條以上的並發執行線索。
進程是指一個內存中運行的應用程序,每個進程都有自己獨立的一塊內存空間,一個進程中可以啟動多個線程。比如在Windows系統中,一個運行的exe就是一個進程。
線程是指進程中的一個執行流程,一個進程中可以運行多個線程。比如java.exe進程中可以運行很多線程。線程總是屬於某個進程,進程中的多個線程共享進程的內存。
“同時”執行是人的感覺,在線程之間實際上輪換執行。
例如:網上購物時郵件通知程序,用戶單擊提交按鈕確認訂單時,一方面要顯示信息提示用戶訂單已經確認,一方面應該自動給用戶發送一份電子郵件。
二、Java中的線程
使用java.lang.Thread類或者java.lang.Runnable接口編寫代碼來定義、實例化和啟動新線程。
一個Thread類實例只是一個對象,像Java中的任何其他對象一樣,具有變量和方法,生死於堆上。
Java中,每個線程都有一個調用棧,即使不在程序中創建任何新的線程,線程也在後台運行著。
一個Java應用總是從main()方法開始運行,mian()方法運行在一個線程內,它被稱為主線程。
一旦創建一個新的線程,就產生一個新的調用棧。
線程總體分兩類:用戶線程和守候線程。
當所有用戶線程執行完畢的時候,JVM自動關閉。但是守候線程卻不獨立於JVM,守候線程一般是由操作系統或者用戶自己創建的。
二、實例化線程
1、繼承java.lang.Thread的類,那麼該類對象便具有了線程的能力。此類的自身對象就是線程對象,在創建線程對象時只需要創建自身的對象即可。
要重寫繼承的run()方法,run()方法中的代碼就是線程所要執行任務的描述。
一旦線程啟動,run方法中的代碼將成為一條獨立的執行線索。
注意,重寫後的run方法雖然具有成為執行線索的能力,但也可以作為一般的方法調用,直接調用run方法並不產生新的執行線索
2、實現了java.lang.Runnable接口的類,其自身對象並不是一個線程,只是在該類中通過實現run方法指出了線程需要完成的任務。
若想得到一個線程,必須創建Thread類或其子類對象,這時就要使用Thread類的特定構造器來完成這個工作。
//創建實現Runnable接口的類的對象。
MyRunnable mr=new MyRunnable();
Thread t=new Thread(mr);
這種方式實際上是告訴線程對象要執行的任務run方法在哪裡。
實現Runnable接口的類的對象只是指出了線程需要完成的任務,其本身並不是線程對象。
三、啟動線程
在線程的Thread對象上調用start()方法,就啟動線程了,又開辟了另一條執行線索。
在調用start()方法之前:線程處於新狀態中,新狀態指有一個Thread對象,但還沒有一個真正的線程。
在調用start()方法之後:發生了一系列復雜的事情
啟動新的執行線程(具有新的調用棧);
該線程從新狀態轉移到可運行狀態;
當該線程獲得機會執行時,其目標run()方法將運行。
注意:對Java來說,run()方法沒有任何特別之處。像main()方法一樣,它只是新線程知道調用的方法名稱(和簽名)。
四、例子
1、實現Runnable接口的多線程例子
/**
* 實現Runnable接口的類
*
* @author leizhimin 2008-9-13 18:12:10
*/
public class DoSomething implements Runnable {
private String name;
public DoSomething(String name) {
this.name = name;
}
public void run() {
for (int i = 0; i < 5; i++) {
for (long k = 0; k < 100000000; k++) ;
System.out.println(name + ": " + i);
}
}
}
/**
* 測試Runnable類實現的多線程程序
*
*/
public class TestRunnable {
public static void main(String[] args) {
DoSomething ds1 = new DoSomething("阿三");
DoSomething ds2 = new DoSomething("李四");
Thread t1 = new Thread(ds1);
Thread t2 = new Thread(ds2);
t1.start();
t2.start();
}
}
執行結果:
李四: 0
阿三: 0
李四: 1
阿三: 1
李四: 2
李四: 3
阿三: 2
李四: 4
阿三: 3
阿三: 4
Process finished with exit code 0
2、擴展Thread類實現的多線程例子
/**
* 測試擴展Thread類實現的多線程程序
*
*/
public class TestThread extends Thread{
public TestThread(String name) {
super(name);
}
public void run() {
for(int i = 0;i<5;i++){
for(long k= 0; k <100000000;k++);
System.out.println(this.getName()+" :"+i);
}
}
public static void main(String[] args) {
Thread t1 = new TestThread("阿三");
Thread t2 = new TestThread("李四");
t1.start();
t2.start();
}
}
執行結果:
阿三 :0
李四 :0
阿三 :1
李四 :1
阿三 :2
李四 :2
阿三 :3
阿三 :4
李四 :3
李四 :4
五、一些常見問題
1、線程的名字,一個運行中的線程總是有名字的,名字有兩個來源,一個是虛擬機自己給的名字,一個是你自己的定的名字。在沒有指定線程名字的情況下,虛擬機總會為線程指定名字,並且主線程的名字總是mian,非主線程的名字不確定。
3、獲取當前線程的對象的方法是:Thread.currentThread();
4、在上面的代碼中,只能保證:每個線程都將啟動,每個線程都將運行直到完成。一系列線程以某種順序啟動並不意味著將按該順序執行。對於任何一組啟動的線程來說,調度程序不能保證其執行次序,持續時間也無法保證。
5、當線程目標run()方法結束時該線程完成。
6、一旦線程啟動,它就永遠不能再重新啟動。只有一個新的線程可以被啟動,並且只能一次。一個可運行的線程或死線程可以被重新啟動。
7、線程的調度是JVM的一部分,在一個CPU的機器上上,實際上一次只能運行一個線程。一次只有一個線程棧執行。JVM線程調度程序決定實際運行哪個處於可運行狀態的線程。
眾多可運行線程中的某一個會被選中做為當前線程。可運行線程被選擇運行的順序是沒有保障的。
8、盡管通常采用隊列形式,但這是沒有保障的。隊列形式是指當一個線程完成“一輪”時,它移到可運行隊列的尾部等待,直到它最終排隊到該隊列的前端為止,它才能被再次選中。事實上,我們把它稱為可運行池而不是一個可運行隊列,目的是幫助認識線程並不都是以某種有保障的順序排列一個隊列的事實。
9、盡管我們沒有無法控制線程調度程序,但可以通過別的方式來影響線程調度的方式。
要理解線程調度的原理,以及線程執行過程,必須理解線程棧模型。
線程棧是指某時內存中線程調度的棧信息,當前調用的方法總是位於棧頂。線程棧的內容是隨著程序的運行動態變化的,因此研究線程棧必須選擇一個運行的時刻(實際上指代碼運行到什麼地方)。
下面通過一個示例性的代碼說明線程(調用)棧的變化過程。
這幅圖描述在代碼執行到兩個不同時刻1、2時候,虛擬機線程調用棧示意圖。
當程序執行到t.start();時候,程序多出一個分支(增加了一個調用棧B),這樣,棧A、棧B並行執行。
從這裡就可以看出方法調用和線程啟動的區別了。
一、線程狀態
線程的狀態轉換是線程控制的基礎。線程狀態總的可分為五大狀態:分別是生、死、可運行、運行、等待/阻塞。用一個圖來描述如下:
1、新狀態:線程對象已經創建,還沒有在其上調用start()方法,不能被線程調度程序調度。
2、可運行狀態:線程有資格運行,但調度程序還沒有把它選定為運行線程時線程所處的狀態。當start()方法調用時,線程首先進入可運行狀態。這種狀態下其隨時可能被線程調度程序調度,獲取CPU執行時間而執行,同時可能有多個線程處於准備狀態,等待被調度執行。線程一旦進入准備狀態就再也不可能回到新建狀態。
3、運行狀態:線程調度程序從可運行池中選擇一個線程作為當前線程時線程所處的狀態。這也是線程進入運行狀態的唯一一種方式。
一旦處於准備狀態的線程獲取了CPU時間,就進入運行狀態,在運行狀態下,線程隨時可能被調度程序調度到准備狀態,線程在執行時,由於需要等待某些必要條件可能會進入等待阻塞狀態。
同時又幾個線程能處於運行狀態取決於硬件,如果是雙核(每核心一線程)CPU,同一時刻可能有兩個線程處於運行狀態。
4、等待/阻塞/睡眠狀態:這是線程有資格運行時它所處的狀態。實際上這個三狀態組合為一種,其共同點是:線程仍舊是活的,但是當前沒有條件運行。換句話說,它是可運行的,但是如果某件事件出現,他可能返回到可運行狀態。
5、死亡態:當線程的run()方法完成時或由於發生異常而終止執行時,就認為它死去。線程一旦死亡,就不能復生。進入死亡的線程可以被當做普通對象來使用,可以調用其方法或變量,但是不能再次啟動, 如果在一個死去的線程上調用start()方法,會拋出java.lang.IllegalThreadStateException異常。
二、線程調度
java自動調度沒有邏輯約束的線程時,其執行順序是沒有保障的,但是可以通過編程調用一些調度線程的方法,來實現一定程度上對線程的調度。
有些調度方法是有保障的,有些只是影響線程進入運行狀態的概率。
對於線程的阻止,考慮一下三個方面,不考慮IO阻塞的情況:
睡眠;
等待;
1、睡眠
Thread.sleep(long millis)和Thread.sleep(long millis, int nanos)
靜態方法強制當前正在執行的線程休眠(暫停執行),以“減慢線程”。當線程睡眠時,它入睡在某個地方,在蘇醒之前不會返回到可運行狀態。當睡眠時間到期,則返回到准備狀態。
這兩個方法不是與某個線程對象相關聯的,其可以出現在任何位置,當執行到該方法時,讓執行此方法的線程進入睡眠狀態。
注意線程醒來將進入准備狀態,並不能保證立即執行,因此指定的時間是線程暫停執行的最小時間。
線程睡眠的原因:線程執行太快,或者需要強制進入下一輪,因為Java規范不保證合理的輪換。
睡眠的實現:調用靜態方法。
try {
Thread.sleep(123);
} catch (InterruptedException e) {
e.printStackTrace();
}
注意:
1、線程睡眠是幫助所有線程獲得運行機會的最好方法。
2、線程睡眠到期自動蘇醒,並返回到可運行狀態,不是運行狀態。sleep()中指定的時間是線程不會運行的最短時間。因此,sleep()方法不能保證該線程睡眠到期後就開始執行。
3、sleep()是靜態方法,只能控制當前正在運行的線程。
//定義實現Runnable接口的類
class MyRunnable1 implements Runnable
{
//重寫run方法,指定該線程執行的代碼
public void run()
{
for(int i=0;i<5;i++)
{
System.out.println("["+i+"] 我是線程1!!!");
//使此線程進入睡眠狀態
try
{
Thread.sleep(100);
}
catch(InterruptedException ie)
{
ie.printStackTrace();
}
}
}
}
//定義另外一個實現Runnable接口的類
class MyRunnable2 implements Runnable
{
//重寫run方法,指定該線程執行的代碼
public void run()
{
for(int i=0;i<5;i++)
{
System.out.println("<"+i+"> 我是線程2!!!");
//使此線程進入睡眠狀態
try
{
Thread.sleep(100);
}
catch(InterruptedException ie)
{
ie.printStackTrace();
}
}
}
}
public class Sample16_3
{
public static void main(String[] args)
{
//創建實現Runnable接口的類
MyRunnable1 mr1=new MyRunnable1();
MyRunnable2 mr2=new MyRunnable2();
//創建線程Thread對象,並指定各自的target
Thread t1=new Thread(mr1);
Thread t2=new Thread(mr2);
//啟動線程t1
t1.start();
//使主線程進入睡眠狀態
try
{
Thread.sleep(5);
}
catch(InterruptedException ie)
{
ie.printStackTrace();
}
//啟動線程t2
t2.start();
}
}
2、線程的優先級和線程讓步yield()
線程總是存在優先級,優先級范圍在1~10之間,值越大優先級越高。
在沒有特別指定的情況下,主線程的優先級為5,對於子線程,其初始優先級與其父線程的優先級相同。
JVM線程調度程序是基於優先級的搶先調度機制。在大多數情況下,當前運行的線程優先級將大於或等於線程池中任何線程的優先級。但這僅僅是大多數情況。
注意:當設計多線程應用程序的時候,一定不要依賴於線程的優先級。因為線程調度優先級操作是沒有保障的,只能把線程優先級作用作為一種提高程序效率的方法,但是要保證程序不依賴這種操作。
當線程池中線程都具有相同的優先級,調度程序的JVM實現自由選擇它喜歡的線程。這時候調度程序的操作有兩種可能:一是選擇一個線程運行,直到它阻塞或者運行完成為止。二是時間分片,為池內的每個線程提供均等的運行機會。
設置線程的優先級:線程默認的優先級是創建它的執行線程的優先級。可以通過setPriority(int newPriority)更改線程的優先級。例如:
Thread t = new MyThread();
t.setPriority(8);
t.start();
線程優先級為1~10之間的正整數,JVM從不會改變一個線程的優先級。然而,1~10之間的值是沒有保證的。一些JVM可能不能識別10個不同的值,而將這些優先級進行每兩個或多個合並,變成少於10個的優先級,則兩個或多個優先級的線程可能被映射為一個優先級。
java中的線程優先級是依賴於本地平台的,在實際運行時會將線程在java中的優先級映射到本地的某個優先級。這樣,如果本地提供的優先級比10個要少,則java中的不同的優先級可能會映射成相同的本地優先級,而具有基本相同的執行概率。
class MyThread1 extends Thread
{
public void run()
{
for(int i=0;i<=49;i++)
{
System.out.print("<xiao"+i+"> ");
}
}
}
class MyThread2 extends Thread
{
public void run()
{
for(int i=0;i<=49;i++)
{
System.out.print("[大"+i+"] ");
}
}
}
public class aa
{
public static void main(String[] args)
{
MyThread1 t1=new MyThread1();
MyThread2 t2=new MyThread2();
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();
}
}
這裡並不是說高優先級的一直占用CPU,只是說高優先級的爭奪的機會大,交替執行還是存在的。
3、線程的讓步,暫停當前正在執行的線程對象,並執行其他線程。
線程讓步包括兩種方式,一:線程只是讓出當前CPU的資源,具體讓給誰不確定。
二:線程將給指定的線程讓步,指定線程沒有完成,其絕不恢復執行。
yield()方法
yield()應該做的是讓當前運行線程回到准備狀態,以允許具有相同優先級的其他線程獲得運行機會。因此,使用yield()的目的是讓相同優先級的線程之間能適當的輪轉執行。但是,實際中無法保證yield()達到讓步目的,因為讓步的線程還有可能被線程調度程序再次選中。
結論:yield()從未導致線程轉到等待/睡眠/阻塞狀態。在大多數情況下,yield()將導致線程從運行狀態轉到准備狀態,但有可能沒有效果。
class MyRunnable implements Runnable
{
private String flagl;
private String flagr;
public MyRunnable(String flagl,String flagr)
{
this.flagl=flagl;
this.flagr=flagr;
}
public void run()
{
for(int i=0;i<30;i++)
{
System.out.print(flagl+i+flagr);
//調用yield方法使當前正在執行的線程讓步
Thread.yield();
}
}
}
public class Sample16_5
{
public static void main(String[] args)
{
//創建兩個實現Runnable接口的類的對象
MyRunnable mr1=new MyRunnable("[","] ");
MyRunnable mr2=new MyRunnable("<","> ");
//創建兩個線程Thread對象,並指定執行的target
Thread t1=new Thread(mr1);
Thread t2=new Thread(mr2);
//啟動線程t1、t2
t1.start();
t2.start();
}
}
這裡線程時交替執行的,但是是沒有保障的,有時交替有時無法做到。
join()方法
當一個線程必須等待另一個線程執行完畢時,才恢復執行時使用join方法。是有保障的。
Thread的非靜態方法join()讓一個線程B“加入”到另外一個線程A的尾部。在A執行完畢之前,B不能工作。
class MyThread extends Thread
{
public void run()
{
for(int i=0;i<30;i++)
{
System.out.print("["+i+"] ");
}
System.out.print("{子線程執行結束} ");
}
}
public class Sample16_6
{
public static void main(String[] args)
{
Thread t=new MyThread();
t.start();
for(int i=0;i<30;i++)
{
if(i==10)
{
//主線程中調用join方法使主線程進行讓步
try
{
System.out.print("{使用了Jion方法} ");
t.join();//等待該線程終止。
}
catch(InterruptedException ie)
{
ie.printStackTrace();
}
}
System.out.print("<"+i+"> ");
}
}
}
另外,join()方法還有帶超時限制的重載版本。 例如t.join(5000);則讓線程等待5000毫秒,如果超過這個時間,則停止等待,變為准備狀態。
線程加入join()對線程棧導致的結果是線程棧發生了變化,當然這些變化都是瞬時的。下面給示意圖:
小結
到目前位置,介紹了線程離開運行狀態的3種方法:
1、調用Thread.sleep():使當前線程睡眠至少多少毫秒(盡管它可能在指定的時間之前被中斷)。
2、調用Thread.yield():不能保障太多事情,盡管通常它會讓當前運行線程回到可運行性狀態,使得有相同優先級的線程有機會執行。
3、調用join()方法:保證當前線程停止執行,直到該線程所加入的線程完成為止。然而,如果它加入的線程沒有存活,則當前線程不需要停止。
除了以上三種方式外,還有下面幾種特殊情況可能使線程離開運行狀態:
1、線程的run()方法完成。
2、在對象上調用wait()方法(不是在線程上調用)。
3、線程不能在對象上獲得鎖定,它正試圖運行該對象的方法代碼。
4、線程調度程序可以決定將當前運行狀態移動到可運行狀態,以便讓另一個線程獲得運行機會,而不需要任何理由。