線程池

Java多線程

線程的同步是Java多線程編程的重點和難點,往往讓人搞不清楚什麼是競爭資源、什麼時候需要考慮同步,怎麼同步等等問題,當然,這些問題沒有很明確的答案,但有些原則問題需要考慮,是否有競爭資源被同時改動的問題?對於同步,在具體的Java代碼中需要完成以下兩個操作:把競爭訪問的資源標識爲private;同步哪些修改變量的代碼,使用synchronized關鍵字同步方法或代碼。當然這不是唯一控制併發安全的途徑。synchronized關鍵字使用說明synchronized只能標記非抽象的方法,不能標識成員變量

工作原理

線程是進程中的實體,一個進程可以擁有多個線程,一個線程必須有一個父進程。線程不擁有系統資源,只有運行必須的一些數據結構;它與父進程的其它線程共享該進程所擁有的全部資源。線程可以創建和撤消線程,從而實現程序的併發執行。一般,線程具有就緒阻塞運行三種基本狀態。

在多中央處理器系統裏,不同線程可以同時在不同的中央處理器上運行,甚至當它們屬於同一個進程時也是如此。大多數支持多處理器的操作系統都提供編程接口來讓進程可以控制自己的線程與各處理器之間的關聯度(affinity)。

有時候,線程也稱作輕量級進程。就象進程一樣,線程在程序中是獨立的、併發的執行路徑,每個線程有它自己的堆棧、自己的程序計數器和自己的局部變量。但是,與分隔的進程相比,進程中的線程之間的隔離程度要小。它們共享內存文件句柄和其它每個進程應有的狀態。

進程可以支持多個線程,它們看似同時執行,但互相之間並不同步。一個進程中的多個線程共享相同的內存地址空間,這就意味着它們可以訪問相同的變量對象,而且它們從同一堆中分配對象。儘管這讓線程之間共享信息變得更容易,但您必須小心,確保它們不會妨礙同一進程裏的其它線程。

Java 線程工具和 API看似簡單。但是,編寫有效使用線程的複雜程序並不十分容易。因爲有多個線程共存在相同的內存空間中並共享相同的變量,所以您必須小心,確保您的線程不會互相干擾。

線程屬性

爲了正確有效地使用線程,必須理解線程的各個方面並瞭解Java 實時系統。必須知道如何提供線程體、線程的生命週期、實時系統如 何調度線程、線程組、什麼是幽靈線程(Demo nThread)。

線程體

所有的操作都發生在線程體中,在Java中線程體是從Thread繼承的run()方法,或實現Runnable接口的類中的run()方法。當線程產生並初始化後,實時系統調用它的run()方法。run()方法內的代碼實現所產生線程的行爲,它是線程的主要部分。

線程狀態

thread.png
表示了線程在它的生命週期內的任何時刻所能處的狀態以及引起狀態改變的方法。這圖並不是完整的有限狀態圖,但基本概括了線程中比較感興趣和普遍的方面。以下討論有關線程生命週期以此爲據。

●New Thread
產生一個Thread對象就生成一個新線程。當線程處於"新線程"狀態時,僅僅是一個空線程對象,它還沒有分配到系統資源。因此只能啓動或終止它。任何其他操作都會引發異常。例如,一個線程調用了new方法之後,並在調用start方法之前的處於新線程狀態,可以調用start和stop方法。

●Runnable
start()方法產生運行線程所必須的資源,調度線程執行,並且調用線程的run()方法。在這時線程處於可運行態。該狀態不稱爲運行態是因爲這時的線程並不總是一直佔用處理機。特別是對於只有一個處理機的PC而言,任何時刻只能有一個處於可運行態的線程佔用處理 機。Java通過調度來實現多線程處理機的共享。注意,如果線程處於Runnable狀態,它也有可能不在運行,這是因爲還有優先級和調度問題。

●Blocked
當以下事件發生時,線程進入非運行態。
①suspend()方法被調用;
②sleep()方法被調用;
③線程使用wait()來等待條件變量
④線程處於I/O請求的等待。

●Dead
當run()方法返回,或別的線程調用stop()方法,線程進入死亡態。通常Applet使用它的stop()方法來終止它產生的所有線程。

