SE高階(4):多線程(併發)—①創建啓動方式和控制線程方法

進程概念

進程是操作系統運用程序實例,擁有獨立的內存空間和數據,一個進程包含多個子線程,不同進程相互獨立。

進程的特徵:

  • 獨立性:進程是系統中獨立存在的實體,擁有獨立的資源,每個進程都有自己的內存空間。一個進程不能直接訪問另一個進程的內存空間。
  • 動態性:進程是一個正在系統中活動的指令集合,有時間概念,具有生命週期和不同狀態。程序是一個靜態的指令集合,不具備這些狀態。
  • 併發性:多個進程在同一個CPU上併發執行,各進程互不影響。

併發和並行的區別:併發是同時間一個CPU上只執行一條指令集合,多個指令集合輪流搶佔CPU執行權;並行是同一時間在不同的CPU上執行多個指令集合。

簡要理解:併發是單核CPU執行多個任務,但同一時刻只會執行一個任務,任務會爭搶CPU權。並行是多個CPU在同一時刻在執行多個任務,互不干擾。

多線程概念

多線程是對多進程的擴展,使一個進程同一時刻併發執行多個任務。線程也可以叫做輕量級進程,具有進程的部分特徵。線程是進程的執行單元,線程在程序中是獨立、併發的執行流。

可以理解爲:操作系統同時執行多個任務,任務就是進程;進程同時執行多個任務,任務是線程。線程爲進程服務,進程爲操作系統服務。

線程的特徵:

  • 只要進程運行,就必然有一個主線程。當一個進程中有多個線程併發執行,這就是多線程。
  • 線程共享進程的全部資源。
  • 線程是獨立運行的,線程不知道進程中其他線程的存在。
  • 線程是搶佔式的,意味着當前運行的線程下一時刻就會被掛起,另一個線程得到運行。

線程的生命週期:

               

Java多線程的創建和啓動

Java使用Thread類代表線程,所有線程對象都必須是Thread類或者是其子類實例,每個線程都有一個run()方法,該方法作爲線程執行體。

創建線程的三種方式:

  • 繼承Thread類。實例變量不會共享。
  • 實現Runnable接口。實例變量共享。
  • 實現Callable接口,call()允許有返回值。實例變量共享,增加許多方法。
創建線程的三種方式對比:
  • 繼承Thread類,就不能繼承別的父類。但可以使用this訪問當前線程,能直接使用線程對象來啓動線程執行體。但需注意線程類的對象的實例變量不共享,因爲每一個對象都是獨立的。
  • 實現Runnable和Callable差不多,Callable是call()作爲線程執行體,允許有返回值。
  • 多個線程可以共享一個Runnable對象,很適合多個相同線程處理同一份資源的情況。
  • Callable不能直接被線程對象使用,需要一個FutureTask對象對其包裝。

繼承Thread類實例:

public class ThreadDemo {
	//主線程
	public static void main(String[] args) {
		for(int i = 0; i <= 50; i++) {
			System.out.println(Thread.currentThread().getName() + ":" + i);
		}
		//啓動線程
		new A().start();
		new A().start();
	}
}
class A extends Thread{
	int i = 0;
	@Override
	public void run() {
		for(; i <= 50; i++) {
			System.out.println(Thread.currentThread().getName() + ":" + i);
		}
	}
}
  • 實例解析:查看結果會發現,一共有三個線程在執行,而且線程對象中的實例不共享。這是因爲對象具有唯一標識,實例變量和方法都屬於對象本身,不會共享出來。這是A繼承了Thread類,A的對象是能直接代表線程對象,再加上一個主線程,所以一共三個線程在執行。

實現Runnable接口實例:

public class ThreadDemo {
	//主線程
	public static void main(String[] args) {
		A a = new A();
		//把Runnable對象作爲target
		Thread t1 = new Thread(a);
		Thread t2 = new Thread(a);
		//啓動線程
		t1.start();
		t2.start();
	}
}
class A implements Runnable{
	int i = 0;
	@Override
	public void run() {
		for(; i <= 50; i++) {
			System.out.println(Thread.currentThread().getName() + ":" + i);
		}
	}
}
  • 實例解析:查看結果會發現兩個線程共用一個i變量,這是因爲兩個線程使用同一個Runnable對象。如果我們往兩個線程對象中傳入同一個Thread類對象,也能實現該效果,所以這個本質上是執行同一個對象的run()方法。但建議使用Runnable,繼承Thrad類有許多限制。


實現Callable接口實例:

public class ThreadDemo {
	//主線程
	public static void main(String[] args) throws InterruptedException, ExecutionException {
		for(int i=0; i < 20; i++) {
			System.out.println(Thread.currentThread().getName() + i);
		}
		//包裝Callable對象
		FutureTask task = new FutureTask(new Abc());
		//FutureTask對象作爲target
		Thread t1 = new Thread(task,"Call線程");
		//取消Callable任務
		task.cancel(true);
		//啓動線程
		t1.start();
		System.out.println(task.isDone());//Callable任務是否完成	
		System.out.println(task.get());//獲取call()返回值,Excepetion		
	}
}
class Abc implements Callable{
	@Override //帶返回值
	public Object call() throws Exception {
		int sum = 0;
		for(int i=0; i <= 50; i++) {
			sum += i; 
		}
		return sum;
	}
}

  • 實例解析:FutureTask的get()可以獲取call()返回值,但前提是call()執行完成。有兩種方式獲取不了值:第一種是取消FutureTask中的Callable任務,則call()不會被執行。第二種是在線程啓動之前就使用get(),線程沒完成自然返回不了值。可以使用isDone()來判斷Callable任務是否完成,然後再選擇獲取返回值。

