1. 程式人生 > >執行緒池和執行緒相關類

執行緒池和執行緒相關類

執行緒池概述
系統啟用一個新執行緒的成本是比較高的,因為它涉及與作業系統互動。在這種情形下,使用執行緒池可以很好的提高效能。執行緒池在系統啟動時即建立大量空閒的執行緒,程式將一個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是為了隔離多個執行緒的資料共享,從而避免多個執行緒之間對共享資源的競爭,也就不需要對多個執行緒進行同步了。