線程的本操作:

  • 派生:線程在進程內派生出來,它即可由進程派生,也可由線程派生。
  • 阻塞(Block):如果一個線程在執行過程中需要等待某個事件發生,則被阻塞。
  • 激活(unblock):如果阻塞線程的事件發生,則該線程被激活並進入就緒隊列。
  • 調度(schedule):選擇一個就緒線程進入執行狀態
  • 結束(Finish):如果一個線程執行結束,它的寄存器上下文以及堆棧內容等將被釋放。
線程池的分類
  • FixedThreadPool

ExecutorsnewFixedThreadPool方法創建。它是一種線程數量固定的線程池,當線程處於空閒狀態時,他們並不會被回收,除非線程池被關閉。當所有的線程都處於活動狀態時,新的任務都會處於等待狀態,直到有線程空閒出來。FixedThreadPool只有核心線程,且該核心線程都不會被回收,這意味着它可以更快地響應外界的請求

  • CachedThreadPool

ExecutorsnewCachedThreadPool方法創建,不存在覈心線程,只存在數量不定的非核心線程,而且其數量最大值爲Integer.MAX_VALUE。當線程池中的線程都處於活動時(全滿),線程池會創建新的線程來處理新的任務,否則就會利用新的線程來處理新的任務,線程池中的空閒線程都有超時機制,默認超時時長爲60s,超過60s的空閒線程就會被回收。和FixedThreadPool不同的是,CachedThreadPool的任務隊列其實相當於一個空的集合,這將導致任何任務都會被執行,因爲在這種場景下SynchronousQueue是不能插入任務的,SynchronousQueue是一個特殊的隊列,在很多情況下可以理解爲一個無法儲存元素的隊列。從CachedThreadPool的特性看,這類線程比較適合執行大量耗時較小的任務。當整個線程池都處於閒置狀態時,線程池中的線程都會因爲超時而被停止回收,幾乎是不佔任何系統資源。

  • ScheduledThreadPool

通過ExecutorsnewScheduledThreadPool方式創建,核心線程數量是固定的,而非核心線程是沒有限制的,並且當非核心線程閒置時它會被立即回收,ScheduledThreadPool這類線程池主要用於執行定時任務和具有固定時期的重複任務

  • SingleThreadExecutor

通過ExecutorsnewSingleThreadExecutor方法來創建。這類線程池內部只有一個核心線程,它確保所有的任務都在同一個線程中按順序執行。SingleThreadExecutor的意義在於統一所有外界任務一個線程中,這使得這些任務之間不需要處理線程同步的問題,

線程池的調度策略

ThreadPool.png

線程的另一個執行特性是同步。線程中所使用的同步控制機制與進程中所使用的同步控制機制相同。

ThreadPoolExecutor參數詳解

ThreadPoolExecutor有多個構造函數,這裏我們詳細展開一個參數最多的展開。

new ThreadPoolExecutor(
            CORE_THREAD_COUNT,
            MAX_THREAD_COUNT,
            KEEP_ALIVE,
            TimeUnit.SECONDS,
            new LinkedBlockingQueue<Runnable>(),
            threadFactory
);
  • CORE_THREAD_COUNT
    池中所保存的線程數,包括空閒線程。需要注意的是在初創建線程池時線程不會立即啓動,直到有任務提交纔開始啓動線程並逐漸時線程數目達到CORE_THREAD_COUNT。若想一開始就創建所有核心線程需調用prestartAllCoreThreads方法。
  • MAX_THREAD_COUNT-池中允許的最大線程數。需要注意的是當核心線程滿且阻塞隊列也滿時纔會判斷當前線程數是否小於最大線程數,並決定是否創建新線程。
  • KEEP_ALIVE
    當線程數大於核心時,多於的空閒線程最多存活時間
  • TimeUnit.SECONDS

keepAliveTime 參數的時間單位。

  • LinkedBlockingQueue 阻塞隊列(線程安全)後面會詳細介紹
  • ThreadFactory - 執行程序創建新線程時使用的工廠。
