Javaの多線程基礎(1)

一、多線程編程

1.多線程的概念

一般來說,程序只能循序單獨運行一個程序塊,不能同時運行多個程序塊。但Java提供了內置的多線程支持。
多線程是在單個進程中運行多個不同的線程,執行不同的任務,
它允許不同的程序塊在同一個程序中幾乎同時運行,可以提高處理效率、達到多任務的目的。

2.進程與線程

學習多線程需要區分兩個概念:進程線程
進程:

  1. 每一個進程有獨立的一塊內存空間和一組系統資源。進程間,數據和狀態是完全獨立的。
  2. 創建和執行一個線程的系統開銷相對較大
  3. 進程表示程序的一次執行過程,它是系統運行程序的基本單位

線程:

  1. 線程不能獨立存在,它是進程的一部分
  2. 一條線程表示程序中單個按順序的程序流控制
  3. 同一個進程的線程間共享內存空間和系統資源;線程的創建和切換需要的開銷比進程小得多。因此線程也稱輕負荷進程

二、多線程的實現方法

Java默認擁有一個主線程,它是執行main方法的線程。
Java提供了多種實現多線程的方式。其中比較常用且簡單的是繼承Thread類實現Runnable接口

1.Thread類

繼承Thread類是最簡單的多線程實現方法。其步驟如下:

  1. 新建一個類繼承自Thread類;
  2. 在run方法中覆寫想要同時運行的代碼塊;
  3. 在需要運行線程的地方創建一個自建類的實例,即可同時運行寫入的代碼塊。
public class Demo {
	public static void main(String[] args) {
		new TestThread().start();
		int t = 10;
		while(t-- > 0) {
			Thread.sleep(50);			//爲了清晰表示運行結果,使用sleep暫停線程,該方法需要顯示異常處理,此處省略
			System.out.println('1');
		}
	}
}

class TestThread extends Thread{
	@Override
	public void run() {
		int t = 10;
		Thread.sleep(25);
		while(t-- > 0) {
			Thread.sleep(50);
			System.out.println('2');
		}
	}
}
//Output:12121212121212121212

2.Runnable接口

Java只允許單繼承,如果一個類已經是子類,還想使用多線程技術,就不能再使用Thread類,而要實現Runnable接口。

  1. 新建一個實現Runnbale接口的類;
  2. 覆寫run方法;
  3. 在需要運行線程的地方創建一個以Thread對象包裝的自建類實例;
  4. 調用包裝對象的start方法即可運行寫入的代碼塊。
public class Demo {
	public static void main(String[] args) {
		Thread thread = new Thread(new TestThread());	//Thread類允許使用一個實現了runnable接口的實例構造Thread實例
		thread.start();
		int t = 10;
		while(t-- > 0) {
			Thread.sleep(50);
			System.out.println('1');
		}
	}
}

class TestThread implements Runnable{
	@Override
	public void run() {
		int t = 10;
		Thread.sleep(25);
		while(t-- > 0) {
			Thread.sleep(50);
			System.out.println('2');
		}
	}
}
//Output:12121212121212121212

3.兩者的區別

Thread類實際上也是一個實現了Runnable接口的類。但是使用這兩種方式實現多個線程在資源共享上有一些區別:

//使用Thread類創建兩個線程
public class Demo {
	public static void main(String[] args) throws Exception {
		TestThread t1 = new TestThread();
		TestThread t2 = new TestThread();
		t1.start();
		t2.start();
	}
}

class TestThread extends Thread{
	private int v = 0;
	@Override
	public void run() {
		int t = 10;
		while(t-- > 0)
			System.out.print(v++);
	}
}
//Output:01234567801234567899	兩個線程同時運行,擁有獨立的數據v
//使用Runnable接口創建兩個線程
public class Demo {
	public static void main(String[] args) throws Exception {
		TestRunnable tr = new TestRunnable();
		t1 = new Thread(tr);
		t2 = new Thread(tr);
		t1.start();
		Thread.sleep(250);
		t2.start();
	}
}

class TestRunnable implements Runnable{
	private int v = 0;
	@Override
	public void run() {
		int t = 10;
		while(t-- > 0){
			//Thread.sleep(500);
			System.out.print(v++);
		}
	}
}
//Output1:0012345687991010111112131414	兩個線程可能同時訪問到數據v,因此兩個線程讀取的數據相同
//Output2:012345678910111213141516171819  加入註釋掉的兩條語句,將兩個線程的運行錯開,最終可以輸出19,也就是執行了20次v++

由此可以發現,使用同一個Runnable對象構造的兩個線程,互相之間可以共享對象的數據
而繼承Thread類的TestThread類不能共享成員數據,只能創建兩個不同的實例,因此它們之間的實例成員也是獨立的。
並且如果試圖用同一個TestThread對象執行兩次start,會引發異常,原因就是線程的狀態

