天天看點

瘋狂Java講義_Chapter16多線程Java多線程

Java多線程

1.線程概述

1.線程和程序

  • 在一個系統中,每個獨立運作的程式是一個程序;每個程式中有多個順序流,稱為線程;
  • 程序的特征:
  1. 獨立性:每個程序都有着自己獨立的位址空間,互不影響;
  2. 動态性;
  3. 并發性:多個程序可以在單個處理器上并發執行,且互不影響;
  • 并發性:同一時刻隻能有一條指令執行,但是多個程序指令被快速輪換執行,在宏觀上看起來是一起執行的效果;
  • 并行性:同一時刻,多條指令在多個處理器上同時執行;
  • Windows和Linux都是搶占式的多任務操作政策;
  • 線程又被稱為是輕量級程序,每個線程是獨立運作的;一個程序至少包括一個線程;

2.多線程的優勢

  • 線程之間共享記憶體、檔案句柄和狀态資訊;這也是線程執行速度高于程序的原因;
  • 多線程的應用:
  1. javaWeb伺服器響應多個使用者需求;
  2. JVM虛拟機的垃圾回收機制;

2.線程的建立和啟動

1.繼承Thread類建立線程類

  • 所有的線程對象都必須是Thread類或其子類的執行個體;每個線程的任務就是執行一段程式流;
  • 建立并啟動線程的步驟如下:
/*
1.定義Thread類的子類,重寫Thread的run方法,run就是線程的執行體;
2.建立Thread子類的執行個體,即建立了線程對象;
3.調用線程對象的start()方法來啟動線程;
 */

//繼承Thread
public class FirstThread extends Thread
{
	private int i;
	// 重寫run
    @Override
	public void run()
	{
		for ( ; i < 100; i++)
		{
			// 直接調用getName方法可以傳回目前線程的名字
			System.out.println(getName() + " " + i);
		}
	}
	public static void main(String[] args)
	{
        //這是主線程
		for (var i = 0; i < 100; i++)
		{
			// 調用Thread的currentThread方法獲得目前線程;
			System.out.println(Thread.currentThread().getName()
				+ " " + i);
			if (i == 20)
			{
				// 建立并啟用第一個線程
				new FirstThread().start();
				// 建立并啟用第二個線程
				new FirstThread().start();
			}
		}
	}
}
           
  • main方法體就是主線程的方法體;

2.實作Runable接口建立線程類

  • 步驟如下
/*
使用Runable接口建立線程
1.定義Runable接口的實作類,并重寫run方法,run是線程的實作體;
2.建立實作類的執行個體,并以此執行個體作為Thread的target來建立Thread對象,該對象為線程對象;
3.調用線程對象的start方法啟動線程;
 */



//實作Runable接口
public class SecondThread implements Runnable
{
	private int i;
	//重寫run方法
	public void run()
	{
		for ( ; i < 100; i++)
		{
			//如果想獲得線程名,隻能通過Thread.currentThread()方法
			System.out.println(Thread.currentThread().getName()
				+ " " + i);
		}
	}

	public static void main(String[] args)
	{
		for (var i = 0; i < 100; i++)
		{
			System.out.println(Thread.currentThread().getName() //主線程
				+ " " + i);
			if (i == 20)
			{
				var st = new SecondThread();     //建立線程執行個體;
				//用執行個體初始化Thread執行個體,并啟動線程;
				new Thread(st, "新線程1").start();
				new Thread(st, "新線程2").start();
			}
		}
	}
}
           

