天天看點

Java多線程程式設計---線程基礎線程與程序線程的狀态轉換建立線程的基本方法常用的線程相關函數一些常見問題

線程與程序

程序

    要解釋線程,就必須明白什麼是程序,就好象要搞清中國曆史,就必須要了解春秋戰國。什麼是程序呢?程序是指運作中的應用程式,每個程序都有自己獨立的位址空間(記憶體空間),比如使用者點選桌面的IE浏覽器,就啟動了一個程序,作業系統就會為該程序配置設定獨立的位址空間。當使用者再次點選桌面的IE浏覽器,又啟動了一個程序,作業系統将為新的程序配置設定新的獨立的位址空間。目前作業系統都支援多程序。

    要點:使用者每啟動一個程序,作業系統就會為該程序配置設定一個獨立的記憶體空間。

線程

    線程是程序中的一個實體,是被系統獨立排程和分派的基本機關,線程自己不擁有系統資源,隻擁有一點在運作中必不可少的資源,但它可與同屬一個程序的其它線程共享程序所擁有的全部資源。一個線程可以建立和撤消另一個線程,同一程序中的多個線程之間可以并發執行。線程有就緒、阻塞和運作三種基本狀态。

    1、線程是輕量級的程序

    2、線程沒有獨立的位址空間(記憶體空間)

    3、線程是由程序建立的(寄生在程序)

    4、一個程序可以擁有多個線程-->這就是我們常說的多線程程式設計

    5、線程有幾種狀态:a、建立狀态(new)、b、就緒狀态(Runnable)、c、運作狀态(Running)、d、阻塞狀态(Blocked)、e、死亡狀态(Dead)。

    目前絕大部分應用程式都會涉及到多并發的問題。隻要應用程式涉及到并發,就離不開多線程程式設計。

程序與線程

    程序與線程的差別:

    1、每個程序都有獨立的代碼和資料空間,程序間的切換會有較大的開銷;

    2、線程可以看成是輕量級的程序,同一程序内的線程共享代碼和資料空間,每個線程有獨立的運作棧和程式計數器,線程切換的開銷小;

    3、多程序:在作業系統中能同時運作多個任務(程式);

    4、多線程:在同一應用程式中有多個順序流同時執行。

    程序和線程的出現,一句話概括:為了充分利用CPU資源。那麼結合計算機我們再來了解一下,程序:我們計算機中運作的各個應用程式,線程:每個應用程式内部的子任務。

    換句話說,程序讓作業系統的并發性成為可能,而線程讓程序的内部并發成為可能。

    但是要注意,一個程序雖然包括多個線程,但是這些線程是共同享有程序占有的資源和位址空間的。程序是作業系統進行資源配置設定的基本機關(各個程序間互不幹擾),而線程是作業系統進行排程的基本機關(線程間的互相切換)。

    程序并發:作業系統已經幫我們處理完成

    線程并發:Java采用的是單線程程式設計模型,即在我們自己的程式中如果沒有主動建立線程的話,隻會建立一個線程,通常稱為主線程。但是要注意,雖然隻有一個線程來執行任務,不代表JVM(在Java中,一個應用程式對應着一個JVM執行個體(也有地方稱為JVM程序))中隻有一個線程,JVM執行個體在建立的時候,同時會建立很多其他的線程(比如垃圾收集器線程)。

線程的狀态轉換

    線程的狀态轉換是線程控制的基礎。線程狀态總的可分為五大狀态:分别是建立狀态、就緒(可運作)狀态、運作狀态、阻塞(等待)狀态、死亡狀态。用一個圖來描述如下:

Java多線程程式設計---線程基礎線程與程式線程的狀态轉換建立線程的基本方法常用的線程相關函數一些常見問題

    1、建立狀态(new):線程對象已經建立,還沒有在其上調用start()方法。

    2、就緒狀态(Runnable):當線程有資格運作,但排程程式還沒有把它標明為運作線程時線程所處的狀态。當start()方法調用時,線程首先進入可運作狀态。線上程運作之後或者從阻塞、等待或睡眠狀态回來後,也傳回到可運作狀态。

    3、運作狀态(Running):線程排程程式從可運作池中選擇一個線程作為目前線程時線程所處的狀态。這也是線程進入運作狀态的唯一一種方式。

    4、等待/阻塞/睡眠狀态(Blocked):這是線程有資格運作時它所處的狀态。實際上這個三狀态組合為一種,其共同點是:線程仍舊是活的,但是目前沒有條件運作。換句話說,它是可運作的,但是如果某件事件出現,他可能傳回到可運作狀态。

    5、死亡态(Dead):當線程的run()方法完成時就認為它死去。這個線程對象也許是活的,但是,它已經不是一個單獨執行的線程。線程一旦死亡,就不能複生。如果在一個死去的線程上調用start()方法,會抛出java.lang.IllegalThreadStateException異常。

