線程基礎安全問題、死鎖及線程間通信詳解

1、線程概述

1.1、基本概念

進程:正在運行的程序,負責了這個程序的內存空間分配,代表了內存 中的執行區域。
線程:就是在一個進程中負責一個執行路徑。
多線程:就是在一個進程中多個執行路徑同時執行。
電腦上的程序同時在運行,“多任務”操作系統能同時運行多個進程(程序),但實際是由於CUP分時機制的作用,使每個進程都能循環獲得自己的CUP時間片,由於輪換速度非常快,使得所有程序好象是在“同時”運行一樣。 與其說是快速的切換進程,還不如說是線程進行着CUP的資源爭奪戰。

1.2、多線程的利弊

1.2.1、 多線程好處

1、解決一個進程裏可以同時執行多個任務;
2、提高了資源的利用率(不是效率);

1.2.2.、多線程弊端

1、降低了一個進程中線程的執行頻率;
2、對線程進行管理需要額爲的CUP開銷(多線程的使用會給系統帶來上下文切換的額外負擔);
3、線程死鎖,較長時間的等待或資源爭奪以及死鎖等;
4、線程安全問題;

2、線程的創建方式

2.1、繼承Thread類,方式一
步驟:
1、自定義一個類,並繼承Thread類;
2、重寫父類的run()方法,把自定義線程的任務代碼寫在run()方法裏;
3、創建Thread類的子類對象,並調用start()方法啓動線程;

public class BuildThread01 extends Thread{
	@Override
	public void run() {
		for(int i=0;i<100;i++){
			System.out.println("自定義線程中的方法-->"+i);
		}
	}

	public static void main(String[] args) {
		BuildThread01 thrad = new BuildThread01();
		thrad.start();
		for (int i = 0; i < 100; i++) {
			System.out.println("主線程中的方法-->"+i);
		}
	}
}

注意要點
1、不能試着直接調用Thread子類對象的run()方法來啓動線程,如果直接調用run()方法,相當於被當成一個普通的方法;
2、調用start()啓動線程,線程一旦開啓,就會執行run()方法裏的代碼;
這裏額外說一點,main()方法裏除了自定義線程的那一個外,還有兩個線程,一個是main的主線程,另一個是GC垃圾回收器的線程。所以一個main函數裏至少有兩個線程;
有一個問題:重寫父類run()方法的作用是什麼:
每個線程都有自己的任務代碼,JVM創建主線程的任務代碼就是main()函數裏的代碼,自定義線程的任務代碼就是寫在run()方法中的,所以自定義線程負責了run()方法中的代碼。

2.2、類實現Runnable接口,方式一
步驟:
1、自定義一個類實現Runnable接口;
2、實現Runnable接口的run()方法,把自定義線程的任務代碼寫在run()方法裏;
3、創建Runnable實現類對象;
4、創建Thread類的對象,並且把Runnable實現類的對象作爲實參傳遞。
5、調用Tread類對象的start()方法來啓動線程;

public class BuildThread02 implements Runnable{

	@Override
	public void run() {
		for (int i = 0; i <50; i++) {
			System.out.println(Thread.currentThread().getName()+"--->"+i);
		}
	}
	
	public static void main(String[] args) {
	    //創建Runnable實現類的對象
		BuildThread02 thread02 = new BuildThread02();
		//創建Thread類的對象, 把Runnable實現類對象作爲實參傳遞。
		Thread thread = new Thread(thread02,"狗娃");
		//調用thread對象的start方法開啓線程。
		thread.start();
		for (int i = 0; i <50; i++) {
			System.out.println(Thread.currentThread().getName()+"--->"+i);
		}
	}
}

注意:
1、thread02是Runnable實現類的對象,是不具備start()方法的。

2.3、提問

問題一:Runable實現類的對象是線程對象嗎?
Runable實現類的對象並不是線程對象,它只不過是一個實現了Runable接口的普通對象,不具備start()方法;只有Thread或者thread的子類,纔是線程對象,具備start()方法;
問題二:爲什麼要把Runable實現類的對象作爲實參傳遞給Thread對象?作用是什麼?
這個我們先看一下Thread thread = new Thread(thread02,"狗娃");Thread()構造方法的源碼:

 public Thread(Runnable target, String name) {
        init(null, target, name, 0);
    }