LinkedBlockingQueue
    1. LinkedBlockingQueue不允許元素爲null,這一點在構造方法中也說過了。
    1. 同一時刻,只能有一個線程執行入隊操作,因爲putLock在將元素插入到隊列尾部時加鎖了
    1. 如果隊列滿了,那麼將會調用notFull的await()方法將該線程加入到Condition等待隊列中。await()方法就會釋放線程佔有的鎖,這將導致之前由於被鎖阻塞的入隊線程將會獲取到鎖,執行到while循環處,不過可能因爲由於隊列仍舊是滿的,也被加入到條件隊列中。
    1. 一旦一個出隊線程取走了一個元素,並通知了入隊等待隊列中可以釋放線程了,那麼第一個加入到Condition隊列中的將會被釋放,那麼該線程將會重新獲得put鎖,繼而執行enqueue()方法,將節點插入到隊列的尾部
    1. 然後得到插入一個節點之前的元素個數,如果隊列中還有空間可以插入,那麼就通知notFull條件的等待隊列中的線程。
    1. 通知出隊線程隊列爲空了,因爲插入一個元素之前的個數爲0,而插入一個之後隊列中的元素就從無變成了有,就可以通知因隊列爲空而阻塞的出隊線程了。
Handler

阻塞隊列已滿且線程數達到最大值時所採取的飽和策略。java默認提供了4種飽和策略的實現方式:中止、拋棄、拋棄最舊的、調用者運行。

  • 1.Abort策略

默認策略,新任務提交時直接拋出未檢查的異常RejectedExecutionException,該異常可由調用者捕獲。

  • 2.CallerRuns策略:爲調節機制,既不拋棄任務也不拋出異常,而是將某些任務回退到調用者。不會在線程池的線程中執行新的任務,而是在調用exector的線程中運行新的任務。
  • 3.Discard策略:新提交的任務被拋棄。
  • 4.DiscardOldest策略:隊列的是“隊頭”的任務,然後嘗試提交新的任務。(不適合工作隊列爲優先隊列場景)
ThreadPoolExecutor實例

在使用線程池之前我們需要預先定義線程池配置信息,在此之前我們已經詳細解釋了創建線程池各個參數的意義,這裏給出一個比較常規的配置方案。

CPU_COUNT=Runtime.getRuntime().availableProcessors();
CORE_THREAD_COUNT=CPU_COUNT+1;
MAX_THREAD_COUNT=CPU_COUNT*2+1;

之後需要創建一個靜態工廠並是實現newThread(Runnable r)方法用於創建新線程。其中AtomicInteger是線程安全的整型變量用與區分線程號。

private static final ThreadFactory threadFactory=new ThreadFactory() {
        private AtomicInteger atomicInteger=new AtomicInteger(1);
        @Override
        public Thread newThread(Runnable r) {
            return new Thread(r,"Thread#"+atomicInteger.getAndIncrement());
        }
    };

測試代碼

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class Main {
    private final static int CPU_COUNT=Runtime.getRuntime().availableProcessors();
    private final static int CORE_THREAD_COUNT=CPU_COUNT+1;
    private final static int MAX_THREAD_COUNT=CPU_COUNT*2+1;
    private final static long KEEP_ALIVE=10l;
    private static final ThreadFactory threadFactory=new ThreadFactory() {
        private AtomicInteger atomicInteger=new AtomicInteger(1);
        @Override
        public Thread newThread(Runnable r) {
            return new Thread(r,"Thread#"+atomicInteger.getAndIncrement());
        }
    };
    private static final ThreadPoolExecutor threadPollExecutor=new ThreadPoolExecutor(
            CORE_THREAD_COUNT,
            MAX_THREAD_COUNT,
            KEEP_ALIVE,
            TimeUnit.SECONDS,
            new LinkedBlockingQueue<Runnable>(),
            threadFactory);
    public static void main(String[] args) {
        for (int i=0;i<5;i++){
            Runnable runnable=new Runnable() {
                @Override
                public void run() {
                    int index=0;
                    while (true){
                        System.out.println(Thread.currentThread().getName()+":"+index++);
                    }
                }
            };
            threadPollExecutor.execute(runnable);
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章