19.Java學習筆記第十九節——多執行緒(尚矽谷視訊整理)

Hhuaahua發表於2020-10-06


一、執行緒的生命週期

1.理解

JDK中用Thread.State類定義了執行緒的幾種狀態。
要想實現多執行緒,必須在主執行緒中建立新的執行緒物件。Java語言使用Thread類及其子類的物件來表示執行緒,在它的一個完整的生命週期中通常要經歷如下的五種狀態

> 新建: 當一個Thread類或其子類的物件被宣告並建立時,新生的執行緒物件處於新建狀態
> 就緒:處於新建狀態的執行緒被start()後,將進入執行緒佇列等待CPU時間片,此時它已
具備了執行的條件,只是沒分配到CPU資源
> 執行:當就緒的執行緒被排程並獲得CPU資源時,便進入執行狀態, run()方法定義了線
程的操作和功能
> 阻塞:在某種特殊情況下,被人為掛起或執行輸入輸出操作時,讓出 CPU 並臨時中
止自己的執行,進入阻塞狀態
> 死亡:執行緒完成了它的全部工作或執行緒被提前強制性地中止或出現異常導致結束

2.五種狀態之間的關係

在這裡插入圖片描述

二、執行緒的同步

通過同步機制解決執行緒安全問題。

1.理解

  • 多個執行緒執行的不確定性引起執行結果的不穩定。
  • 多個執行緒對賬本的共享,會造成操作的不完整性,會破壞資料。

2.思想

  1. 多執行緒出現了安全問題
  2. 問題的原因:
    當多條語句在操作同一個執行緒共享資料時,一個執行緒對多條語句只執行了一部分,還沒有執行完,另一個執行緒參與進來執行。導致共享資料的錯誤。
  3. 解決辦法:
    對多條操作共享資料的語句,只能讓一個執行緒都執行完,在執行過程中,其他執行緒不可以參與執行。這種情況即使當前執行緒出現了阻塞也不能被改變。

3.處理兩種執行緒安全問題

3.1 同步程式碼塊處理繼承Thread類的執行緒安全問題

Synchronized關鍵字:

形式:

synchronized (同步監視器)
{
	// 需要被同步的程式碼;
}

但是若此時建立了多個Thread物件,所以還是會出現安全問題,此時需要宣告同步監視器是靜態的即可。同理, 也不能用 this 。可以用當前類充當同步監視器(類名.class)。推測出類也是物件。

3.2 同步方法處理繼承Thread類的執行緒安全問題

synchronized還可以放在方法宣告中,表示整個方法為同步方法。

形式:

public static synchronized void show () //宣告為靜態的,//此時同步監視器是當前類
{ 
	// 需要被同步的程式碼;
}
3.3 同步程式碼塊處理實現Runnable的執行緒安全問題

形式:

synchronized (同步監視器)
{
	// 需要被同步的程式碼;
}

1.操作共享資料的程式碼即為同步程式碼。
2.同步監視器,俗稱鎖。任何一個類的物件都可以充當鎖。
          要求多個執行緒共用一把鎖。
          為了方便,不再建立任意一個物件,可以用當前物件 this 。同步監視器的位置直接寫 this 即可。

3.4 同步方法處理實現Runnable的執行緒安全問題

synchronized還可以放在方法宣告中,表示整個方法為同步方法。

形式:

public  synchronized void show () //此時同步監視器是this 
{ 
	// 需要被同步的程式碼;
}

1. 同步方法仍然涉及到同步監視器,只是不需要顯示宣告瞭。
2. 非靜態的同步方法,同步監視器是:this;
靜態的同步方法,監視器是:當前類本身。

4.處理單例模式之懶漢式的執行緒安全問題

class Singleton 
{
	private Singleton() // 1.私有化構造器
	{
	}

	private static Singleton single=null;  //2.宣告當前類的物件,沒有初始化 4.此例項也必須靜態化

	public static synchronized Singleton getInstance() 	// 3.提供公共的靜態的方法,返回當前類的物件
	{
		if(single == null) 
		{
			single = new Singleton();
		}
		return single; 
	}
}

或者:

class Singleton 
{
	private Singleton() // 1.私有化構造器
	{
	}

	private static Singleton single=null;  //2.宣告當前類的物件,沒有初始化 4.此例項也必須靜態化

	public static Singleton getInstance() 	// 3.提供公共的靜態的方法,返回當前類的物件
	{
		synchronized(Bank.class)
		{
			if(single == null) 
			{
				single = new Singleton();
			}
			return single; 
		}
	}
}

這兩種效率都較差。當第一個執行緒建立了物件後,其他執行緒就沒必要再進去了。

class Singleton 
{
	private Singleton() // 1.私有化構造器
	{
	}

	private static Singleton single=null;  //2.宣告當前類的物件,沒有初始化 4.此例項也必須靜態化

	public static Singleton getInstance() 	// 3.提供公共的靜態的方法,返回當前類的物件
	{
	if(single==null)
	{
		synchronized(Bank.class)
		{
			if(single == null) 
			{
				single = new Singleton();
			}
		 }
	  }
	  return single; 
	}
}

5.死鎖問題

5.1 定義
  • 不同的執行緒分別佔用對方需要的同步資源不放棄,都在等待對方放棄自己需要的同步資源,就形成了執行緒的死鎖。
  • 出現死鎖後,不會出現異常,不會出現提示,只是所有的執行緒都處於阻塞狀態,無法繼續。
5.2 解決方法
  • 專門的演算法、原則
  • 儘量減少同步資源的定義
  • 儘量避免巢狀同步