注意:構造方法裏將Runable實現類的對象名稱叫做target;
然後我們再看Thread的run()方法的源碼:

  @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

run()方法裏target正是Runable實現類的對象,也就是:thread02 ,所以target.run();就等於thread02.run();
那麼我們現在來回答這個問題:把Runnable實現類的對象作爲實參傳遞給Thread對象,作用就是把Runnable實現類的對象的run方法作爲了線程的任務代碼去執行了。
問題三:這兩種方法一般使用哪種?
一般使用第二種,實現Runnable接口,因爲java是單繼承,多實現的。

3、線程的一般方法

Thread(String name) 初始化線程的名字
setName(String name) 設置線程對象的名字
getName() 獲取線程對象的名字
sleep(long time) 線程睡眠的指定毫秒數,注意:sleep爲靜態方法,那個線程執行了sleep方法,就是那個線程進入睡眠
currentThread() 返回當前線程對象,注意:currentThread爲靜態方法,那個線程執行了currentThread方法,返回的就是那個線程的對象
getPriority() 返回當前線程的優先級,默認優先級爲5,線程的優先級範圍:[1,10];優先級越大,執行的概率越高
下面我貼一下代碼,作爲簡單使用這幾個方法的例子:

public class ThreadMethods extends Thread{
	
	public ThreadMethods(String threadName) {
		super(threadName);
	}
	@Override
	public void run() {  // 子類拋出的異常,小於等於父類的異常。
		System.out.println("自定義線程的名稱01-》"+this.getName());// this.getName() == Thread.currentThread().getName()
		System.out.println("自定義線程的名稱02-》"+Thread.currentThread().getName());
	/*	for(int i=0 ;i<50;i++){
			System.out.println("自定義線程---》"+i);
		}*/
		try {
			//子類拋出的異常,小於等於父類的異常。父類的run()沒有拋出異常,所以子類的run()只能捕獲異常
			Thread.sleep(1000); 
		} catch (InterruptedException e) {
		}
	}
	public static void main(String[] args) throws InterruptedException {
		ThreadMethods thread = new ThreadMethods("自定義線程");
		Thread.sleep(100);
		thread.setPriority(6); // 設置自定義線程的優先級
		thread.start();
		thread.setName("狗蛋");
		System.out.println("自定義線程的名稱-》"+thread.getName());
		System.out.println("主線程線程的名稱-》"+Thread.currentThread().getName());
		System.out.println("自定義線程的優先級-》"+thread.getPriority());
		System.out.println("主線程線程的優先級-》"+Thread.currentThread().getPriority());
//		for (int i = 0; i < 50; i++) {
//			System.out.println("主方法的線程--->"+i);
//		}
	}
}

注意:
這裏我簡單講一下main()中Thread.sleep(100);的異常處理方法與自定義線程Thread.sleep(100);異常處理方式不一樣,main()中是拋異常,自定義線程中是try-catch
因爲java中:子類拋出的異常,小於等於父類的異常。父類的run()沒有拋出異常,所以子類的run()只能捕獲異常。

4、線程生命週期

這裏我就直接上一張生命週期圖吧
在這裏插入圖片描述

5、線程安全問題

5.1、線程安全問題的由因

嗯,,,我先講一下線程安全問題的出現,看下面代碼:

class SaleTicket extends Thread {
	public SaleTicket(String name) {
		super(name);
	}
	static int sum = 50
	// static Object o = new Object();
	@Override
	public void run() {
		while (true) {
				if (sum <= 0) {
					System.out.println("票售完了...");
					break;
				}
				System.out.println(Thread.currentThread().getName() + "售出了第" + sum + "號票");
				sum--;
		}
	}
}
public class ThreadSafes01 {
	public static void main(String[] args) {
		SaleTicket sale01 = new SaleTicket("一號窗口");
		SaleTicket sale02 = new SaleTicket("二號窗口");
		SaleTicket sale03 = new SaleTicket("三號窗口");
		sale01.start();
		sale02.start();
		sale03.start();
	}

}