控制線程

線程之間是搶佔式的,CPU在下一時刻就可能執行別的線程,充滿不確定性。Java提供一些方法來控制線程的執行,保證線程按規定來執行。

  • sleep():使當前線程睡眠,讓出CPU,進入阻塞狀態,時間到了進入就緒狀態爭搶CPU。
  • yield():直接讓出CPU,進入就緒狀態,然後又開始搶CPU。所以有可能連續執行,因爲又搶到執行權了。
  • join():被加入的線程會馬上執行,其他線程進入等待狀態。
  • setDaemon():將線程設成後臺線程,當前臺線程執行結束,後臺在一段時間後就隨之死亡。JVM的gc回收就是一個典型後臺線程。
  • setPriority():設置線程優先級,通常主線程(main)的優先級最高。建議使用常量作爲設置值。

sleep()方法實例:

public class ThreadDemo {
	//主線程
	public static void main(String[] args) throws InterruptedException {
		Abc abc = new Abc();
		Thread t = new Thread(abc);
		for(int i=0; i < 100; i++) {
			System.out.println(Thread.currentThread().getName() +"->" + i);
			if(i == 30) {
				Thread.sleep(2000);//睡2s
			}
		}
		t.start();
	}
}
class Abc implements Runnable{
	@Override
	public void run() {
		int i = 0;
		while(++i <= 30) {
			System.out.println(Thread.currentThread().getName() + "->" + i);
		}
	}
}
  • 實例解析:使用sleep(2000)讓當前線程(主線程)睡眠2s,睡眠期間爲什麼沒執行別的線程呢?這是因爲要t.start()纔會啓動線程,而該方法是由主線程來調用,所以必須等循環語句結束之後纔會執行到該語句,自然不會出現兩個線程交替執行。如果我們先執行t.start(),就能看到效果了
	//主線程
	public static void main(String[] args) throws InterruptedException {
		Abc abc = new Abc();
		Thread t = new Thread(abc);
		//先啓動線程
		t.start();	
		for(int i=0; i < 50; i++) {
			System.out.println(Thread.currentThread().getName() +"->" + i);
			if(i == 30) 
				Thread.sleep(2000);//睡眠2s
		}
	}


yield()方法實例

public class ThreadDemo {
	//主線程
	public static void main(String[] args) throws InterruptedException {
		Abc abc = new Abc();
		Thread t = new Thread(abc);
		//先啓動線程
		t.start();	
		for(int i=0; i < 50; i++) {
			System.out.println(Thread.currentThread().getName() +"->" + i);
			if(i == 30) 
				Thread.yield(); //放棄CPU,進入就緒
		}
	}
}
class Abc implements Runnable{
	@Override
	public void run() {
		int i = 0;
		while(++i <= 30) {
			Thread.currentThread().setName("Abc線程");
			System.out.println(Thread.currentThread().getName() + "->" + i);
		}
	}
}
圖片顯示yield()的作用。


join()和setDaemon()與setPriority()方法實例

public class ThreadDemo {
	public static void main(String[] args) throws InterruptedException {
		Thread ta = new Thread(new A(),"A線程");
		Thread tb = new Thread(new B(),"B線程");
		for(int i=0; i <= 80; i++) {
			System.out.println(Thread.currentThread().getName() + "->" + i);
			//循環到10,啓動B線程並使用join()
			if(i == 10) {
				tb.start();	
				tb.join(); //主線程必須等B線程執行結束纔開始執行
			}
		}
		//A線程的優先級設置爲10
		ta.setPriority(Thread.MAX_PRIORITY);
		//A線程設爲守護線程
		ta.setDaemon(true);
		ta.start();
				
	}
}
class A implements Runnable{
	@Override
	public void run() {
		int i = 0;
		while(++i <= 150) 
			System.out.println(Thread.currentThread().getName() + "->" + i);			
	}
}
class B implements Runnable{
	@Override
	public void run() {
		int i = 0;
		while(++i <= 100) 
			System.out.println(Thread.currentThread().getName() + "->" + i);					
	}
}
  • 執行流程:從main(主線程)開始執行,當循環值到10時,啓動B線程並join(),此時主線程就會進入阻塞,直到B線程執行結束,處於就緒狀態的線程開始爭搶CPU。但該程序現在只有一個主線程還在運行,執行完整個循環,繼續往下執行,將A線程優先級設爲最高(這個其實很雞肋,瞭解怎麼用就好),然後把A線程設爲主線程的後臺線程,然後啓動該線程。
  • 實例解析:B線程join()結束之後,僅僅只有主線程被執行,是因爲此時只有一個主線程,A線程還沒啓動。當主線程執行到ta.start(),會發現A線程並不會全部執行完,有時候甚至沒執行就死了。這是因爲主線程已經消亡了,A線程一段時間後也隨之消亡。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章