3.使用Callable和Future建立線程(739)(這個方法有點複雜)

  • Thread将run方法作為線程執行體,但是不能将其他方法作為線程執行體;(C#可以)
  • Callable接口是Runable的增強版,提供了一個較強的call方法代替了run方法;

4.建立線程的三種方式對比

  • 由于擴充性原因,一般推薦采用Runable接口實作線程的編寫;

3.線程的生命周期

1.建立和就緒狀态

  • 線程的生命周期中,包括建立New,就緒Ready,運作Running,阻塞Blocked、死亡Dead共5個狀态;
  • new一個線程對象,就進入了線程的建立狀态;
  • 線程對象調用start方法,就進入了就緒狀态;處于這個狀态的線程并沒有開始運作,隻是表示該線程可以運作了,何時開始運作取決于JVM線程排程器的排程;
  • run方法不能直接執行,啟動線程隻能是start方法;
  • 如果希望子線程點選start方法後立即執行,程式可以使用Thread.sleep(1)來讓目前運作的線程睡眠1ms,此時CPU就會去執行另一個已經就緒的線程;

2.運作和阻塞狀态

  • 線程排程平台會定期的讓一些運作的線程進入阻塞狀态,讓其他線程有機會運作;
  • Windows和Linux都是搶占式排程方法;而手機系統采用協作式排程方法,即由目前運作線程自動調用sleep或者yield方法進入阻塞狀态;
  • 當發生如下狀态時,系統進入阻塞狀态:
  1. 線程調用sleep方法主動放棄占用線程資源;
  2. 線程調用了一個阻塞式IO方法,在該方法傳回之前,該線程被程序阻塞;
  3. 線程試圖獲得一個同步螢幕;
  4. 線程在等待某個通知notify;
  5. 程式調用了線程的suspend方法将該線程挂起(此方法容易導緻死鎖)
  • 被阻塞的線程會在解除阻塞後重新進入就緒狀态;

3.線程死亡

  • 線程會以如下三種方式結束,并進入死亡狀态:
  1. run或者call方法執行完成,線程正常結束;
  2. 線程抛出一個未捕獲的Exception或Error;
  3. 直接調用stop方法停止線程;
  • 主線程和子線程是平級關系,主線程死亡不會影響子線程;
  • 為了驗證線程是否死亡,可以調用isAlive()方法來驗證;
  • 已經死亡的線程不能被再次start;

4.控制線程

1.join線程

  • join是讓一個線程等待另一個線程的方法,當某個程式執行流過程中調用其他線程的join方法時,該線程将被阻塞,直到被join方法加入的join線程執行完為止;
/*
join方法有如下三種重載形式
1.join():等待被join的線程執行完成;
2.join(long millis):等待被join的線程的執行時間最長為millis毫秒;如果在給定時間内被join的線程還沒有執行結束,則不再等待;
3.join(long millis, int nanos):等待被join的線程的時間最長為millis+nanos毫微秒;
 */


public class JoinThread extends Thread
{
	// 該構造器用于設定線程名;Thread的有參構造器用于給線程命名
	public JoinThread(String name)
	{
		super(name);
	}
	//重寫run方法
	public void run()
	{
		for (var i = 0; i < 100; i++)
		{
			System.out.println(getName() + "  " + i);
		}
	}

	public static void main(String[] args) throws Exception
	{
		// 啟動線程
		new JoinThread("新線程").start();
		for (var i = 0; i < 100; i++)
		{
			if (i == 20)
			{
				var jt = new JoinThread("被join的線程");
				jt.start();
				// main線程調用了jt線程的join方法,必須等到jt結束才會向下執行;
				jt.join();
			}
			System.out.println(Thread.currentThread().getName()
				+ "  " + i);
		}
	}
}
           

2.背景線程

  • 背景線程Daemon Thread:為其他線程提供服務,又被稱為是守護線程、精靈線程;典型的就是JVM垃圾回收;
  • 如果所有的前台線程都死亡,背景線程會自動死亡;
  • 調用Thread對象的setDaemon(true)方法可以将指定的線程設定為背景線程:
public class DaemonThread extends Thread
{
	public void run()
	{
		for (var i = 0; i < 1000; i++)
		{
			System.out.println(getName() + "  " + i);
		}
	}
	public static void main(String[] args)
	{
		var t = new DaemonThread();
		// 設定為守護程序
		t.setDaemon(true);
		t.start();
		for (var i = 0; i < 10; i++)
		{
			System.out.println(Thread.currentThread().getName()
				+ "  " + i);
		}
	}
}
           
  • Thread類提供了isDaemon方法,用于判定線程是否為背景線程;

3.睡眠線程sleep

  • Thread類的sleep方法可以讓目前正在執行的線程暫停一段時間,并進入阻塞狀态;
  • sleep的兩種重載形式
  1. static void sleep(long millis):讓目前正在執行的線程暫停millis毫秒;
  2. static void sleep(long millis, long nanos)
  • sleep常用于暫停線程的執行,即使記憶體中沒有其他線程執行,sleep的線程也不會執行;
import java.util.*;

public class SleepTest
{
	public static void main(String[] args)
		throws Exception
	{
		for (var i = 0; i < 10; i++)
		{
			System.out.println("時間為 " + new Date());
			// 暫停主線程
			Thread.sleep(1000);
		}
	}
}

           
  • yield方法可以将目前正在執行的線程暫停,但是不會阻塞該線程,它隻是将線程轉入就緒狀态; 

4.改變線程優先級

  • Thread類提供了setPriority(int newPriority)、getPriority()方法來設定和傳回指定線程的優先級;
  • Thread類優先級有關的三個靜态常量:
  1. MAX_PRIORITY:10
  2. MIN_PRIORITY:1
  3. NORM_PRIORITY:5
public class PriorityTest extends Thread
{
	// 給定線程名稱
	public PriorityTest(String name)
	{
		super(name);
	}

	public void run()
	{
		for (var i = 0; i < 50; i++)
		{
			System.out.println(getName() + ",其優先級為:"
				+ getPriority() + ", 循環變量的值為:" + i);
		}
	}
	public static void main(String[] args)
	{
		// 設定主線程優先級為6;
		Thread.currentThread().setPriority(6);
		for (var i = 0; i < 30; i++)
		{
			if (i == 10)
			{
				var low = new PriorityTest("低級");
				low.start();
				System.out.println("建立之初的優先級:"
					+ low.getPriority());
				// 設定low的優先級為最低;
				low.setPriority(Thread.MIN_PRIORITY);
			}
			if (i == 20)
			{
				var high = new PriorityTest("進階");
				high.start();
				System.out.println("建立之初的優先級為:"
					+ high.getPriority());
				// 設定high的優先級
				high.setPriority(Thread.MAX_PRIORITY);
			}
		}
	}
}
           

5.線程同步

1.線程安全問題

  • 定義一個賬戶取錢程式
public class Account
{
    //封裝賬戶編号、賬戶餘額的兩個成員變量
	private String accountNo;
	private double balance;
	public Account(){}
	// 給定構造體,初始化賬戶名和餘額;
	public Account(String accountNo, double balance)
	{
		this.accountNo = accountNo;
		this.balance = balance;
	}

	public void setAccountNo(String accountNo)
	{
		this.accountNo = accountNo;
	}
	public String getAccountNo()
	{
		return this.accountNo;
	}
	public void setBalance(double balance)
	{
		this.balance = balance;
	}
	public double getBalance()
	{
		return this.balance;
	}

	// 重寫hashcode和equals方法
	public int hashCode()
	{
		return accountNo.hashCode();
	}
	public boolean equals(Object obj)
	{
		if (this == obj)
			return true;
		if (obj != null
			&& obj.getClass() == Account.class)
		{
			var target = (Account) obj;
			return target.getAccountNo().equals(accountNo);
		}
		return false;
	}
}
           
  • 提供一個取錢的線程類
public class DrawThread extends Thread
{
	//賬戶
	private Account account;
	// 取錢數量
	private double drawAmount;
	//使用者姓名,賬戶,取錢數
	public DrawThread(String name, Account account,
		double drawAmount)
	{
		super(name);
		this.account = account;
		this.drawAmount = drawAmount;
	}
	// 線程體
	public void run()
	{
		// 餘額大于取錢數
		if (account.getBalance() >= drawAmount)
		{
			// 吐出鈔票
			System.out.println(getName()
				+ "取錢成功" + drawAmount);
			try
			{
				Thread.sleep(1);
			}
			catch (InterruptedException ex)
			{
				ex.printStackTrace();
			}
			account.setBalance(account.getBalance() - drawAmount);
			System.out.println("剩餘餘額為: " + account.getBalance());
		}
		else
		{
			System.out.println(getName() + "取錢失敗餘額不足");
		}
	}
}
           
  • 啟動兩個取錢線程
public class DrawTest
{
	public static void main(String[] args)
	{
		// 建立一個Account執行個體
		var acct = new Account("1234567", 1000);
		//設定兩個取錢線程
		new DrawThread("甲", acct, 800).start();
		new DrawThread("乙", acct, 800).start();
	}
}
           

2.同步代碼塊

  • 當兩個線程通路和修改同一檔案時,就有可能導緻線程執行錯誤;
  • 為了解決這個問題,Java引入了同步螢幕來解決這個問題,使用同步螢幕的通用代碼方法就是同步代碼塊,文法格式為:
//線程開始執行同步代碼塊之前,必須先獲得同步螢幕的鎖定
synchronized (obj)
{
   ...
}
           
  •  同步螢幕的目的就是阻止兩個線程對同一共享資源進行并發通路;
  • 對于上面的取錢程式,可以改為:
public class DrawThread extends Thread
{
	private Account account;
	private double drawAmount;
	public DrawThread(String name, Account account, double drawAmount)
	{
		super(name);
		this.account = account;
		this.drawAmount = drawAmount;
	}
	// 重寫線程體
	public void run()
	{
		synchronized (account)  //使用account作為同步螢幕(因為它是共享資源),任何線程進入下面的同步代碼之前,必須先獲得對account賬戶的鎖定——其他線程無法獲得鎖,也就無法修複它;
                //這種做法符合“加鎖——修改——釋放鎖”的邏輯;
		{
			if (account.getBalance() >= drawAmount)
			{
				System.out.println(getName()
					+ "取錢額度為" + drawAmount);
				try //這步是為了測試有沒有搶占發生
				{
					Thread.sleep(1);
				}
				catch (InterruptedException ex)
				{
					ex.printStackTrace();
				}
				//設定餘額
				account.setBalance(account.getBalance() - drawAmount);
				System.out.println("餘額為: " + account.getBalance());
			}
			else
			{
				System.out.println(getName() + "額度不足");
			}
		}
	}
}
           

3.同步方法

  • 與同步代碼塊對應,Java的多線程安全支援還提供了同步方法,同步方法的同步螢幕是this,也就是調用該方法的對象;
  • 通過使用同步方法可以非常友善的實作線程安全的類,線程安全的類具有如下特征:
  1. 該類的對象可以被多個線程安全通路;
  2. 每個線程調用該對象的任意方法都能獲得正确結果;
  3. 每個線程調用該對象的任意方法後,該對象的狀态依然保持合理狀态;
public class Account
{
	private String accountNo;
	private double balance;
	public Account(){}

	public Account(String accountNo, double balance)
	{
		this.accountNo = accountNo;
		this.balance = balance;
	}

	public void setAccountNo(String accountNo)
	{
		this.accountNo = accountNo;
	}
	public String getAccountNo()
	{
		return this.accountNo;
	}
	public double getBalance()
	{
		return this.balance;
	}

	// 使用synchronized關鍵字修飾方法
	public synchronized void draw(double drawAmount)
	{
		if (balance >= drawAmount)
		{
			System.out.println(Thread.currentThread().getName()
				+ "取錢額度為:" + drawAmount);
			try
			{
				Thread.sleep(1);
			}
			catch (InterruptedException ex)
			{
				ex.printStackTrace();
			}
			balance -= drawAmount;
			System.out.println("餘額為: " + balance);
		}
		else
		{
			System.out.println(Thread.currentThread().getName()
				+ "額度不足");
		}
	}
	
	public int hashCode()
	{
		return accountNo.hashCode();
	}
	public boolean equals(Object obj)
	{
		if (this == obj)
			return true;
		if (obj != null
			&& obj.getClass() == Account.class)
		{
			var target = (Account) obj;
			return target.getAccountNo().equals(accountNo);
		}
		return false;
	}
}
           
  • synchronized關鍵字可以修飾方法、代碼塊,但是不能修飾變量、構造器;
  •  線程安全是會降低程式性能的,在程式設計時要遵循以下幾點:
  1. 不要對線程安全類的所有方法都進行同步,隻對那些會引起共享資源改變的方法進行同步;
  2. 如果可變類有兩種運作環境:單線程和多線程環境,則應該提供兩個版本,在單線程中提供線程不安全版本提高性能,在多線程中提供線程安全版本保證正确性;
  • StringBuilder線程不安全;StringBuffer線程安全;

4.釋放同步螢幕的鎖定(753)

5.同步鎖

  • Java5定義了lock對象作為同步鎖來實作線程安全;
import java.util.concurrent.locks.*;

public class Account
{
	// ReetrantLock可以顯式加鎖和釋放鎖;
	private final ReentrantLock lock = new ReentrantLock();
	private String accountNo;
	private double balance;
	public Account(){}
	
	public Account(String accountNo, double balance)
	{
		this.accountNo = accountNo;
		this.balance = balance;
	}
	
	public void setAccountNo(String accountNo)
	{
		this.accountNo = accountNo;
	}
	public String getAccountNo()
	{
		return this.accountNo;
	}
	public double getBalance()
	{
		return this.balance;
	}
	
	public void draw(double drawAmount)
	{
		// 上鎖
		lock.lock();
		try
		{
			if (balance >= drawAmount)
			{
				System.out.println(Thread.currentThread().getName()
					+ "取錢數量為" + drawAmount);
				try
				{
					Thread.sleep(1);
				}
				catch (InterruptedException ex)
				{
					ex.printStackTrace();
				}
				balance -= drawAmount;
				System.out.println("餘額為: " + balance);
			}
			else
			{
				System.out.println(Thread.currentThread().getName()
					+ "餘額不足");
			}
		}
		finally
		{
			// 在finally塊中解鎖
			lock.unlock();
		}
	}
	
	public int hashCode()
	{
		return accountNo.hashCode();
	}
	public boolean equals(Object obj)
	{
		if (this == obj)
			return true;
		if (obj != null
			&& obj.getClass() == Account.class)
		{
			var target = (Account) obj;
			return target.getAccountNo().equals(accountNo);
		}
		return false;
	}
           

6.死鎖及常用處理政策

  • 死鎖:當兩個線程互相等待對方釋放同步螢幕時就會發生死鎖;例如,A和B分别鎖了兩個資源,但是,A必須通路B的鎖定資源,才能完成線程;而B也必須通路A的鎖定資源,才能完成線程,由此互相等待;
  • 避免死鎖的方法:
  1. 盡量避免同一個線程對多個同步螢幕進行鎖定;
  2. 讓線程具有相同的加鎖順序;
  3. 使用定時鎖;

6.線程通信

1.傳統的線程通信

  • 為什麼線程需要通信:舉個例子,銀行如果要求存錢者在存好錢之後,取錢者立刻取錢,不允許多次連續存錢和連續取錢,這時候就需要線程通信;
  • Object類提供三種方法:wait(), notify(), notifyAll()方法,這三個方法必須由同步螢幕對象調用;
  • 同步方法中(synchronized)可以直接調用這三個方法;同步代碼塊中需要使用對象調用;
  • 方法解釋:
  1. ​​​​​​​wait():導緻目前線程等待,直到其他線程調用該同步螢幕的notify方法或notifyAll方法來喚醒該線程;
  2. notify():喚醒在此同步螢幕上等待的單個線程。如果所有的線程都在這個同步螢幕上等待,則會随機喚醒其中一個線程
  3. notifyAll():喚醒在此同步螢幕上等待的所有線程;
public class Account
{
	private String accountNo;
	private double balance;
	// 狀态判定變量
	private boolean flag = false;
    
	public Account(){}
	public Account(String accountNo, double balance)
	{
		this.accountNo = accountNo;
		this.balance = balance;
	}
	
	public void setAccountNo(String accountNo)
	{
		this.accountNo = accountNo;
	}
	public String getAccountNo()
	{
		return this.accountNo;
	}
	public double getBalance()
	{
		return this.balance;
	}

	//取錢方法
	public synchronized void draw(double drawAmount)
	{
		try
		{
			// 如果flag為true,表示還沒有存錢進賬戶,方法阻塞;
			if (!flag)
			{
				wait();
			}
			else
			{
				// ִ執行取錢
				System.out.println(Thread.currentThread().getName()
					+ "取錢" + drawAmount);
				balance -= drawAmount;
				System.out.println("賬戶餘額為" + balance);
				// 将flag設為false,表示辨別賬戶已有存款;
				flag = false;
				//喚醒其他線程;
				notifyAll();
			}
		}
		catch (InterruptedException ex)
		{
			ex.printStackTrace();
		}
	}
	//存錢方法
	public synchronized void deposit(double depositAmount)
	{
		try
		{
			if (flag)             
			{
				wait();
			}
			else
			{
				//存錢
				System.out.println(Thread.currentThread().getName()
					+ "存錢" + depositAmount);
				balance += depositAmount;
				System.out.println("餘額" + balance);
				// 存入錢,喚醒其他線程
				flag = true;
				notifyAll();
			}
		}
		catch (InterruptedException ex)
		{
			ex.printStackTrace();
		}
	}
	
	public int hashCode()
	{
		return accountNo.hashCode();
	}
	public boolean equals(Object obj)
	{
		if (this == obj)
			return true;
		if (obj != null
			&& obj.getClass() == Account.class)
		{
			var target = (Account) obj;
			return target.getAccountNo().equals(accountNo);
		}
		return false;
	}
}
           
  • 存款者線程:
public class DrawThread extends Thread
{
	// 使用者賬戶
	private Account account;
	// 要取得錢數
	private double drawAmount;
	public DrawThread(String name, Account account,
		double drawAmount)
	{
		super(name);
		this.account = account;
		this.drawAmount = drawAmount;
	}
	//線程體
	public void run()
	{
		for (var i = 0; i < 100; i++)
		{
			account.draw(drawAmount);
		}
	}
}
           
  • 取錢者線程:
public class DepositThread extends Thread
{
	private Account account;
	private double depositAmount;
	public DepositThread(String name, Account account,
		double depositAmount)
	{
		super(name);
		this.account = account;
		this.depositAmount = depositAmount;
	}
	// 線程體
	public void run()
	{
		for (var i = 0; i < 100; i++)
		{
			account.deposit(depositAmount);
		}
	}
}
           
  • 主程式:
public class DrawTest
{
	public static void main(String[] args)
	{
		var acct = new Account("1234567", 0);
		new DrawThread("取錢", acct, 800).start();
		new DepositThread("存錢", acct, 800).start();
		new DepositThread("存錢", acct, 800).start();
		new DepositThread("存錢", acct, 800).start();
	}
}
           

2.使用Condition控制線程通信

  • 當使用lock對象來保證同步時,Java提供了Condition類來保持協調,Condition可以讓那些已經得到Lock對象卻無法繼續執行的線程釋放Lock對象,Condition也可以喚醒其他處于等待的線程;
  • Condition提供的三種方法:
  1. ​​​​​​​await();
  2. signal();
  3. signalAll();
  • 程式示例:
import java.util.concurrent.*;
import java.util.concurrent.locks.*;

public class Account
{
	// 建一個鎖對象
	private final Lock lock = new ReentrantLock();
	// 将一個Condition對象綁定在這個Lock上
	private final Condition cond = lock.newCondition();
	
	private String accountNo;
	private double balance;
	private boolean flag = false;

	public Account(){}
	public Account(String accountNo, double balance)
	{
		this.accountNo = accountNo;
		this.balance = balance;
	}
	
	public void setAccountNo(String accountNo)
	{
		this.accountNo = accountNo;
	}
	public String getAccountNo()
	{
		return this.accountNo;
	}
	public double getBalance()
	{
		return this.balance;
	}

	public void draw(double drawAmount)
	{
		// 鎖方法
		lock.lock();
		try
		{
			if (!flag)
			{
				cond.await();   //類似于wait()方法
			}
			else
			{
				System.out.println(Thread.currentThread().getName()
					+ "取錢:" + drawAmount);
				balance -= drawAmount;
				System.out.println("餘額為" + balance);
				flag = false;
				cond.signalAll();   //類似notify()
			}
		}
		catch (InterruptedException ex)
		{
			ex.printStackTrace();
		}
		// 解鎖
		finally
		{
			lock.unlock();
		}
	}
	public void deposit(double depositAmount)
	{
		lock.lock();
		try
		{
			if (flag)             
			{
				cond.await();
			}
			else
			{
				System.out.println(Thread.currentThread().getName()
					+ "存錢" + depositAmount);
				balance += depositAmount;
				System.out.println("餘額為" + balance);
				flag = true;
				cond.signalAll();
			}
		}
		catch (InterruptedException ex)
		{
			ex.printStackTrace();
		}
		finally
		{
			lock.unlock();
		}
	}
	
	public int hashCode()
	{
		return accountNo.hashCode();
	}
	public boolean equals(Object obj)
	{
		if (this == obj)
			return true;
		if (obj != null
			&& obj.getClass() == Account.class)
		{
			var target = (Account) obj;
			return target.getAccountNo().equals(accountNo);
		}
		return false;
	}
}
           

3.使用阻塞隊列(BlockingQueue)控制線程通信

  • BlockingQueue是Queue的子接口,它是一種線程工具:當生産者線程試圖從BQ中放入資料,如果隊列已滿,則阻塞;當消費者線程試圖從BQ中拿出資料,如果隊列為空,則阻塞;
  • BQ提供兩個支援阻塞的方法:
  1. put(E e):嘗試把E元素放入BQ中;
  2. take():從BQ中取出元素;
  • 同時,BQ繼承了Queue接口,BQ也能使用Queue的方法;(763)
  • BQ的使用例程:
import java.util.concurrent.*;

public class BlockingQueueTest
{
	public static void main(String[] args)
		throws Exception
	{
		// 建立一個容量為2的BQ隊列
		BlockingQueue<String> bq = new ArrayBlockingQueue<>(2);
		bq.put("Java"); // 等同于add("java")
		bq.put("Java"); 
		bq.put("Java"); //超出隊列要求,程式阻塞; 
	}
}
           

7.線程組和未處理的異常(767)

8.線程池

1.使用線程池管理線程

  • 當系統需要在啟動時就建立大量線程時,采用線程池可以提高效率;
  • Java5新增了一個Executors工廠類來産生線程池:
  1. newCachedThreadPool():建立一個具有緩存功能的線程池,線程将會被存在這個線程池中;
  2. newFixedThreadPool(int nThread):建立一個可重用的,具有固定線程數的線程池;
  3. newSingleThreadExecutor():建立一個隻有單線程的線程池;
  4. newScheduledThreadPool(int corePoolSize):建立具有指定數的線程池;可以在指定延遲後執行線程任務;
  5. newSingleThreadScheduledExecutor():建立隻有一個線程的線程池,它可以在指定延遲後執行線程任務;
  • 使用線程池來執行線程任務的步驟如下:
  1. 調用Executors類的靜态工廠方法建立一個ExecutorService對象,該對象代表一個線程池;
  2. 建立Runnable實作類或Callable實作類的執行個體,作為線程執行任務;
  3. 調用ExecutorService對象的submit()方法來送出Runnable執行個體或Callable執行個體;
  4. 當不想送出任何任務時,調用ExecutorService對象的shutdown()方法來關閉線程池;
import java.util.concurrent.*;

public class ThreadPoolTest
{
	public static void main(String[] args)
		throws Exception
	{
		//建立一個線程池,包含6個線程
		ExecutorService pool = Executors.newFixedThreadPool(6);
		// 使用lambda表達式建立Runnable對象
		Runnable target = () -> {
			for (var i = 0; i < 100; i++)
			{
				System.out.println(Thread.currentThread().getName()
					+ "線程名稱" + i);
			}
		};
		// 送出兩個線程
		pool.submit(target);
		pool.submit(target);
		// 關閉線程池
		pool.shutdown();
	}
}
           

2.使用ForkJoinPool利用多CPU

  • Java7提供了ForkJoinPool來支援将一個任務拆分成多個小任務并行計算,再把多個小任務的結果合并成總的計算結果;FJP是ExecutorService的實作類,是一種特殊的線程池;
  • 沒有傳回值的FJP:把一個任務拆成2個任務
import java.util.concurrent.*;

// RecursiveAction代表沒有傳回值的FPJ子類;
class PrintTask extends RecursiveAction
{
	// 每個小任務列印的數字
	private static final int THRESHOLD = 50;
	private int start;
	private int end;
	
	public PrintTask(int start, int end)
	{
		this.start = start;
		this.end = end;
	}
	@Override
    //執行體
	protected void compute()
	{
		if (end - start < THRESHOLD)
		{
			for (var i = start; i < end; i++)
			{
				System.out.println(Thread.currentThread().getName()
					+ "的i值" + i);
			}
		}
		else
		{
			// 将任務分解成兩個小任務
			int middle = (start + end) / 2;
			var left = new PrintTask(start, middle);
			var right = new PrintTask(middle, end);
			// 執行兩個任務
			left.fork();
			right.fork();
		}
	}
}
public class ForkJoinPoolTest
{
	public static void main(String[] args)
		throws Exception
	{
		var pool = new ForkJoinPool();
		// 送出可分解的FJP任務
		pool.submit(new PrintTask(0, 300));
		pool.awaitTermination(2, TimeUnit.SECONDS);
		// 關閉線程池
		pool.shutdown();
	}
}
           
  • 有傳回值的FJP:
import java.util.concurrent.*;
import java.util.*;
//RecursiveTask<T>代表了能夠傳回值的可分解任務
class CalTask extends RecursiveTask<Integer>
{
	private static final int THRESHOLD = 20;
	private int arr[];
	private int start;
	private int end;

	public CalTask(int[] arr, int start, int end)
	{
		this.arr = arr;
		this.start = start;
		this.end = end;
	}
	@Override
	protected Integer compute()
	{
		int sum = 0;
		if (end - start < THRESHOLD)
		{
			for (var i = start; i < end; i++)
			{
				sum += arr[i];
			}
			return sum;
		}
		else
		{
		    //遞歸調用
			int middle = (start + end) / 2;
			var left = new CalTask(arr, start, middle);
			var right = new CalTask(arr, middle, end);

			left.fork();
			right.fork();

			return left.join() + right.join();    // ��
		}
	}
}
public class Sum
{
	public static void main(String[] args)
		throws Exception
	{
		var arr = new int[100];
		var rand = new Random();
		var total = 0;
		// ��ʼ��100������Ԫ��
		for (int i = 0, len = arr.length; i < len; i++)
		{
			int tmp = rand.nextInt(20);
			// ������Ԫ�ظ�ֵ����������Ԫ�ص�ֵ��ӵ�sum�ܺ��С�
			total += (arr[i] = tmp);
		}
		System.out.println(total);
		// ����һ��ͨ�ó�
		ForkJoinPool pool = ForkJoinPool.commonPool();
		// �ύ�ɷֽ��CalTask����
		Future<Integer> future = pool.submit(new CalTask(arr, 0, arr.length));
		System.out.println(future.get());
		// �ر��̳߳�
		pool.shutdown();
	}
}
           

9.線程相關類(773)