先大概講一下代碼,這片代碼是模仿車子窗口售票,創建了三個自定義線程,分別取名爲一、二、三號窗口,上面這個代碼是存在線程安全問題的,我截圖一部分運行的結果圖:
在這裏插入圖片描述
車站售的票是不能重複的,但是圖中運行結果我們可以看到一號跟二號窗口都出售了43號票,這就是線程安全問題了。
簡單講一下出現這一問題的步驟:
1、當一號線程獲取到了CUP的執行權,它走啊走,終於走到了System.out.println(Thread.currentThread().getName() + "售出了第" + sum + "號票");這一塊代碼,打印出了:一號窗口售出了第43號票;但就在這時,當一號線程還來不及執行sum--;它的執行權就被二號線程奪取了,所以這裏要記得票數sum還是等於43
2、現在二號線程拿到了CPU執行權,也是走啊走,在沒有被其他線程奪去執行權的情況下,也是到了System.out.println(Thread.currentThread().getName() + "售出了第" + sum + "號票");這個位置,打印出了:二號窗口售出了第43號票;
基於上面步驟的解析,那麼線程安全問題出現的條件是什麼呢?
1、有兩個或者兩個以上的線程;
2、多個線程共享一個資源;
3、共享資源由多條代碼組成;這條件不難理解,試想一下啊,假如我們的共享資源只有一句System.out.println(“哈哈哈哈”);那執行完不就完了嗎,哪兒那麼多事。

5.2、線程安全問題的解決

sun公司提供了線程同步機制來幫我們解決線程安全問題,其同步機制方法有兩種:同步代碼塊,同步函數;

5.2.1、同步代碼塊

格式:

synchronized (鎖對象) {
			共享資源(代碼)
		}

至於具體的用法,我們以之前的模擬賣車票的代碼爲例,將線程共享的那一片代碼,放進synchronized 的代碼塊裏,如下:

while (true) {
			synchronized ("鎖") {
				if (sum <= 0) {
					System.out.println("票售完了...");
					break;
				}
				System.out.println(Thread.currentThread().getName() + "售出了第" + sum + "號票");
				sum--;
			}
		}

這裏要注意同步代碼塊的範圍,不能連同while (true) {}這一個區域一同放入同步代碼塊中,如果放入的話,結果就是,只要任何一個線程進入同步代碼塊,就會將所有的票出售完,才釋放鎖對象。所以同步代碼塊同步的範圍是需要根據自己實際的業務進行分析的。
再上一個整體的代碼:

class SaleTicket extends Thread {
	public SaleTicket(String name) {
		super(name);
	}
	static int sum = 50;
	@Override
	public void run() {
		while (true) {
			synchronized ("鎖") {
				if (sum <= 0) {
					System.out.println("票售完了...");
					break;
				}
				System.out.println(Thread.currentThread().getName() + "售出了第" + sum + "號票");
				sum--;
			}
		}
	}
}
public class ThreadSafes01 {
	public static void main(String[] args) {
		SaleTicket sale01 = new SaleTicket("一號窗口");
		SaleTicket sale02 = new SaleTicket("二號窗口");
		SaleTicket sale03 = new SaleTicket("三號窗口");
		sale01.start();
		sale02.start();
		sale03.start();
	}
}

同步代碼塊注意事項:
1、鎖對象可以是任意對象;每個對象內部都維護得有狀態碼synchronized()就是根據這一狀態碼進行同步判斷的。
2、鎖對象得是各個線程共享且唯一的,比如用static修飾的對象;我們來做一個假設:假設多線程的鎖對象不是唯一且共享的,那麼就是每個線程自己內部都維護了各自的鎖對象,就相當於每個對象都具有一把鑰匙,那麼每個線程就可以隨意打開synchronized()這把鎖,不用考慮其他線程的感受。所以鎖對象得是各個線程共享且唯一的。
3、在同步代碼塊中調用sleep()方法,並不會釋放鎖對象,而是當線程的睡眠時間結束,並且執行完同步代碼塊中的方法時,纔會釋放鎖對象。
4、只有當真的存在線程安全時,才設置同步代碼塊,不然會影響效率。
我在上面的代碼中,使用的鎖對象是"鎖"這樣的字符串對象,這是最簡單的鎖對象。因爲"鎖"這個已經在字符串常量池中生成,共享且唯一。

