執行緒池和執行緒相關類
執行緒池概述
系統啟用一個新執行緒的成本是比較高的,因為它涉及與作業系統互動。在這種情形下,使用執行緒池可以很好的提高效能。執行緒池在系統啟動時即建立大量空閒的執行緒,程式將一個Runnable物件或Callable物件傳給執行緒池,執行緒池會啟動一個執行緒來執行它們的run()或call方法,當方法執行結束後,執行緒並不會死亡,而是再次返回到執行緒池成為空閒狀態,等待執行下一個Runnable物件的方法。除此之外,執行緒池可以有效地控制系統中併發執行緒的數量。
Java8改進的執行緒池
在Java5之前,開發者需要手動實現自己的執行緒池,從Java5開始,Java內建支援執行緒池。提供了一個Executors工廠類來產生執行緒池,該工廠類包含如下幾個靜態工廠方法來建立執行緒池。
newCachedThreadPool()
:建立一個具有快取功能的執行緒池,系統根據需要建立執行緒,這些執行緒將會被快取線上程池中。newFixedThreadExecutor(int nThreads)
:建立一個可重用的、具有固定執行緒數的執行緒池。newSingleThreadExecutor()
:建立一個只有單執行緒的執行緒池;newScheduledThreadPool(int corePoolSize)
:建立具有指定執行緒數的執行緒池,它可以在指定延遲後執行執行緒任務。newSingleThreadScheduledPool()
:建立只有一個執行緒的執行緒池,它可以在指定延遲後執行執行緒任務。ExcutorService newWorkStealingPool(int parallelism)
:建立持有足夠的執行緒的執行緒池來支援給定的並行級別,該方法還會使用多個佇列來減少競爭。ExcutorService newWorkStealingPool()
:前一個版本的簡化版,如果當前機器有4個CPU,則目標並行級別被設定為4.
前三個方法返回ExecutorService物件,中間兩個方法返回它的子類;ScheduledExecutorService執行緒池,後來兩個方法充分利用多CPU並行的能力;
ExecutorService代表儘快執行執行緒的執行緒池,它提供瞭如下三個方法:
Future<?> submit(Runnable task)
:將一個Runnable物件提交給指定的執行緒池,執行緒池將在有空閒執行緒時執行任務。其中Future代表Runnable任務的返回值,但run方法沒有返回值,所以返回null,但是可以呼叫Future的isDone()、isCancelled()方法來獲得Runnable物件的執行狀態。<T>Future<T>submit(Runnable task,T result)
:將一個Runnable物件提交給指定的執行緒池,執行緒池將在有空閒執行緒時執行任務。其中result顯式指定執行緒執行結束後的返回值,所以Future物件將在方法執行完之後返回null。<T>Future<T> submit(Callable<T> task)
:將一個Callable物件提交給指定的執行緒池,執行緒池將在有空閒執行緒時執行任務。其中Future代表Callable任務的返回值.
ScheduledExecutorService代表可在指定延遲後或週期性地執行執行緒任務的執行緒池。
ScheduledFuture<V>schedule(Callable<V>callable,long delay,TimeUnit unit)
:指定callable任務將在delay延遲後執行。ScheduledFuture<?>schedule(Runnable command,long delay,TimeUnit unit)
:指定command任務將在delay延遲後執行。ScheduledFuture<?>scheduleAtFixedRate(Runnable command,,long initialDelay,long period,TimeUnit unit)
:指定command任務將在delay延遲後執行,並且以設定頻率重複執行;ScheduledFuture<?>scheduleWithFixedRate(Runnable command,,long initialDelay,long delay,TimeUnit unit)
:建立並執行一個在給定初始延遲後首次啟用的定期操作,隨後在每一個執行終止和下一次執行開始之間都存在給定延遲,如果任務在任一次執行時遇到異常,就會取消後續執行,否則,只能通過程式來顯式取消或終止該任務。
用完一個執行緒池之後,應該呼叫該執行緒池的shutdown方法,該方法將啟動執行緒池的關閉序列,呼叫後的執行緒池將不再接受新任務,但會將以前所有已經提交的任務完成。當所有執行緒都完成的時候,執行緒池都的所有執行緒都會死亡;除此之外,也可以呼叫執行緒池的shutdownNow()方法來關閉執行緒池,該方法試圖通知所有正在執行的活動任務,暫停正在等待的任務,並返回等待的任務列表。
使用執行緒池來執行執行緒任務的步驟:
- 呼叫Executors類的靜態工廠方法建立一個Executor Service物件,該物件代表一個執行緒池;
- 建立Runnable實現類或Callable實現類的例項,作為執行緒執行任務;
- 呼叫Executor Service物件的submit()方法來提交Runnable例項或者Callable例項;
- 使用shutdown來關閉執行緒池;
package org.westos.demo8;
import java.util.TreeMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolTest {
public static void main(String[] args) {
//建立一個具有固定執行緒數的執行緒池
ExecutorService pool = Executors.newFixedThreadPool(6);
//使用Lambda表示式建立Runnable物件
Runnable target=()->{
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+"的i值為:"+i);
}
};
//向執行緒池中提交兩個執行緒
pool.submit(target);
pool.submit(target);
//關閉執行緒池
pool.shutdown();
}
}
Java8增強的ForkJoinPool
為了充分利用國多CPU、多核CPU的優勢,Java7提供了ForkJoinPool來支援講一個任務拆分成多個“小任務”平行計算,再把多個“小任務”的結果合併成總的計算結果。
ForkJoinPool是ExecutorService的實現類。
常用構造器:
ForkJoinPool(int parallelism)
:建立一個包含parallelism個並行執行緒的ForkJoinPool。ForkJoinPool()
:以Runtime.availableProcessors()方法的返回值作為parallelism引數建立ForkJoinPool。
Java8開始ForkJoinPool中增加了通用池功能:
ForkJoinPool commonPool()
:該方法返回一個通用池,通用池的執行狀態不會受shutdown或shutdownNow方法的影響。除非使用System.exit(0)來終止虛擬機器;int getCommonPoolParallelism()
:該方法返回通用池的並行級別;
建立了ForkJoinPool例項後,就可以呼叫ForkJoinPool的submit(ForkJoinTask task)或invoke(ForkJoinTask task)方法來執行指定任務。其中ForkJoinTask代表一個可以並行、合併的任務。
案例:以執行沒有返回值的“大任務”(列印0~300的數值)為例,程式將一個“大任務”差分成多個“小任務”,並將任務交給ForkJoinPool來執行。
package org.westos.demo8;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveAction;
import java.util.concurrent.TimeUnit;
class PrintTask extends RecursiveAction{
//每個“小任務”最多隻能列印50個數
private static final int THRESROLD=50;
private int start;
private int end;
public PrintTask(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected void compute() {
//當end和start之間的數小於THRESROLD時開始列印
if(end-start<THRESROLD){
for(int i=start;i<end;i++){
System.out.println(Thread.currentThread().getName()
+"的i值:"+i);
}
}else {
//當任務大於50時,分解大任務
int middle=(end+start)/2;
PrintTask left = new PrintTask(start, middle);
PrintTask right = new PrintTask(middle, end);
//並行執行兩個小任務
left.fork();
right.fork();
}
}
}
public class ForkJoinPoolTest {
public static void main(String[] args) throws InterruptedException {
ForkJoinPool pool = new ForkJoinPool();
pool.submit(new PrintTask(0,300));
pool.awaitTermination(2,TimeUnit.SECONDS);
//關閉縣城池
pool.shutdown();
}
}
從執行結果可以看到,執行緒池啟動了四個執行緒來列印,這是因為執行計算機是四核的。還可以看到列印並不是按順序列印,這是因為四個執行緒是並行的。
上面案例是一個沒有返回值的列印任務,如果是有返回值的列印任務,則可以讓任務繼承RecuriveTask,其中泛型引數T代表的就是返回值的型別。
案例2:對一個長度為100的陣列值進行疊加:
在這裡插入程式碼片
執行緒相關類
Java還為執行緒安全提供了一些工具類,如ThreadLocal類,它代表一個執行緒區域性變數,通過把資料放在ThreadLocal中就可以讓每個執行緒建立一個該變數的副本,從而避免併發訪問的執行緒安全問題。
ThreadLocal
Thread的功能是為每一個使用該變數的執行緒都提供變數值副本,使每個執行緒可以獨立地改變自己的副本,而不會和其他執行緒副本衝突。
ThreadLocal類的用法很簡單,它只提供瞭如下三個方法:
T get()
:返回此執行緒區域性變數中當前執行緒副本的值;void remove()
:刪除此執行緒區域性變數中當前執行緒的值;void set(T value)
:設定次執行緒區域性變數中當前執行緒副本的值;
package org.westos.demo4;
class Account{
//定義一個ThreadLocal型別的變數,該變數將是一個執行緒區域性變數,每個執行緒都會保留該變數的一個副本
private ThreadLocal<String> name=new ThreadLocal<>();
//定義一個初始化name成員變數的構造器
public Account(String str){
this.name.set(str);
//用於訪問當前執行緒的name副本的值
System.out.println("---"+this.name.get());
}
public String getName() {
return this.name.get();
}
public void setName(String name) {
this.name.set(name);
}
}
class MyTest extends Thread {
private Account account;
public MyTest(Account account, String name) {
super(name);
this.account = account;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
if (i == 6) {
//當i=6時,將賬戶名改為當前執行緒名
account.setName(getName());
}
System.out.println(account.getName() + "賬戶的值:" + "i");
}
}
}
public class ThreadLocalTest {
public static void main(String[] args) {
Account at = new Account("初始名");
new MyTest(at, "執行緒甲").start();
new MyTest(at, "執行緒乙").start();
}
}
可以看到兩個執行緒都會在i=6時將賬戶名改為與執行緒名相同,所以兩個執行緒擁有兩個賬戶名的情形。
實際上賬戶有三個副本,主執行緒一個,另外兩個啟動執行緒各一個,它們的值互不干擾。
Thread和其他所有同步機制一樣,都是為了解決多執行緒中對同一變數的訪問衝突。ThreadLocal並不能代替同步機制,兩者面向的問題領域不同。同步機制是為了同步多個執行緒對相同資源的併發訪問,是多個執行緒之間進行通訊的有效方式;而ThreadLocal是為了隔離多個執行緒的資料共享,從而避免多個執行緒之間對共享資源的競爭,也就不需要對多個執行緒進行同步了。