Java多線程
1.線程概述
1.線程和程序
- 在一個系統中,每個獨立運作的程式是一個程序;每個程式中有多個順序流,稱為線程;
- 程序的特征:
- 獨立性:每個程序都有着自己獨立的位址空間,互不影響;
- 動态性;
- 并發性:多個程序可以在單個處理器上并發執行,且互不影響;
- 并發性:同一時刻隻能有一條指令執行,但是多個程序指令被快速輪換執行,在宏觀上看起來是一起執行的效果;
- 并行性:同一時刻,多條指令在多個處理器上同時執行;
- Windows和Linux都是搶占式的多任務操作政策;
- 線程又被稱為是輕量級程序,每個線程是獨立運作的;一個程序至少包括一個線程;
2.多線程的優勢
- 線程之間共享記憶體、檔案句柄和狀态資訊;這也是線程執行速度高于程序的原因;
- 多線程的應用:
- javaWeb伺服器響應多個使用者需求;
- 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方法進入阻塞狀态;
- 當發生如下狀态時,系統進入阻塞狀态:
- 線程調用sleep方法主動放棄占用線程資源;
- 線程調用了一個阻塞式IO方法,在該方法傳回之前,該線程被程序阻塞;
- 線程試圖獲得一個同步螢幕;
- 線程在等待某個通知notify;
- 程式調用了線程的suspend方法将該線程挂起(此方法容易導緻死鎖)
- 被阻塞的線程會在解除阻塞後重新進入就緒狀态;
3.線程死亡
- 線程會以如下三種方式結束,并進入死亡狀态:
- run或者call方法執行完成,線程正常結束;
- 線程抛出一個未捕獲的Exception或Error;
- 直接調用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的兩種重載形式
- static void sleep(long millis):讓目前正在執行的線程暫停millis毫秒;
- 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類優先級有關的三個靜态常量:
- MAX_PRIORITY:10
- MIN_PRIORITY:1
- 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,也就是調用該方法的對象;
- 通過使用同步方法可以非常友善的實作線程安全的類,線程安全的類具有如下特征:
- 該類的對象可以被多個線程安全通路;
- 每個線程調用該對象的任意方法都能獲得正确結果;
- 每個線程調用該對象的任意方法後,該對象的狀态依然保持合理狀态;
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關鍵字可以修飾方法、代碼塊,但是不能修飾變量、構造器;
- 線程安全是會降低程式性能的,在程式設計時要遵循以下幾點:
- 不要對線程安全類的所有方法都進行同步,隻對那些會引起共享資源改變的方法進行同步;
- 如果可變類有兩種運作環境:單線程和多線程環境,則應該提供兩個版本,在單線程中提供線程不安全版本提高性能,在多線程中提供線程安全版本保證正确性;
- 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的鎖定資源,才能完成線程,由此互相等待;
- 避免死鎖的方法:
- 盡量避免同一個線程對多個同步螢幕進行鎖定;
- 讓線程具有相同的加鎖順序;
- 使用定時鎖;
6.線程通信
1.傳統的線程通信
- 為什麼線程需要通信:舉個例子,銀行如果要求存錢者在存好錢之後,取錢者立刻取錢,不允許多次連續存錢和連續取錢,這時候就需要線程通信;
- Object類提供三種方法:wait(), notify(), notifyAll()方法,這三個方法必須由同步螢幕對象調用;
- 同步方法中(synchronized)可以直接調用這三個方法;同步代碼塊中需要使用對象調用;
- 方法解釋:
- wait():導緻目前線程等待,直到其他線程調用該同步螢幕的notify方法或notifyAll方法來喚醒該線程;
- notify():喚醒在此同步螢幕上等待的單個線程。如果所有的線程都在這個同步螢幕上等待,則會随機喚醒其中一個線程
- 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提供的三種方法:
- await();
- signal();
- 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提供兩個支援阻塞的方法:
- put(E e):嘗試把E元素放入BQ中;
- 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工廠類來産生線程池:
- newCachedThreadPool():建立一個具有緩存功能的線程池,線程将會被存在這個線程池中;
- newFixedThreadPool(int nThread):建立一個可重用的,具有固定線程數的線程池;
- newSingleThreadExecutor():建立一個隻有單線程的線程池;
- newScheduledThreadPool(int corePoolSize):建立具有指定數的線程池;可以在指定延遲後執行線程任務;
- newSingleThreadScheduledExecutor():建立隻有一個線程的線程池,它可以在指定延遲後執行線程任務;
- 使用線程池來執行線程任務的步驟如下:
- 調用Executors類的靜态工廠方法建立一個ExecutorService對象,該對象代表一個線程池;
- 建立Runnable實作類或Callable實作類的執行個體,作為線程執行任務;
- 調用ExecutorService對象的submit()方法來送出Runnable執行個體或Callable執行個體;
- 當不想送出任何任務時,調用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();
}
}