5.2.2、同步函數

同步函數:被synchronized修飾的函數,爲同步函數。
我先直接講一下同步函數的注意要點:
1、使用synchronized修飾的方法爲非靜態的方法時,鎖對象爲this對象,當前函數的調用者;
2、使用synchronized修飾的方法爲靜態方法時,鎖對象爲當前函數所屬對象的字節碼文件(class);
3、同步函數同步機制的鎖對象是固定的,不可以隨意更改;
4、同步函數,同步的是整個方法,所以方法裏的所有代碼都會被同步;
再上一片代碼,來講第一條要點----->

	@Override
	public synchronized void run() {
		while (true) {
				if (sum <= 0) {
					System.out.println("票售完了...");
					break;
				}
				System.out.println(Thread.currentThread().getName() + "售出了第" + sum + "號票");
				sum--;
		}
	}

我們使用synchronized修飾非靜態的run()方法,根據第一條的定義:**使用synchronized修飾的方法爲非靜態的方法時,鎖對象爲this對象;**而這裏的this正是每個正在執行的線程對象,也就是說鎖對象並不是唯一且共享的,所以我們的代碼出現的線程安全問題,如下的執行結果:
在這裏插入圖片描述
然後再上一條代碼解釋第二條:

class SaleTicket extends Thread {
    @Override
	public void run() {
		Ticket();
	}
	public static synchronized void Ticket() {
		while (true) {
				if (sum <= 0) {
					System.out.println("票售完了...");
					break;
				}
				System.out.println(Thread.currentThread().getName() + "售出了第" + sum + "號票");
				sum--;
			}
	}
}

上面這塊代碼來跑售票是不規範的,無論怎麼跑都是一個窗口將票全部售完,我借用這個代碼講一下鎖對象是哪一個,看一下定義:使用synchronized修飾的方法爲靜態方法時,鎖對象爲當前函數所屬對象的字節碼文件(class);Ticket()所屬類是SaleTicket,所以**Ticket()的鎖對象就是SaleTicket的字節碼文件;

6、線程死鎖

先直接上圖,然後再解釋一下圖:
在這裏插入圖片描述
圖片的意思是:狗蛋和狗娃,要看電視,只有同時擁有遙控器和電池才能打開電視。狗蛋搶到了電池,狗娃搶到了遙控器。狗蛋要狗娃給他遙控器,而狗娃要狗蛋給他電池,於是兩人就僵持住了,,,
基於這個情況我們用代碼模擬:

class ThreadLock extends Thread{
	
	public ThreadLock(String name){
		super(name);
	}
	