建立線程的基本方法

    在java中一個類要當作線程來使用有兩種方法。

    1、繼承Thread類,并重寫run函數

    2、實作Runnable接口,并重寫run函數

    因為java是單繼承的,在某些情況下一個類可能已經繼承了某個父類,這時在用繼承Thread類方法來建立線程顯然不可能,java設計者們提供了另外一個方式建立線程,就是通過實作Runnable接口來建立線程。

1、通過繼承Thread類來實作建立線程執行個體

/**
 * 
 * @Description: 建立線程的第一種方法,繼承Thread類(Thread類本質也是實作了Runnable接口)
 *
 * @author: zxt
 *
 * @time: 2018年4月6日 下午12:17:48
 *
 */
public class TestThread extends Thread {

	public static void main(String[] args) {
		TestThread t1 = new TestThread("張三");
		TestThread t2 = new TestThread("李四");

		t1.start();
		t2.start();
	}

	public TestThread(String name) {
		super(name);
	}

	public void run() {
		for (int i = 0; i < 5; i++) {
			try {
				Thread.sleep(new Random().nextInt(10) * 100);
				
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			
			System.out.println(Thread.currentThread().getName() + ":" + i);
		}
	}
}
           

2、通過實作Runnable接口來建立線程執行個體

/**
 * 
 * @Description: 建立線程的第二種方法,實作Runnable接口,盡量使用這種方法,展現面向對象程式設計的特點
 *
 * @author: zxt
 *
 * @time: 2018年4月6日 下午12:21:47
 *
 */
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();
	}
}

// 實作Runnable接口的類
class DoSomething implements Runnable {
	private String name;

	public DoSomething(String name) {
		this.name = name;
	}

	public void run() {
		for (int i = 0; i < 5; i++) {
			try {
				Thread.sleep(new Random().nextInt(10) * 100);
				
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(name + ": " + i);
		}
	}
}
           

3、多線程執行個體示範

/**
 * 
 * @Description: 多線程舉例
 *
 * @author: zxt
 *
 * @time: 2018年4月6日 下午4:10:17
 *
 */
public class MutliRunnable {

	public static void main(String[] args) {
		
		Pig pig = new Pig(10);
		Bird bird = new Bird(10);
		
		Thread t1 = new Thread(pig);
		Thread t2 = new Thread(bird);
		
		t1.start();
		t2.start();
	}
}

class animal {
	int age = 0;
	int times = 0;

	public animal(int age) {
		this.age = age;
	}
}

// 列印
class Pig extends animal implements Runnable {

	public Pig(int age) {
		super(age);
	}

	public void run() {
		while (true) {
			try {
				Thread.sleep(1000);
				
			} catch (Exception ex) {
				ex.printStackTrace();
			}

			times++;
			System.out.println("我是一個線程,在輸出第" + times + "個 Hello World!");
			if (times == age) {
				break;
			}
		}
	}
}

// 計算
class Bird extends animal implements Runnable {
	int res = 0;

	public Bird(int age) {
		super(age);
	}

	public void run() {
		while (true) {
			try {
				Thread.sleep(1000);
				
			} catch (Exception ex) {
				ex.printStackTrace();
			}

			res += (++times);
			System.out.println("目前結果是:" + res);
			if (times == age) {
				System.out.println("最後結果是:" + res);
				break;
			}
		}
	}
}
           

    不管是通過繼承Thread,還是通過實作Runnable接口建立線程,它們的一個對象隻能啟動(即:start())一次。否則就會有異常抛出。

繼承Thread與實作Runnable的差別

    從java的設計來看,通過繼承Thread類或者實作Runnable接口來建立線程本質上沒有差別,從jdk幫助文檔我們可以看到Thread類本身就實作了Runnable接口,如果一定要說它們有什麼差別,總結幾點:

    1、盡可能使用實作Runnable接口的方式來建立線程

    2、在使用Thread的時候隻需要new一個執行個體出來,調用start()方法即可以啟動一個線程,如:

    Thread test = new Thread();

    test.start();

    3、在使用Runnable的時候需要先new一個實作Runnable接口的執行個體,之後用Thread調用,如:

    Test implements Runnable {}

    Test t = new Test();

    Thread test = new Thread(t);

    tset.start();

    注意:必須調用start()方法才會建立新的線程,如果是調用run()方法,則隻是在主線程裡面進行方法調用,而沒有建立新的線程。

用實作Runnable接口的特點

    1、用實作Runnable接口的方法建立線程對象可以避免java單繼承機制帶來的局限;

    2、用實作Runnable接口的方法,可以實作多個線程共享同一段代碼(資料);是以建議大家如果你的程式有同步邏輯需求,則使用Runnable的方法來建立線程。

常用的線程相關函數

    1、Thread.sleep(longmillis),一定是目前線程調用此方法,目前線程進入阻塞,但不釋放對象鎖,millis後線程自動蘇醒進入可運作狀态。作用:給其它線程執行機會的最佳方式。

    2、Thread.yield(),一定是目前線程調用此方法,目前線程放棄擷取的cpu時間片,由運作狀态變會可運作狀态,讓OS再次選擇線程。作用:讓相同優先級的線程輪流執行,但并不保證一定會輪流執行。實際中無法保證yield()達到讓步目的,因為讓步的線程還有可能被線程排程程式再次選中。Thread.yield()不會導緻阻塞。

    3、t.join()/t.join(longmillis),目前線程裡調用其它線程1的join方法,目前線程阻塞,但不釋放對象鎖,直到線程1執行完畢或者millis時間到,目前線程進入可運作狀态。例如在main()函數中:

    ThreadJoinTest t1 = newThreadJoinTest("小明");

    ThreadJoinTest t2 = newThreadJoinTest("小東");

    t1.start();

    t1.join();

    t2.start();

    其中ThreadJoinTest 繼承了Thread類,程式在main線程中調用t1線程的join方法,則main線程放棄cpu控制權,并傳回t1線程繼續執行直到線程t1執行完畢。是以結果是t1線程執行完後,才到主線程執行,相當于在main線程中同步t1線程,t1執行完了,main線程才有執行的機會。

    4、obj.wait(),目前線程調用對象的wait()方法,目前線程釋放對象鎖,進入等待隊列。依靠notify()/notifyAll()喚醒或者wait(longtimeout)timeout時間到自動喚醒。

    obj.notify()喚醒在此對象螢幕上等待的單個線程,選擇是任意性的。notifyAll()喚醒在此對象螢幕上等待的所有線程。

一些常見問題

    1、線程的名字,一個運作中的線程總是有名字的,名字有兩個來源,一個是虛拟機自己給的名字,一個是你自己的定的名字。在沒有指定線程名字的情況下,虛拟機總會為線程指定名字,并且主線程的名字總是main,非主線程的名字不确定。

    2、線程都可以設定名字,也可以擷取線程的名字,連主線程也不例外。

    3、擷取目前線程的對象的方法是:Thread.currentThread()。

    4、當線程目标run()方法結束時該線程完成。

    5、一旦線程啟動,它就永遠不能再重新啟動。隻有一個新的線程可以被啟動,并且隻能一次。

關于線程排程

    1、對于任何一組啟動的線程來說,排程程式不能保證其執行次序,持續時間也無法保證。Java線程的排程是JVM的一部分,在一個CPU的機器上,實際上一次隻能運作一個線程。一次隻有一個線程棧執行。JVM線程排程程式決定實際運作哪個處于可運作狀态的線程。衆多可運作線程中的某一個會被選中做為目前線程。可運作線程被選擇運作的順序是沒有保障的。    

    2、盡管通常采用隊列形式,但這是沒有保障的。隊列形式是指當一個線程完成“一輪”時,它移到可運作隊列的尾部等待,直到它最終排隊到該隊列的前端為止,它才能被再次選中。事實上,我們把它稱為可運作池而不是一個可運作隊列,目的是幫助認識線程并不都是以某種有保障的順序排列運作的事實。

    3、盡管我們沒有無法控制線程排程程式,但可以通過别的方式來影響線程排程的方式。

繼續閱讀