綜上,Runnable接口相較於Thread類有幾個明顯的優勢:
● 可以使用多個相同代碼的線程處理同一個資源。
● 避免單線程特性帶來的侷限。
● 令代碼與數據相互獨立,增強程序的健壯性。

三、線程的狀態與優先級

1.線程的狀態

一個線程在其生命週期內,有五種可能的狀態,它們的關係如下:
在這裏插入圖片描述
對一個Thread類實例調用getState方法能夠獲取線程的當前狀態,可能得到的結果如下:
● NEW:尚未啓動的線程處於的狀態,對應圖中的創建
● RUNNABLE:正在JVM中執行的線程處於的狀態,對應就緒運行,兩種狀態由CPU調度切換;
● BLOCKED:受阻塞並等待某個監視器鎖的線程處於的狀態,屬於阻塞的一種;
● WAITING:無限期地等待另一個線程來執行某一個特定操作的線程處於的狀態,屬於阻塞的一種;
● TIMED_WAITING:等待另一個線程來執行,取決於指定等待時間的線程處於的狀態,屬於阻塞的一種;
● TERMINATED:已經結束/退出的線程處於的狀態,對應圖中的阻止

任意一個確定的時刻,線程只能處於上面的一個狀態。

線程在不同的狀態,只能調用對應的狀態轉換方法,如圖中所示;例如在程序運行前調用join、sleep等進入阻塞狀態的方法;在運行狀態下調用start方法;在線程結束後調用阻塞方法或start方法等,都會引發特定的illegalThreadStateException異常。

2.線程的優先級

每一個Java線程都有一個優先級,便於操作系統確定線程的調度順序。
線程的優先級是一個整數,取值範圍爲1(Thread.MIN_PRIORITY) ~ 10(Thread.MAX_PRIORITY)。
默認情況下,線程會自動分配一個普通優先級 5 (NORM_PRIORITY)。
一般來說,具有較高優先級的線程對程序而言更爲重要,並應先於較低優先級的線程分配CPU資源;但線程的優先級並不保證線程的執行順序,並且資源的分配依賴於平臺完成。

四、線程操作的方法

1.線程名稱

Thread類的getName方法可以獲取線程的名稱,setName方法可以設置線程的名稱;在創建實例時也可使用對應的構造函數指定線程的名稱;若沒有爲線程指定名稱,系統會爲線程自動分配名稱。
此外,setName和getName方法沒有限制調用時的狀態。可以在線程啓動前設置名稱,也可以啓動後修改名稱。

Thread test = new Thread(new TestRunnable());	
		System.out.println(test.getName());		//自動分配的名稱:Thread-0
		test.setName("test");
		System.out.println(test.getName());		//修改後的名稱:test

2.線程啓動

Thread類的start方法,可以啓動線程;isAlive方法可以判斷線程是否已經啓動,或是否尚未終止;

class TestRunnable implements Runnable{
	@Override
	public void run() {
		for(int i = 0;i < 10;i++) {
			try {
				Thread.sleep(100);		//每次停留100ms,1s後線程結束
			}
			catch (Exception e) {
				e.printStackTrace();
			}
		}
	}
}

public class Demo {
	public static void main(String[] args) throws Exception {
		Thread t = new Thread(new TestRunnable());
		System.out.println(t.isAlive());	//線程啓動前:false
		t.start();
		System.out.println(t.isAlive());	//線程剛啓動:true
		Thread.sleep(500);
		System.out.println(t.isAlive());	//線程啓動後0.5s:true
		System.out.println(t.getState());	//線程狀態:TIMED_WAITING
		Thread.sleep(600);
		System.out.println(t.isAlive());	//線程啓動後1.1s:false
	}
}

在這裏例子中可以看出,處於阻塞狀態的線程,也被認爲是活線程(isAlive返回true)。也就是說isAlive方法只有在線程開始前線程結束後,才返回false。

3.後臺線程

Java程序中,線程分爲前臺線程後臺線程。程序結束的標誌是所有前臺線程結束。也就是說,即使還有後臺線程正在運行,在前臺線程結束時,整個進程就結束了。
默認情況下,新建的線程都是前臺線程。若需要將線程設置爲後臺線程,則需要在線程運行前調用setDaemon方法。

class TestRunnable implements Runnable{
	private int v = 0;
	@Override
	public void run() {
		while(true) {	//無限循環
			try {
				Thread.sleep(100);
				System.out.print(v++);
			}
			catch (Exception e) {
				e.printStackTrace();
			}
		}
	}
}

public class Demo {
	public static void main(String[] args) throws Exception {
		Thread t = new Thread(new TestRunnable());
		t.setDaemon(true);	//若沒有該語句,程序將無限打印自然數
		t.start();
		Thread.sleep(1000);
	}
}
//Output:012345678	由於線程運行start語句的時間消耗,不能打印到9

4.線程插入