	@Override
	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 DeadLock {
	public static void main(String[] args) {
		ThreadLock thread01 = new ThreadLock("狗娃");
		ThreadLock thread02 = new ThreadLock("狗蛋");
		thread01.start();
		thread02.start();
	}
}

這一片代碼執行後有兩種情況,一種是正常的運行,另一種則是陷入了死鎖;
我們先看正常的結果:
在這裏插入圖片描述
我們再看一種死鎖的結果:
在這裏插入圖片描述
這一結果就是狗娃在等電池,狗蛋在等遙控器的僵持狀態。我們來看一下導致這一狀態的原因:
首先我們創建了兩個線程狗娃和狗蛋,併線程的任務代碼裏有兩個鎖對象:遙控器電池
然後我們來走一下代碼:
1、狗娃先獲得CUP的執行權,走啊走啊,走,走到了System.out.println("狗娃拿到了遙控器了,馬上拿電池...");這個位置,佔用着遙控器這個鎖對象,當他正要準備通過電池這個鎖對象去拿電池的時候,他的執行權被狗蛋搶走了。這時記住了,狗娃任然佔用着遙控器這個鎖對象
2、狗蛋拿到的執行權,也是走啊走啊,走,這時由於狗娃並沒有佔用着電池這一鎖對象,所有狗蛋輕鬆的通過電池這個鎖對象拿到了電池。但是正當狗蛋去拿遙控器的時候,狗娃正佔用着遙控器這個鎖對象,所以狗蛋拿不到遙控器,並佔用着電池這個鎖對象。而狗娃也拿不到電池,因爲狗蛋並未釋放電池這個鎖對象。就有了上面的死鎖結果。

從上面的運行結果我們可以看出,死鎖並不是一定會發生的,而是概率問題;

出現線程死鎖的根本原因

1、存在兩個或者兩個以上的線程
2、存在兩個或者兩個以上的共享資源

出現線程死鎖的解決方法

沒有,儘量避免

7、線程之間的通訊

線程通訊:一個線程完成任務,去通知另外的線程進行其他的任務
線程通訊方法
1、wait():調用wait()方法的線程,將釋放鎖對象進入等待狀態,並且只有當其他線程調用notify()方法才能被喚醒;
2、notify():喚醒線程池中等待線程中的一個,不能喚醒指定的線程,一般先等待先喚醒;
3、notifyAll():喚醒線程池中所以的等待線程;
wait()、notify()的注意事項:
1、wait()和notify()方法是屬於Object對象的;
2、wait()和notify()必須在同步代碼塊或者同步函數中才能使用;
3、wait()和notify()只能由鎖對象調用;
4、調用了notify()方法,即使線程池中沒有等待的線程也沒有關係;

提問:爲什麼wait()和notify()只能由鎖對象調用?
1、一個線程執行了wait()方法,那麼該線程就會進入到一個一鎖對象爲標識符的線程池中等待;
2、一個線程執行了notify()方法,那麼就會喚醒以鎖對象爲標識符的線程池中等待的一個線程;
不同的鎖調用wait()和notify()方法就創建了不同的線程池,notify()只能喚醒同一線程池裏的線程;
在這裏插入圖片描述
我們舉一個官方線程通訊的例子:生產者生產一個產品,消費者就消費一個產品,產品是生產者與消費者共享的;
使用代碼模擬上述需要:

// 產品
class Product{
	String name;
	double price;
	boolean flag = false;//是否有生產的產品,默認爲無
}

// 生產者
class Producer extends Thread{
	Product p; // 維護了產品
	public Producer(Product p,String name) {
		super(name);
		this.p = p;
	}
	
	@Override
	public void run() {
		int i = 0;
		while(true){
			synchronized (p) {
				try {
					if(!p.flag){  // 還未生產
						if(i%2==0){
							p.name = "橘子";
							p.price = 4.5;
						}else {
							p.name = "蘋果";
							p.price = 2.0;
						}
						System.out.println(Thread.currentThread().getName()+"生產了"+p.name+"<--->價格是:"+p.price);
						p.flag = true; // 已經生產產品,將判斷改爲有產品
						p.notify();  //  生產完畢,喚醒消費者去消費
						i++;
					}else {
						p.wait();   // 生產者進入等待狀態,即等待消費者去消費
					}
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
	}
}

// 消費者
class Customers extends Thread{
	
	Product p; // 維護了產品
	
	public Customers(Product p,String name) {
		super(name);
		this.p = p;
	}
	
	@Override
	public void run() {
		while (true) {
			synchronized (p) {
				try {
					if (p.flag) {//判斷是否有生產的產品
						System.out.println(Thread.currentThread().getName()+"消費了"+p.name+"<--->花費了"+p.price);
						p.flag = false;// 已經消費產品,將判斷改爲無產品
						p.notify();   // 喚醒生產者去生產
					}else {
						p.wait();     // 消費者進入等待狀態,即等待生產者去生產
					}
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				
			}
		}
	}
}
public class ThreadCommunication {
	public static void main(String[] args) {
		Product p = new Product();
		Customers customers = new Customers(p,"消費者");
		Producer producer = new Producer(p,"生產者");
		producer.start();
		customers.start();
	}
}

上面代碼一些詳情在備註裏說明:
1、分別有產品、消費者、生產者三個對象,並且消費者、生產者內部維護了產品這一對象;
2、由於產品是消費者和生產者共享的,所以在消費者、生產者的構造方法裏有產品這一形參,在main方法中將產品對象作爲實參傳入到消費者、生產者中;
3、由於產品對象p是消費者、生產者共享的,所以消費者、生產者中的鎖對象統一爲產品P;

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