線程

一.線程概述

1.進程:正在執行的程序作爲一個進程,進程負責了這個程序的內存空間的劃分。

2.疑問1:Windows號稱是多任務的操作系統,那麼Windows是同時運行多個應用程序嗎?

(1) 從宏觀的角度:windows確實是在同時運行多個應用程序。

(2) 從微觀角度:cpu是做了一個快速切換執行的動作,由於速度太快,所以我們感覺不到在切換而已。


3.線程:線程在一個進程中負責了代碼的執行,就是進程中一個執行路徑,(一個進程中的代碼是由線程去執行的)。


如上圖所示,圖上的一鍵優化與垃圾清理在同時運行,在一個進程中同時執行了多個任務。

4.進程與線程的區別:

(1) 進程負責了一個程序的內存空間的分配;

(2) 線程負責了一個程序的執行路徑;

5.多線程:在一個進程中有多個線程同時在執行不同的任務。

6.疑問2:既然線程負責了代碼的執行,那麼我們之前沒有學過線程,爲什麼代碼可以執行呢?

因爲運行任何一個java程序,jvm在運行的時候都會創建一個main線程去執行main方法中所有代碼。

7.疑問3:一個Java應用程序至少有幾個線程?

至少有兩個線程,一個是主線程負責main方法代碼的執行,一個是垃圾回收器線程,負責了回收垃圾。

8.多線程的好處:

(1) 解決了一個進程裏面能同時執行多個任務(執行路徑)的問題。

(2) 提高了資源的利用率,而不是提高了效

9.多線程的弊端:

(1) 增加cpu的負擔。因爲對線程進行管理需要額外的CPU開銷,線程的使用會給系統帶來上下文切換的額外負擔。
(2) 降低了一個進程中線程的執行概率。
(3) 引發了線程安全問題。

(4) 出現了死鎖現象

二.創建線程的方式

1.方式一:繼承Thread類。

(1) 步驟:

①自定義一個類繼承Thread類;

②重寫Thread類的run方法 , 把自定義線程的任務的代碼寫在run方法中;

③創建Thread的子類對象,並且調用start方法開啓線程

(2) 疑問:重寫run方法的目的是什麼?  

因爲每個線程都有自己的任務代碼,jvm創建的主線程的任務代碼就是main方法中的所有代碼, 自定義線程的任務代碼就寫在run方法中,自定義線程負責了run方法中代碼。

(3) 注意: 一個線程一旦開啓,那麼線程就會執行run方法中的代碼,run方法千萬不能直接調用,直接調用run方法就相當調用了一個普通的方法而已並沒有開啓新的線程。

(4) 實例一

public class Demo1 extends Thread {
	@Override  //把自定義線程的任務代碼寫在run方法中。
	public void run() {
		for(int i  = 0 ; i < 3 ; i++){
			System.out.println("自定義線程:"+i);
		}
	}
	public static void main(String[] args) {
		//創建了自定義的線程對象。
		Demo1 d = new Demo1();
		//調用start方法啓動線程
		d.start();
		for(int i  = 0 ; i < 3 ; i++){
			System.out.println("main線程:"+i);
		}
	}
}

運行結果如下圖所示:


(5)實例二

①需求:模擬QQ視頻與聊天可以同時進行。

②分析:首先需要創建兩個線程,視頻和聊天同時進行。

③實例:

class TalkThread extends Thread{
	@Override
	public void run() {
		while(true){
			System.out.println("聊天中....");
		}
	}
}
class VideoThread extends Thread{	
	@Override
	public void run() {
		while(true){
			System.out.println("視頻中....");
		}
	}
}
public class Demo2 {	
	public static void main(String[] args) {
		TalkThread talkThread = new TalkThread();
		talkThread.start();
		VideoThread videoThread = new VideoThread();
		videoThread.start();
	}
}

④運行結果:


2.方式二:實現Runnable接口。

(1) 步驟:

①自定義一個類實現Runnable接口;

②重寫Runnable接口實現類的run方法,把自定義線程的任務代碼定義在run方法上;
③創建Runnable實現類對象;
④創建Thread類的對象,並且把Runnable實現類的對象作爲實際參數,傳遞給Thread類構造方法;

⑤調用Thread對象的start方法開啓一個線程

(2) 疑問1:請問Runnable實現類的對象是線程對象嗎?

Runnable實現類的對象並不是一個線程對象,只不過是實現了Runnable接口的對象而已。只有是Thread或者是Thread的子類纔是線程對象。因爲如果是線程對象,必須得具備一個start()方法可以開啓一個線程。