Thread類的join方法可以將指定線程插入到當前線程的前面,以合併線程,達到線程的順序執行
調用join方法的當前線程(執行程序塊的線程,而不是調用join的實例線程)將進入WAITING狀態

class TestRunnable implements Runnable{
	@Override
	public void run() {
		for(int i = 0;i < 10;i++) {
			try {
				Thread.sleep(100);
				System.out.print(i);
			}
			catch (Exception e) {
				e.printStackTrace();
			}
		}
	}
}

public class Demo {
	public static void main(String[] args) throws Exception {
		Thread t = new Thread(new TestRunnable());
		t.start();
		t.join();
		for(int i = 0;i < 10;i++) {
			Thread.sleep(100);
			System.out.print(i);
		}
	}
}
//Output:01234567890123456789	在t線程執行完後,再執行main線程

join類有多個重載,可以指定兩個線程合併的時間,在時間到後,兩個線程由再次分離,同時運行。
調用指定時間的join方法的線程將進入TIMED_WAITING狀態

class TestRunnable implements Runnable{
	@Override
	public void run() {
		for(int i = 0;i < 10;i++) {
			try {
				Thread.sleep(100);
				System.out.print(i);
			}
			catch (Exception e) {
				e.printStackTrace();
			}
		}
	}
}

public class Demo {
	public static void main(String[] args) throws Exception {
		Thread t = new Thread(new TestRunnable());
		t.start();
		t.join(500);
		for(int i = 0;i < 10;i++) {
			Thread.sleep(100);
			System.out.print(i);
		}
	}
}
//Output:01234051627384956789	在500ms後,t線程和main線程同時執行

5.線程休眠

Thread類的sleep靜態方法可以讓當前執行的線程休眠一段時間。
調用了sleep方法的線程將進入TIMED_WAITING狀態
該方法會可能會拋出一個檢查性異常InteruptedException,需要顯示處理。

package learning_test;

class TestRunnable implements Runnable{
	@Override
	public void run() {
		try {
			Thread.sleep(10000);
		}
		catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

public class Demo {
	public static void main(String[] args) throws Exception {
		Thread t = new Thread(new TestRunnable());
		t.start();
		System.out.println(t.getState());	//RUNNABLE	線程start方法調用和run方法調用需要時間,因此立即檢查狀態可能得到RUNNABlE
		Thread.sleep(100);
		System.out.println(t.getState());	//TIMED_WAITTING 留給t線程進入run方法的時間,此時t線程由於sleep休眠,線程將進入TIMED_WAITING狀態
	}
}

6.線程中斷

Java中線程中斷有三種方式:
● 對於非無限執行的run方法,在run方法結束後,線程自動中斷;
對於無限執行的run方法:
● 在while循環中使用標識符,當需要線程結束時,修改標識符。

class TestRunnable implements Runnable{
	public boolean exit = false;
	@Override
	public void run() {
		while(!exit){
			<Statement>
		}
	}
}

● 使用stop方法。該方法由於線程不安全,已經被棄用;
● 使用interrupt方法。該方法不同於stop方法那樣立即停止run方法的執行,它僅僅是給線程發送停止標記,通知線程終止;但收到通知後,run方法並不會強制終止。

class TestRunnable implements Runnable {
	@Override
	public void run() {
		for (int i = 0; i < 10; i++) {
			System.out.print(i);
		}
	}
}

public class Demo {
	public static void main(String[] args) throws Exception {
		Thread t = new Thread(new TestRunnable());
		t.start();
		t.interrupt();
	}
}
//Output:0123456789

可以看到,在主線程中給t線程發送interrupt信息後,t線程並沒有強制結束。這雖然避免了stop方法帶來的安全問題,也需要我們單獨處理程序的結束。

class TestRunnable implements Runnable {
	@Override
	public void run() {
		int v = 0;
		while(true) {
			if(Thread.currentThread().isInterrupted())
				break;
			System.out.println(v++);
		}
	}
}

public class Demo {
	public static void main(String[] args) throws Exception {
		Thread t = new Thread(new TestRunnable());
		t.start();
		Thread.sleep(1);	//給t線程執行的時間
		t.interrupt();		//發送停止信息
	}
}
//Output:本次輸出0 ~ 180,具體數字每次運行不定

除了使用break停止線程,interrupt方法還會引發sleep方法的異常。利用異常處理機制可以中斷線程。

package learning_test;

class TestRunnable implements Runnable {
	@Override
	public void run() {
		int v = 0;
		try {
			while (true) {
				Thread.sleep(100);
				System.out.print(v++);
			}
		} catch (InterruptedException e) {
			System.out.print('\t');
			System.out.println(e.getMessage());
		}
	}
}

public class Demo {
	public static void main(String[] args) throws Exception {
		Thread t = new Thread(new TestRunnable());
		t.start();
		Thread.sleep(1000);
		t.interrupt();
	}
}
//Output:012345678	sleep interrupted
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章