6.處理執行緒安全問題方式三——Lock(鎖)

從JDK 5.0開始,Java提供了更強大的執行緒同步機制——通過顯式定義同步鎖物件來實現同步。同步鎖使用Lock物件充當。
java.util.concurrent.locks.Lock介面是控制多個執行緒對共享資源進行訪問的工具。

class A
{
	private final ReentrantLock lock = new ReenTrantLock();
	public void m()
	{
		lock.lock();
		try
		{
			//保證執行緒安全的程式碼; 
		}
		finally
		{
			lock.unlock(); 
		}
  	} 
}

注意:如果同步程式碼有異常,要將unlock()寫入finally語句塊

7. synchronized 與 Lock 的對比

  1. Lock是顯式鎖(手動開啟和關閉鎖,別忘記關閉鎖),synchronized是隱式鎖,出了作用域自動釋放。
  2. Lock只有程式碼塊鎖,synchronized有程式碼塊鎖和方法鎖。
  3. 使用Lock鎖,JVM將花費較少的時間來排程執行緒,效能更好。並且具有更好的擴充套件性(提供更多的子類)。

優先使用順序:
Lock → 同步程式碼塊(已經進入了方法體,分配了相應資源) → 同步方法(在方法體之外)

三、執行緒的通訊

1. 三種方法

wait():一旦執行該方法,當前程式就進入阻塞狀態,並釋放同步監視器。

notify():喚醒被wait的執行緒中優先順序最高者。(喚醒一個)

notifyAll ():喚醒被wait的所有執行緒。(喚醒所有)

注:這三個方法只有在synchronized方法或synchronized程式碼塊中才能使用,否則會報java.lang.IllegalMonitorStateException異常。

這三個方法的呼叫者必須是同步程式碼塊或同步方法中的同步監視器。

因為這三個方法必須有鎖物件呼叫,而任意物件都可以作為synchronized的同步鎖,因此這三個方法只能在Object類中宣告。

2. wait 和 sleep 方法的異同

相同點:

(1)一旦執行方法,都可以使得當前的執行緒進入阻塞狀態。

不同點:

(1)兩個方法宣告的位置不同:Thread類中宣告sleep(),Object類中宣告wait()。
(2)呼叫的要求不同:sleep()可以在任何需要的場景下呼叫,wait()必須使用在同步程式碼塊或同步方法中。
(3)關於是否釋放同步監視器:如果兩個方法都使用在同步程式碼塊或同步方法中,sleep()不會釋放同步監視器,而wait()會釋放同步監視器。

四、JDK5.0新增執行緒建立方式

1.實現Callable介面

與使用Runnable相比, Callable功能更強大些:
> 相比run()方法,可以有返回值
> 方法可以丟擲異常
> 支援泛型的返回值
> 需要藉助FutureTask類,比如獲取返回結果

藉助FutureTask類就需要用到:

Future介面
> 可以對具體Runnable、Callable任務的執行結果進行取消、查詢是否完成、獲取結果等。
> FutrueTask是Futrue介面的唯一的實現類
> FutureTask 同時實現了Runnable, Future介面。它既可以作為Runnable被執行緒執行,又可以作為Future得到Callable的返回值

過程:
(1)建立一個實現Callable的實現類;
(2)實現call方法,將此執行緒需要執行的操作宣告在call()中;
(3)建立Callable介面實現類的物件;
(4)將此Callable介面實現類的物件傳遞到FutureTask構造器中,建立FutureTask的物件;
(5)將FutureTask的物件作為引數傳遞到Thread類的構造器中,建立Thread的物件,並呼叫start();
(6)獲取Callable中call方法的返回值。(可沒有返回值)

2.使用執行緒池

背景:經常建立和銷燬、使用量特別大的資源,比如併發情況下的執行緒,對效能影響很大。

思路:提前建立好多個執行緒,放入執行緒池中,使用時直接獲取,使用完放回池中。可以避免頻繁建立銷燬、實現重複利用。類似生活中的公共交通工具。

好處:
> 提高響應速度(減少了建立新執行緒的時間)
> 降低資源消耗(重複利用執行緒池中執行緒,不需要每次都建立)
> 便於執行緒管理
> corePoolSize:核心池的大小
> maximumPoolSize:最大執行緒數
> keepAliveTime:執行緒沒有任務時最多保持多長時間後會終止
>

執行緒池相關API:
> JDK 5.0起提供了執行緒池相關API:ExecutorService 和 Executors
> ExecutorService:真正的執行緒池介面。常見子類ThreadPoolExecutor
> void execute(Runnable command) :執行任務/命令,沒有返回值,一般用來執行Runnable
> Future submit(Callable task):執行任務,有返回值,一般又來執行Callable
> void shutdown() :關閉連線池
> Executors:工具類、執行緒池的工廠類,用於建立並返回不同型別的執行緒池
> Executors.newCachedThreadPool():建立一個可根據需要建立新執行緒的執行緒池
> Executors.newFixedThreadPool(n); 建立一個可重用固定執行緒數的執行緒池
> Executors.newSingleThreadExecutor() :建立一個只有一個執行緒的執行緒池
> Executors.newScheduledThreadPool(n):建立一個執行緒池,它可安排在給定延遲後執行命令或者定期地執行。

過程:
(1)提供指定執行緒數量的執行緒池
(2)執行指定的執行緒的操作,需要提供實現Runnable介面或Callable介面實現類的物件
(3)關閉連線池。

相關文章