(3) 疑問2:爲什麼要把Runnable實現類的對象作爲實參傳遞給Thread對象呢?作用是什麼?

因爲自定義的Runnable實現類中的run方法所屬對象是Runnable接口的子類對象,所以要讓線程去執行指定對象的run方法。作用就是把Runnable實現類的對象的run方法作爲了Thread線程的任務代碼去執行了。

(4) 疑問3:如何理解Runnable?

Thread類可以理解爲一個工人,而Runnable的實現類的對象就是這個工人的工作(通過構造方法傳遞),Runnable接口中只有一個run方法,該方法中定義的是會被新線程執行的代碼。當我們把Runnable的子類對象傳遞給Thread的構造方法時,實際上就是讓Thread取得run方法,就是給Thread一項任務。

(5) 實例:

public class Demo3 implements Runnable{
	@Override
	public void run() {
		for(int i = 0 ; i < 3 ; i++){
			System.out.println(Thread.currentThread().getName()+":"+i);
		}
	}
	public static void main(String[] args) {
		//創建Runnable實現類的對象
		Demo3 d = new Demo3();
		//創建Thread類的對象, 把Runnable實現類對象作爲實參傳遞。
		Thread thread = new Thread(d,"張三");  //Thread類使用Target變量記錄了d對象,
		//調用thread對象的start方法開啓線程。
		//不能再通過d.start()來開啓一個線程,因爲start()方法是屬於thread類的,而現在是實現了Runnable接口,這個接口只有一個run()方法,這個類又沒有定義start()方法。
		thread.start();
		for(int i = 0 ; i < 3 ; i++){
			System.out.println(Thread.currentThread().getName()+":"+i);
		}
	} 
}
	

運行結果如下圖所示:

三.線程的生命週期狀態圖

1.CPU的等待資格:CPU在切換過程中,有可能會切換到這裏執行你的代碼,這時候你就具備等待CPU的資格,就好像學生有等待老師問問題的資格一樣,

 2.CPU的執行權:CPU目前正在執行你的代碼,這時候你的線程就具備了CPU的執行權,如果沒有執行到他,那麼就只具備等待的資格。

3.線程的生命週期狀態圖詳解


(1) 一個線程一旦被new出來後,這時候處於創建狀態,創建狀態下的線程什麼都不具備,既不具備等待資格,又不具備執行權,這時候只是new了一個線程對象,還沒有調用start方法;所以這個線程還沒有開啓,沒有開啓的線程是處於創建狀態;

(2) 處於創建狀態的線程一旦調用了start方法,這個線程馬上就進入可運行的狀態,可運行狀態下的線程是具備cpu的等待資格,不具備cpu的執行權;
(3) 處於可運行狀態下的線程一旦搶奪到了cpu的執行權,這時候它就可以進入運行狀態,運行狀態下的線程具備了cpu的執行權,也具備了cpu的等待資格;
(4) 處於運行狀態下的線程完成了任務後,這時候它就進入了死亡狀態,死亡狀態下的線程既不具備等待資格,又不具備執行資格;
(5) 運行狀態下的線程除了可能會完成任務,還有一種可能是被其他線程搶走CPU的執行權,一旦被搶走就會進入可運行狀態。所以運行狀態和可運行狀態是可以互相切換的;
(6) 還有一種狀態叫做臨時阻塞狀態,運行狀態下的線程是有可能進入臨時阻塞狀態的,臨時阻塞狀態下的線程不具備等待資格。

四.線程常用的方法

1.Thread(String name);     

(1) 該方法用於初始化線程的名字。

(2) 線程是有默認的名字的,如下所示:

public class Demo2 extends Thread {
	@Override
	public void run() {
		for (int i = 0; i < 3 ; i++) {
			System.out.println(i);
		}
	}
	public static void main(String[] args) {
		//創建了一個線程對象
		Demo2 d = new Demo2();
		System.out.println("線程的名字:"+d.getName());
		Demo2 d1 = new Demo2();
		System.out.println("線程的名字:"+d1.getName());
	}
}

運行結果如下圖所示:


(3) 實例

public class Demo3 extends Thread{
	public Demo3(String name){
		super(name); //調用了Thread類的一個 參數的構造方法。
	}
	@Override
	public void run() {
		for (int i = 0; i < 3 ; i++) {
			System.out.println(i);
		}
	}
	public static void main(String[] args) {
		//創建了一個線程對象
		Demo3 d= new Demo3("張三");
		System.out.println("線程的名字:"+d.getName());
	}
}

運行結果如下圖所示:


2.start();

(1) 該方法用於啓動一個線程;

(2) 實例:

public class Demo4 extends Thread {
	public Demo4(String name){
		super(name); //調用了Thread類的一個 參數的構造方法。
	}
	@Override
	public void run() {
		for (int i = 0; i < 5 ; i++) {
			System.out.println(this.getName()+":"+i);
		}
	}
	public static void main(String[] args) {
		//創建了一個線程對象
		Demo4 d= new Demo4("張三");
		d.start();
	}
}

(3) 運行結果:


3.getName();

該方法用於返回線程的名字。

4.setName(String name);

(1) 該方法用於設置線程對象名。

(2) 實例:

//也可以通過setName設置線程的名字
public class Demo5 extends Thread {
	public Demo5(String name){
		super(name); //調用了Thread類的一個 參數的構造方法。
	}
	@Override
	public void run() {
		for (int i = 0; i < 5 ; i++) {
			System.out.println(this.getName()+":"+i);
		}
	}
	public static void main(String[] args) {
		//創建了一個線程對象
		Demo5 d= new Demo5("張三");
		d.setName("李四");//設置線程的名字
		d.start();//開啓線程
	}
}

(3) 運行結果:


5.sleep();                 

(1) 該方法返回線程睡眠指定的毫秒數,是一個靜態的方法,哪個線程執行了sleep方法的代碼那麼就是哪個線程睡眠。

(2) 注意:是哪個線程執行了sleep方法的代碼就是哪個線程睡眠,千萬不要理解成哪個線程調用了sleep方法那麼就是哪個線程睡眠。

(3) 實例一

①實例:

public class Demo6 extends Thread {
	public Demo6(String name){
		super(name); //調用了Thread類的一個 參數的構造方法。
	}
	@Override
	public void run() {
		for (int i = 0; i < 5 ; i++) {
			System.out.println(this.getName()+":"+i);
		}
	}
	public static void main(String[] args) throws InterruptedException {
		//創建了一個線程對象
		Demo6 d= new Demo6("張三");
		d.sleep(1000);
		d.setName("李四");//設置線程的名字
		d.start();//開啓線程
	}
}

②運行結果:

③疑問:上述代碼有兩個線程,一個主線程,一個張三的線程,那麼當執行到d.sleep(1000);的時候到底是哪一個線程在睡眠?

答:這句代碼是由主線程來執行的(因爲主方法的所有代碼都是由主線程來執行的),所以不管是哪個對象調用sleep方法都是主線程在睡眠,如果把上面的代碼移動到重寫的run方法中,那麼就是張三的線程在睡眠。

(4) 實例二

①實例:

public class Demo7 extends Thread {
	public Demo7(String name){
		super(name); //調用了Thread類的一個 參數的構造方法。
	}
	@Override
	public void run() {
		for (int i = 0; i < 5 ; i++) {
			System.out.println(this.getName()+":"+i);
			try {
				Thread.sleep(1000); //爲什麼在這裏不能拋出異常,只能捕獲?? Thread類的run方法沒有拋出異常類型,所以子類不能拋出異常類型。
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
	public static void main(String[] args) throws InterruptedException {
		//創建了一個線程對象
		Demo7 d= new Demo7("張三");
		d.setName("李四");//設置線程的名字
		d.start();//開啓線程
	}
}

②運行結果:


6.currentThread();      

(1) 該方法用於返回當前的線程對象,該方法是一個靜態的方法。

(2) 注意:哪個線程執行了currentThread()代碼就返回哪個線程的對象。

(3) 實例一:

public class Demo8 extends Thread{
	public Demo8(String name){
		super(name); //調用了Thread類的一個 參數的構造方法。
	}
	@Override
	public void run() {
		for (int i = 0; i < 5 ; i++) {
			System.out.println(this.getName()+":"+i);
		}
	}
	public static void main(String[] args) throws InterruptedException {
		//創建了一個線程對象
		Demo7 d= new Demo7("張三");
		d.setName("李四");//設置線程的名字
		d.start();//開啓線程
		
		Thread mainThread = Thread.currentThread();//獲取當前的線程對象,現在的當前線程對象主線程的線程對象,因爲這裏的代碼是主線程執行的,
		System.out.println("主線程的名字:"+mainThread.getName());
	}
}

(4) 運行結果:


(5) 實例二:

public class Demo9 extends Thread {
	public Demo9(String name){
		super(name); //調用了Thread類的一個 參數的構造方法。
	}
	@Override
	public void run() {
		System.out.println("this:"+this);
		System.out.println("當前對象:"+Thread.currentThread());//this和當前對象是同一個對象
	}
	public static void main(String[] args) throws InterruptedException {
		//創建了一個線程對象
		Demo9 d= new Demo9("張三");
		d.setName("李四");//設置線程的名字
		d.start();//開啓線程
		
		Thread mainThread = Thread.currentThread();//獲取當前的線程對象,現在的當前線程對象主線程的線程對象,因爲這裏的代碼是主線程執行的,
		System.out.println("主線程的名字:"+mainThread.getName());
	}
}

(6) 運行結果:


7.getPriority();             

(1) 該方法用於返回當前線程對象的優先級,默認線程的優先級是5;

(2) 實例:

public class Demo10 extends Thread{
	public Demo10(String name){
		super(name); //調用了Thread類的一個 參數的構造方法。
	}
	@Override
	public void run() {
		for (int i = 0; i < 3 ; i++) {
			System.out.println(Thread.currentThread().getName()+":"+i);
		}
	}
	public static void main(String[] args) {
		Demo10 d = new Demo10("張三");
		System.out.println("自定義線程的優先級:"+d.getPriority());  //線程的優先級默認是5
		System.out.println("主線程的優先級:"+Thread.currentThread().getPriority());
	}
}

(3) 運行結果:


8.setPriority(int newPriority);

(1) 該方法用於設置線程的優先級,雖然設置了線程的優先級,但是具體的實現取決於底層的操作系統的實現(最大的優先級是10 ,最小的優先級是1,默認是5)。

(2) 設置線程的優先級。優先級的數字越大,優先級越高,優先級的範圍是1~10。

(3) 現在給了張三最大的優先級,張三就一定會先執行完畢嗎?

答:不一定,雖然他的優先級別比主線程要高,只不過是他先執行完的概率大點而已。概率大並不代表一定是這樣。

(4) 實例:

public class Demo11 extends Thread{
	public Demo11(String name){
		super(name); //調用了Thread類的一個 參數的構造方法。
	}
	@Override
	public void run() {
		for (int i = 0; i < 3 ; i++) {
			System.out.println(Thread.currentThread().getName()+":"+i);
		}
	}
	public static void main(String[] args) {
		Demo11 d = new Demo11("張三");
		d.start();
		d.setPriority(10); //設置線程 的優先級。 優先級的數字越大,優先級越高  , 優先級的範圍是1~10
		for (int i = 0; i < 3 ; i++) {
			System.out.println(Thread.currentThread().getName()+":"+i);
		}
	}
}

(5) 運行結果:


五.線程安全問題

1.實例 

(1) 需求:模擬3個窗口同時在售4張票 。

(2) 分析:三個窗口同時在售一輛車的票,也就是需要開啓三個線程。

(3) 實例:

class SaleTicket extends Thread {
	int num = 4;
	 public SaleTicket(String name) {
		super(name);
	}
	@Override
	public void run() {
		while(true){				
				if(num>0){
					System.out.println(Thread.currentThread().getName()+"售出了第"+num+"號票");
					num--;
				}else{
					System.out.println("售罄了..");
					break;
				}
		}
	}	
} 
public class Demo1 {
	public static void main(String[] args) {
		//創建三個線程對象,模擬三個窗口
		SaleTicket thread1 = new SaleTicket("窗口1");
		SaleTicket thread2 = new SaleTicket("窗口2");
		SaleTicket thread3 = new SaleTicket("窗口3");
		//開啓線程售票
		thread1.start();
		thread2.start();
		thread3.start();
	}
}

運行結果如下圖所示:


(4) 問題1:爲什麼4張票被賣出了12次?

①出現的原因:因爲num是非靜態的,非靜態的成員變量數據是在每個對象中都會維護一份數據的,創建三個線程對象就會有三份數據存在內存中。


②解決方案:把num票數共享出來給三個線程對象使用。使用static修飾。

(5) 問題2: 出現了線程安全問題 ?

線程安全問題的解決方案:sun提供了線程同步機制讓我們解決這類問題的。

(6) 改進後的實例:

class SaleTicket implements Runnable{
	int  num = 4; // 票數,是一個非靜態成員變量,每創建一個對象都需要維護一份數據
	@Override
	public void run() {
		while(true){
			synchronized ("鎖") {
				if(num>0){
					System.out.println(Thread.currentThread().getName()+"售出了第"+ num+"號票");
					num--;
				}else{
					System.out.println("售罄了..");
					break;
				}	
			}
		}		
	}
}
public class Demo4 {	
	public static void main(String[] args) {
		//創建了一個Runnable實現類的對象
		SaleTicket saleTicket = new SaleTicket();//現在只創建了一個SaleTicket對象,所以只有一份數據,也就是這份數據共享給三個線程使用,所以num數據不需要共享。
		//創建三個線程對象模擬三個窗口
		Thread thread1 = new Thread(saleTicket,"窗口1");
		Thread thread2 = new Thread(saleTicket,"窗口2");
		Thread thread3 = new Thread(saleTicket,"窗口3");
		//開啓線程售票
		thread1.start();
		thread2.start();
		thread3.start();
	}
}

運行結果如下圖所示:


2.出現線程安全問題的根本原因:

(1) 存在兩個或者兩個以上 的線程對象,而且線程之間共享着一個資源;

(2) 有多個語句操作了共享資源

六.Java線程同步機制

Java提供了兩種實現線程同步機制的方式:

1.方式一:同步代碼塊。

(1) 同步代碼塊的格式:

synchronized(鎖對象){
    需要被同步的代碼...
}

(2) 同步代碼塊要注意事項:

① 任意的一個對象都可以做爲鎖對象。
② 在同步代碼塊中調用了sleep方法並不會釋放鎖對象的。
③ 只有真正存在線程安全問題的時候才使用同步代碼塊,否則會降低效率的。因爲每次執行之前都要判斷鎖對象的狀態是開還是關。
④ 多線程操作的鎖對象必須 是唯一共享 的。否則無效。如果寫成synchronized (new Object())是不能解決線程安全問題的,因爲如果Object沒有使用static修飾的話,在三個線程中的三個對象中,每一個對象都維護了一個Object對象,線程1進去後只會判斷自身的鎖對象,線程2自身也戴了一把鎖,它判斷自己的鎖狀態是開就也會進去

⑤ 如果是synchronized ("鎖")也是可以鎖住的,因爲雙引號引起來的字符串一旦存在了字符串常量池中就不會再創建了,永遠都會使用字符串常量池中的鎖。

(3) 同步代碼塊解決線程安全問題

①實例:

class SaleTicket extends Thread{
	 static int num = 4;//票數  因爲非靜態的成員變量,非靜態的成員變量數據是在每個對象中都會維護一份數據的。因爲創建了三個對象,所以應該聲明爲靜態的
     static	Object o = new Object();
	 public SaleTicket(String name) {
		super(name);
	}
	@Override
	public void run() {
		while(true){
			//同步代碼塊
			synchronized ("o") {				
				if(num>0){
					System.out.println(Thread.currentThread().getName()+"售出了第"+num+"號票");
					try {
						Thread.sleep(100);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					num--;
				}else{
					System.out.println("售罄了..");
					break;
				}
			}
		}
	}
} 
public class Demo4 {
	public static void main(String[] args) {
		//創建三個線程對象,模擬三個窗口
		SaleTicket thread1 = new SaleTicket("窗口1");
		SaleTicket thread2 = new SaleTicket("窗口2");
		SaleTicket thread3 = new SaleTicket("窗口3");
		//開啓線程售票
		thread1.start();
		thread2.start();
		thread3.start();
	}
}

②分析:


假設線程2先搶到了CPU的執行權,線程2先進入到同步代碼塊中,凡是線程到了同步代碼塊中,都要首先看下鎖的狀態,目前鎖的狀態是開着的,就意味着線程2能進去,線程2進去以後,馬上會把鎖的狀態改成關閉,然後執行到輸出語句的時候,假設此時線程1搶到了CPU的執行權,線程1執行到同步代碼塊是,發現鎖的狀態是關着的,此時線程1就需要在外面待着。線程3搶到CPU的執行權時,鎖的狀態依然是關着的,所以也需要在外等待。此時CPU的執行權又交給了線程2,一旦線程2執行到了同步代碼塊結束的位置,鎖的狀態又會恢復爲開的狀態,這時候這三個線程又可以同時搶奪CPU的執行權了。

③運行結果:


2.方式二:同步函數。

(1) 同步函數就是使用synchronized修飾一個函數。

(2) 同步函數要注意的事項:
① 如果是一個非靜態的同步函數的鎖,它的鎖對象是this對象,如果是靜態的同步函數的鎖, 它的鎖對象是當前函數所屬的類的字節碼文件(class對象)。
② 同步函數的鎖對象是固定的,不能由你來指定 的。

(3) 實例:

① 需求:一個銀行賬戶有5000塊,一對夫妻一個拿着存摺,一個拿着卡,開始取錢比賽,每次只能取一千塊,要求不準出現線程安全問題。

② 實例:

class BankThread extends Thread{
	static	int count = 5000;
	public BankThread(String name){
		super(name);
	}
	@Override  //
	public synchronized  void run() {
		while(true){
			synchronized ("鎖") {				
				if(count>0){
					System.out.println(Thread.currentThread().getName()+"取走了1000塊,還剩餘"+(count-1000)+"元");
					count= count - 1000;
				}else{
					System.out.println("取光了...");
					break;
				}
			}
		}
	}	
	//靜態的函數---->函數所屬 的類的字節碼文件對象--->BankThread.class  唯一的。
	public static synchronized  void getMoney(){
		
	}
}
public class Demo1 {
	public static void main(String[] args) {
		//創建兩個線程對象
		BankThread thread1 = new BankThread("老公");
		BankThread thread2 = new BankThread("老婆");
		//調用start方法開啓線程取錢
		thread1.start();
		thread2.start();	
	}
}

③運行結果:


3.推薦使用:同步代碼塊。
原因:
(1) 同步代碼塊的鎖對象可以由我們隨意指定,方便控制。同步函數的鎖對象是固定的,不能由我們來指定。
(2) 同步代碼塊可以很方便控制需要被同步代碼的範圍,同步函數必須是整個函數 的所有代碼都被同步了。

七.死鎖現象

1.java中的同步機制解決了線程安全問題,但是也同時引發死鎖現象。

2.注意:死鎖現象並不是一定會出現的。

3.死鎖現象出現的根本原因:

(1) 存在兩個或者兩個以上的線程。

(2) 存在兩個或者兩個以上的共享資源

4.死鎖現象的解決方案:沒有方案,只能儘量避免發生而已。

5.實例:

class DeadLock extends Thread{
	public DeadLock(String name){
		super(name);
	}
	public void run() {
		if("張三".equals(Thread.currentThread().getName())){
			synchronized ("遙控器") {
				System.out.println("張三拿到了遙控器,準備 去拿電池!!");
				synchronized ("電池") {
					System.out.println("張三拿到了遙控器與電池了,開着空調爽歪歪的吹着...");
				}
			}
		}else if("李四".equals(Thread.currentThread().getName())){
			synchronized ("電池") { 
				System.out.println("李四拿到了電池,準備去拿遙控器!!");
				synchronized ("遙控器") {
					System.out.println("李四拿到了遙控器與電池了,開着空調爽歪歪的吹着...");
				}
			}
		}	
	}
}
public class Demo2 {
	public static void main(String[] args) {
		DeadLock thread1 = new DeadLock("張三");
		DeadLock thread2 = new DeadLock("李四");
		//開啓線程
		thread1.start();
		thread2.start();
	}
}

(1) 分析:


假設張三先搶到CPU的執行權,當他執行到同步代碼塊時,拿到遙控器這個鎖對象後,會把這個鎖關閉,然後就輸出"張三拿到遙控器,準備去拿電池",他剛輸出這句話以後,李四搶到了CPU的執行權,李四一進來就拿到了電池鎖,把電池鎖的狀態改成了關閉,李四就輸出了"李四拿到了電池,準備去拿遙控器",假設這時候李四仍然佔據着CPU的執行權,那麼它就會去拿遙控器鎖,發現遙控器鎖是關着的,此時李四就在外面待着;這時張三搶到了CPU的執行權,張三去拿電池時,發現電池鎖也是關着的。此時,兩個人既進不去又出不了同步代碼塊,這就出現了張三在等李四的電池,李四在等張三的遙控器,這就引發了相互等待資源的情況,這也就是我們所說的死鎖現象。

(2) 運行結果:



由以上運行結果可以知道:死鎖現象並不是一定會出現的。

八.線程通訊

1.當一個線程完成了自己的任務時,要通知另外一個線程去完成另外一個任務。最經典的例子是生產者與消費者的例子。

2.線程通訊常用的方法:

(1) wait():  等待。如果線程執行了wait方法,那麼該線程會進入等待的狀態,等待狀態下的線程必須要被其他線程調用notify方法才能喚醒。一旦執行了wait方法是會釋放鎖對象的。

(2) notify():喚醒。喚醒線程池等待線程其中的一個。

(3) notifyAll() : 喚醒線程池所有等待線程

3.wait與notify方法要注意的事項:

(1) wait方法與notify方法是屬於Object對象 的。因爲鎖對象可以是任意的一個對象,如果把這兩個方法設計在Thread下,那麼就不會是任意的對象了。

(2) wait方法與notify方法必須要在同步代碼塊或者是同步函數中才能 使用。
因爲如果不存在同步代碼塊或者同步函數,就沒有鎖這個概念存在,而這兩個方法一定要由鎖對象調用。

(3) wait方法與notify方法必需要由鎖對象調用。因爲它是要以鎖對象爲標識符建立一個線程池的。不同的鎖調用wait方法是會建立一個不同的線程池的

4.生產者和消費者實例

(1) 生產者和消費者說明:


(2) 實例:

//產品類
class Product {
	String name;  //名字
	double price;  //價格
	boolean flag = false; //產品是否生產完畢的標識,默認情況是沒有生產完成。
}
//生產者
class Producer extends Thread {	
	Product  p ;  	//產品
	public Producer(Product p) {
		this.p  = p ;
	}
	@Override
	public void run() {
		int i = 0 ; 
		while(true){
		 synchronized (p) {
			if(p.flag==false){
				 if(i%2==0){
					 p.name = "蘋果";
					 p.price = 6.5;
				 }else{
					 p.name="香蕉";
					 p.price = 2.0;
				 }
				 System.out.println("生產者生產出了:"+ p.name+" 價格是:"+ p.price);
				 p.flag = true;
				 i++;
				 p.notifyAll(); //喚醒消費者去消費
			}else{
				//已經生產 完畢,等待消費者先去消費
				try {
					p.wait();   //生產者等待
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			} 
		}	
	  }	
	}
}
//消費者
class Customer extends Thread{
	Product p; 
	public  Customer(Product p) {
		this.p = p;
	}
	@Override
	public void run() {
		while(true){
			synchronized (p) {	
				if(p.flag==true){  //產品已經生產完畢
					System.out.println("消費者消費了"+p.name+" 價格:"+ p.price);
					p.flag = false; 
					p.notifyAll(); // 喚醒生產者去生產
				}else{
					//產品還沒有生產,應該 等待生產者先生產。
					try {
						p.wait(); //消費者也等待了...
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		}	
	}
}
public class Demo5 {
	public static void main(String[] args) {
		Product p = new Product();  //產品
		//創建生產對象
		Producer producer = new Producer(p);
		//創建消費者
		Customer customer = new Customer(p);
		//調用start方法開啓線程
		producer.start();
		customer.start();
	}
}

(3) 運行結果:


(4) 生產者與消費者的wait與notify方法說明:


①假設生產者先拿到了CPU的執行權,經過判斷後,拿到了鎖對象,p.flag==false,繼續向下執行,當生產完蘋果並輸出後p.flag=true,執行到p.notify()時,此時沒有線程在等待,沒有線程等待時調用notify方法時不會有任何影響,就好像我們去旅遊,旅館的工作人員每天都會叫早,每個房間敲一次門,如果裏面有人就會被吵醒,如果沒人的話也不會有影響。所以當沒有線程等待時,執行notify方法後不會有任何的影響,此時它繼續拿到CPU的執行權,當執行判斷條件時發現p.flag==true,不滿足條件就會執行wait方法,進行等待,這時候JVM就會建立一個線程池,這個線程池是以鎖對象爲標識符的,這個鎖對象就是p,也就是以p爲標識符建立一個線程池,線程池就類似一個數組一樣,裏面存放着線程,生產者執行wait方法後,這個線程就會進入以鎖對象爲標識符建立的線程池中進行等待,一旦調用了wait方法就會釋放鎖對象。

②這時候消費者拿到了CPU的執行權,拿到鎖後,滿足條件就消費,並且把flag改爲false,當它執行到notify方法時,就會喚醒以鎖對象p爲標識符建立的線程池中等待的線程中的其中一個,線程池中只有生產者一個線程,那就會把生產者喚醒。消費者繼續拿到CPU的執行權,這時候它繼續判斷,發現p.flag==false,執行wait方法,進入線程池中,這樣就可以實現生產一個消費一個的需求。
③如果消費者一開始就拿到了CPU的執行權,它拿到了鎖對象後,發現p.flag==false,還沒有生產,這時候它就會進入線程池中等待。這時候把CPU的執行權交給生產者,生產者開始生產。

九.線程的停止

1.停止一個線程 我們一般都會通過一個變量去控制的。

2.如果需要停止一個處於等待狀態下的線程,那麼我們需要通過變量配合notify方法或者interrupt()來使用。

3.實例

public class Demo6 extends Thread {
	boolean flag = true;
	public Demo6(String name){
		super(name);
	}
	@Override
	public synchronized void run() {
		int i = 0 ;
		while(flag){
			try {
				this.wait(); //張三等待..
			} catch (InterruptedException e) {
				System.out.println("接收到了異常了....");
			}
			System.out.println(Thread.currentThread().getName()+":"+i);
			i++;
		}
	}
	public static void main(String[] args) {
		Demo6 d = new Demo6("張三");
		d.setPriority(10);
		d.start();
		for(int i = 0 ; i<5 ; i++){
			System.out.println(Thread.currentThread().getName()+":"+i);
			//當主線程的i是3的時候停止張三線程。
			if(i==3){
				d.flag = false;
				d.interrupt(); //把線程的等待狀態強制清除,被清除狀態的線程會接收到一個InterruptedException。 
			}
		}
	}
}

運行結果如下圖所示:


十.守護線程

1.守護線程也稱爲後臺線程,在一個進程中如果只剩下 了守護線程,那麼守護線程也會死亡。

2.一個線程默認都不是守護線程。

3.實例:

(1) 需求:模擬下載更新包。

(2) 實例:

public class Demo7 extends Thread {
	public Demo7(String name){
		super(name);
	}
	@Override
	public void run() {
		for(int i = 1 ; i<=100 ; i++){
			System.out.println("更新包目前下載"+i+"%");
			if(i==100){
				System.out.println("更新包下載完畢,準備安裝..");
			}
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
	public static void main(String[] args) {
		 Demo7 d = new Demo7("後臺線程");
		 //d.setDaemon(true); //setDaemon() 設置線程是否爲守護線程,true爲守護線程, false爲非守護線程。
		// System.out.println("是守護線程嗎?"+ d.isDaemon()); //判斷線程是否爲守護線程。
		 d.start();
	}
}

(3) 運行結果如下圖所示:


注意:默認情況下的線程不是守護線程。

4.守護線程實例

(1) 分析:如果是守護線程,那麼當主線程結束時,守護線程也會結束,守護線程會停止下載。

(2) 實例:

public class Demo7 extends Thread {
	public Demo7(String name){
		super(name);
	}
	@Override
	public void run() {
		for(int i = 1 ; i<=100 ; i++){
			System.out.println("更新包目前下載"+i+"%");
			if(i==100){
				System.out.println("更新包下載完畢,準備安裝..");
			}
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
	public static void main(String[] args) {
		 Demo7 d = new Demo7("後臺線程");
		 d.setDaemon(true); //setDaemon() 設置線程是否爲守護線程,true爲守護線程, false爲非守護線程。
		// System.out.println("是守護線程嗎?"+ d.isDaemon()); //判斷線程是否爲守護線程。
		 d.start();
		 for(int i = 1 ; i<=3 ; i++){
			 System.out.println(Thread.currentThread().getName()+":"+i);
		 }
	}
}

(3) 運行結果:


十一.Join方法

(1) 加入,一個線程如果執行join語句,那麼就有新的線程加入,執行該語句的線程必須要讓步給新加入的線程先完成任務,然後才能繼續執行。

(2) 實例:

//老媽
class  Mon extends Thread{
	public void run() {
		System.out.println("媽媽洗菜");
		System.out.println("媽媽切菜");
		System.out.println("媽媽準備炒菜,發現沒有醬油了..");
		//叫兒子去打醬油
		Son s= new Son();
		s.start();
		try {
			s.join();  //加入。 一個線程如果執行join語句,那麼就有新的線程加入,執行該語句的線程必須要讓步給新加入的線程先完成任務,然後才能繼續執行。
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("媽媽繼續炒菜");
		System.out.println("全家一起吃飯..");		
	}
} 
class Son extends Thread{
	@Override
	public void run() {
		System.out.println("兒子下樓..");
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("兒子一直往前走");
		System.out.println("兒子打完醬油了");
		System.out.println("上樓,把醬油給老媽");
	}
}
public class Demo8 {
	public static void main(String[] args) {
		Mon m = new Mon();
		m.start();
	}
}

(3) 運行結果:




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