java之多線程總結二

線程或者說多線程,是咱們處理多任務的強大工具。線程和進程是不一樣的,每一個進程都是一個獨立運行的程序,擁有本身的變量,且不一樣進程間的變量不能共享;而線程是運行在進程內部的,每一個正在運行的進程至少有一個線程,並且不一樣的線程之間能夠在進程範圍內共享數據。也就是說進程有本身獨立的存儲空間,而線程是和它所屬的進程內的其餘線程共享一個存儲空間。線程的使用可使咱們可以並行地處理一些事情。線程經過並行的處理給用戶帶來更好的使用體驗,好比你使用的郵件系統(outlook、Thunderbird、foxmail等),你固然不但願它們在收取新郵件的時候,致使你連已經收下來的郵件都沒法閱讀,而只能等待收取郵件操做執行完畢。這正是線程的意義所在。 

實現線程的方式 

實現線程的方式有兩種: java

  1. 繼承java.lang.Thread,並重寫它的run()方法,將線程的執行主體放入其中。
  2. 實現java.lang.Runnable接口,實現它的run()方法,並將線程的執行主體放入其中。


這是繼承Thread類實現線程的示例: 數據庫

  1. public class ThreadTest extends Thread {  
        public void run() {  
            // 在這裏編寫線程執行的主體  
            // do something  
        }  
    }  

     


這是實現Runnable接口實現多線程的示例: 編程

  1. public class RunnableTest implements Runnable {  
        public void run() {  
            // 在這裏編寫線程執行的主體  
            // do something  
        }  
    }  

     


這兩種實現方式的區別並不大。繼承Thread類的方式實現起來較爲簡單,可是繼承它的類就不能再繼承別的類了,所以也就不能繼承別的類的有用的方法了。而使用是想Runnable接口的方式就不存在這個問題了,並且這種實現方式將線程主體和線程對象自己分離開來,邏輯上也較爲清晰,因此推薦你們更多地採用這種方式。 

如何啓動線程 

咱們經過以上兩種方式實現了一個線程以後,線程的實例並無被建立,所以它們也並無被運行。咱們要啓動一個線程,必須調用方法來啓動它,這個方法就是Thread類的start()方法,而不是run()方法(既不是咱們繼承Thread類重寫的run()方法,也不是實現Runnable接口的run()方法)。run()方法中包含的是線程的主體,也就是這個線程被啓動後將要運行的代碼,它跟線程的啓動沒有任何關係。上面兩種實現線程的方式在啓動時會有所不一樣。 

繼承Thread類的啓動方式: 多線程

public class ThreadStartTest {  
    public static void main(String[] args) {  
        // 建立一個線程實例  
        ThreadTest tt = new ThreadTest();  
        // 啓動線程  
        tt.start();  
    }  
} 

 
實現Runnable接口的啓動方式: ide

public class RunnableStartTest {  
    public static void main(String[] args) {  
        // 建立一個線程實例  
        Thread t = new Thread(new RunnableTest());  
        // 啓動線程  
        t.start();  
    }  
}  


實際上這兩種啓動線程的方式原理是同樣的。首先都是調用本地方法啓動一個線程,其次是在這個線程裏執行目標對象的run()方法。那麼這個目標對象是什麼呢?爲了弄明白這個問題,咱們來看看Thread類的run()方法的實現: 工具

public void run() {  
    if (target != null) {  
        target.run();  
    }  
}  


當咱們採用實現Runnable接口的方式來實現線程的狀況下,在調用new Thread(Runnable target)構造器時,將實現Runnable接口的類的實例設置成了線程要執行的主體所屬的目標對象target,當線程啓動時,這個實例的run()方法就被執行了。當咱們採用繼承Thread的方式實現線程時,線程的這個run()方法被重寫了,因此當線程啓動時,執行的是這個對象自身的run()方法。總結起來就一句話,線程類有一個Runnable類型的target屬性,它是線程啓動後要執行的run()方法所屬的主體,若是咱們採用的是繼承Thread類的方式,那麼這個target就是線程對象自身,若是咱們採用的是實現Runnable接口的方式,那麼這個target就是實現了Runnable接口的類的實例。 

線程的狀態 

在Java 1.4及如下的版本中,每一個線程都具備新建、可運行、阻塞、死亡四種狀態,可是在Java 5.0及以上版本中,線程的狀態被擴充爲新建、可運行、阻塞、等待、定時等待、死亡六種。線程的狀態徹底包含了一個線程重新建到運行,最後到結束的整個生命週期。線程狀態的具體信息以下: 優化

  1. NEW(新建狀態、初始化狀態):線程對象已經被建立,可是尚未被啓動時的狀態。這段時間就是在咱們調用new命令以後,調用start()方法以前。
  2. RUNNABLE(可運行狀態、就緒狀態):在咱們調用了線程的start()方法以後線程所處的狀態。處於RUNNABLE狀態的線程在JAVA虛擬機(JVM)上是運行着的,可是它可能還正在等待操做系統分配給它相應的運行資源以得以運行。
  3. BLOCKED(阻塞狀態、被中斷運行):線程正在等待其它的線程釋放同步鎖,以進入一個同步塊或者同步方法繼續運行;或者它已經進入了某個同步塊或同步方法,在運行的過程當中它調用了某個對象繼承自java.lang.Object的wait()方法,正在等待從新返回這個同步塊或同步方法。
  4. WAITING(等待狀態):當前線程調用了java.lang.Object.wait()、java.lang.Thread.join()或者java.util.concurrent.locks.LockSupport.park()三個中的任意一個方法,正在等待另一個線程執行某個操做。好比一個線程調用了某個對象的wait()方法,正在等待其它線程調用這個對象的notify()或者notifyAll()(這兩個方法一樣是繼承自Object類)方法來喚醒它;或者一個線程調用了另外一個線程的join()(這個方法屬於Thread類)方法,正在等待這個方法運行結束。
  5. TIMED_WAITING(定時等待狀態):當前線程調用了java.lang.Object.wait(long timeout)、java.lang.Thread.join(long millis)、java.util.concurrent.locks.LockSupport.packNanos(long nanos)、java.util.concurrent.locks.LockSupport.packUntil(long deadline)四個方法中的任意一個,進入等待狀態,可是與WAITING狀態不一樣的是,它有一個最大等待時間,即便等待的條件仍然沒有知足,只要到了這個時間它就會自動醒來。
  6. TERMINATED(死亡狀態、終止狀態):線程完成執行後的狀態。線程執行完run()方法中的所有代碼,從該方法中退出,進入TERMINATED狀態。還有一種狀況是run()在運行過程當中拋出了一個異常,而這個異常沒有被程序捕獲,致使這個線程異常終止進入TERMINATED狀態。

一個任務進入阻塞狀態,可能緣由以下:this

    1)經過調用sleep使任務進入休眠狀態,在這種狀況下,任務在制定的時間內不會運行。編碼

    2)你經過調用wait是線程掛起,直到線程獲得了notify和notifyall消息,線程纔會進入就緒狀態。操作系統

    3)任務在等待某個輸入/輸出完成。

    4)任務試圖在某個對象上調用其同步控制的方法,可是對象鎖不可用,由於另外一個任務已經獲取了這個鎖。


在Java5.0及以上版本中,線程的所有六種狀態都以枚舉類型的形式定義在java.lang.Thread類中了,代碼以下: 

public enum State {  
    NEW,  
    RUNNABLE,  
    BLOCKED,  
    WAITING,  
    TIMED_WAITING,  
    TERMINATED;  
}  


實現同步的方式 

同步是多線程中的重要概念。同步的使用能夠保證在多線程運行的環境中,程序不會產生設計以外的錯誤結果。同步的實現方式有兩種,同步方法和同步塊,這兩種方式都要用到synchronized關鍵字。 

給一個方法增長synchronized修飾符以後就可使它成爲同步方法,這個方法能夠是靜態方法和非靜態方法,可是不能是抽象類的抽象方法,也不能是接口中的接口方法。下面代碼是一個同步方法的示例: 

public synchronized void aMethod() {  
    // do something  
}  
public static synchronized void anotherMethod() {  
    // do something  
}  


線程在執行同步方法時是具備排它性的。當任意一個線程進入到一個對象的任意一個同步方法時,這個對象的全部同步方法都被鎖定了,在此期間,其餘任何線程都不能訪問這個對象的任意一個同步方法,直到這個線程執行完它所調用的同步方法並從中退出,從而致使它釋放了該對象的同步鎖以後。在一個對象被某個線程鎖定以後,其餘線程是能夠訪問這個對象的全部非同步方法的。 

同步塊的形式雖然與同步方法不一樣,可是原理和效果是一致的。同步塊是經過鎖定一個指定的對象,來對同步塊中包含的代碼進行同步;而同步方法是對這個方法塊裏的代碼進行同步,而這種狀況下鎖定的對象就是同步方法所屬的主體對象自身。若是這個方法是靜態同步方法呢?那麼線程鎖定的就不是這個類的對象了,也不是這個類自身,而是這個類對應的java.lang.Class類型的對象。同步方法和同步塊之間的相互制約只限於同一個對象之間,因此靜態同步方法只受它所屬類的其它靜態同步方法的制約,而跟這個類的實例(對象)沒有關係。 

下面這段代碼演示了同步塊的實現方式: 

public void test() {  
    // 同步鎖  
    String lock = "LOCK";  
    // 同步塊  
    synchronized (lock) {  
        // do something  
    }  
    int i = 0;  
    // ...  
}  


對於做爲同步鎖的對象並無什麼特別要求,任意一個對象均可以。若是一個對象既有同步方法,又有同步塊,那麼當其中任意一個同步方法或者同步塊被某個線程執行時,這個對象就被鎖定了,其餘線程沒法在此時訪問這個對象的同步方法,也不能執行同步塊。 

synchronized和Lock 

Lock是一個接口,它位於Java 5.0新增的java.utils.concurrent包的子包locks中。concurrent包及其子包中的類都是用來處理多線程編程的。實現Lock接口的類具備與synchronized關鍵字一樣的功能,可是它更增強大一些。java.utils.concurrent.locks.ReentrantLock是較經常使用的實現了Lock接口的類。下面是ReentrantLock類的一個應用實例: 

private Lock lock = new ReentrantLock();  
public void testLock() {  
    // 鎖定對象  
    lock.lock();  
    try {  
        // do something  
    } finally {  
        // 釋放對對象的鎖定  
        lock.unlock();  
    }  
}  


lock()方法用於鎖定對象,unlock()方法用於釋放對對象的鎖定,他們都是在Lock接口中定義的方法。位於這兩個方法之間的代碼在被執行時,效果等同於被放在synchronized同步塊中。通常用法是將須要在lock()和unlock()方法之間執行的代碼放在try{}塊中,而且在finally{}塊中調用unlock()方法,這樣就能夠保證即便在執行代碼拋出異常的狀況下,對象的鎖也老是會被釋放,不然的話就會爲死鎖的產生增長可能。 

使用synchronized關鍵字實現的同步,會把一個對象的全部同步方法和同步塊看作一個總體,只要有一個被某個線程調用了,其餘的就沒法被別的線程執行,即便這些方法或同步塊與被調用的代碼之間沒有任何邏輯關係,這顯然下降了程序的運行效率。而使用Lock就可以很好地解決這個問題。咱們能夠把一個對象中按照邏輯關係把須要同步的方法或代碼進行分組,爲每一個組建立一個Lock類型的對象,對實現同步。那麼,當一個同步塊被執行時,這個線程只會鎖定與當前運行代碼相關的其餘代碼最小集合,而並不影響其餘線程對其他同步代碼的調用執行。 

關於死鎖 

死鎖就是一個進程中的每一個線程都在等待這個進程中的其餘線程釋放所佔用的資源,從而致使全部線程都沒法繼續執行的狀況。死鎖是多線程編程中一個隱藏的陷阱,它常常發生在多個線程共用資源的時候。在實際開發中,死鎖通常隱藏的較深,不容易被發現,一旦死鎖現象發生,就必然會致使程序的癱瘓。所以必須避免它的發生。 

程序中必須同時知足如下四個條件纔會引起死鎖: 

  1. 互斥(Mutual exclusion):線程所使用的資源中至少有一個是不能共享的,它在同一時刻只能由一個線程使用。
  2. 持有與等待(Hold and wait):至少有一個線程已經持有了資源,而且正在等待獲取其餘的線程所持有的資源。
  3. 非搶佔式(No pre-emption):若是一個線程已經持有了某個資源,那麼在這個線程釋放這個資源以前,別的線程不能把它搶奪過去使用。
  4. 循環等待(Circular wait):假設有N個線程在運行,第一個線程持有了一個資源,而且正在等待獲取第二個線程持有的資源,而第二個線程正在等待獲取第三個線程持有的資源,依此類推……第N個線程正在等待獲取第一個線程持有的資源,由此造成一個循環等待。


線程池 

線程池就像數據庫鏈接池同樣,是一個對象池。全部的對象池都有一個共同的目的,那就是爲了提升對象的使用率,從而達到提升程序效率的目的。好比對於Servlet,它被設計爲多線程的(若是它是單線程的,你就能夠想象,當1000我的同時請求一個網頁時,在第一我的得到請求結果以前,其它999我的都在鬱悶地等待),若是爲每一個用戶的每一次請求都建立一個新的線程對象來運行的話,系統就會在建立線程和銷燬線程上耗費很大的開銷,大大下降系統的效率。所以,Servlet多線程機制背後有一個線程池在支持,線程池在初始化初期就建立了必定數量的線程對象,經過提升對這些對象的利用率,避免高頻率地建立對象,從而達到提升程序的效率的目的。 

下面實現一個最簡單的線程池,從中理解它的實現原理。爲此咱們定義了四個類,它們的用途及具體實現以下: 

1.Task(任務):這是個表明任務的抽象類,其中定義了一個deal()方法,繼承Task抽象類的子類須要實現這個方法,並把這個任務須要完成的具體工做在deal()方法編碼實現。線程池中的線程之因此被建立,就是爲了執行各類各樣數量繁多的任務的,爲了方便線程對任務的處理,咱們須要用Task抽象類來保證任務的具體工做統一放在deal()方法裏來完成,這樣也使代碼更加規範。 
Task的定義以下: 

public abstract class Task {  
    public enum State {  
        /* 新建 */NEW, /* 執行中 */RUNNING, /* 已完成 */FINISHED  
    }  
    // 任務狀態  
    private State state = State.NEW;  
    public void setState(State state) {  
        this.state = state;  
    }  
    public State getState() {  
        return state;  
    }  
    public abstract void deal();  
}  


2.TaskQueue(任務隊列):在同一時刻,可能有不少任務須要執行,而程序在同一時刻只能執行必定數量的任務,當須要執行的任務數超過了程序所能承受的任務數時怎麼辦呢?這就有了先執行哪些任務,後執行哪些任務的規則。TaskQueue類就定義了這些規則中的一種,它採用的是FIFO(先進先出,英文名是First In First Out)的方式,也就是按照任務到達的前後順序執行。 
TaskQueue類的定義以下: 

import java.util.Iterator;  
import java.util.LinkedList;  
import java.util.List;  
public class TaskQueue {  
    private List<Task> queue = new LinkedList<Task>();  
    // 添加一項任務  
    public synchronized void addTask(Task task) {  
        if (task != null) {  
            queue.add(task);  
        }  
    }  
    // 完成任務後將它從任務隊列中刪除  
    public synchronized void finishTask(Task task) {  
        if (task != null) {  
            task.setState(Task.State.FINISHED);  
            queue.remove(task);  
        }  
    }  
    // 取得一項待執行任務  
    public synchronized Task getTask() {  
        Iterator<Task> it = queue.iterator();  
        Task task;  
        while (it.hasNext()) {  
            task = it.next();  
            // 尋找一個新建的任務  
            if (Task.State.NEW.equals(task.getState())) {  
                // 把任務狀態置爲運行中  
                task.setState(Task.State.RUNNING);  
                return task;  
            }  
        }  
        return null;  
    }  
}  

3.addTask(Task task)方法用於當一個新的任務到達時,將它添加到任務隊列中。這裏使用了LinkedList類來保存任務到達的前後順序。finishTask(Task task)方法用於任務被執行完畢時,將它從任務隊列中清除出去。getTask()方法用於取得當前要執行的任務。
TaskThread(執行任務的線程):它繼承自Thread類,專門用於執行任務隊列中的待執行任務。

public class TaskThread extends Thread {  
    // 該線程所屬的線程池  
    private ThreadPoolService service;  
    public TaskThread(ThreadPoolService tps) {  
        service = tps;  
    }  
    public void run() {  
        // 在線程池運行的狀態下執行任務隊列中的任務  
        while (service.isRunning()) {  
            TaskQueue queue = service.getTaskQueue();  
            Task task = queue.getTask();  
            if (task != null) {  
                task.deal();  
            }  
            queue.finishTask(task);  
        }  
    }  
}  


4.ThreadPoolService(線程池服務類):這是線程池最核心的一個類。它在被建立了時候就建立了幾個線程對象,可是這些線程並無啓動運行,但調用了start()方法啓動線程池服務時,它們才真正運行。stop()方法能夠中止線程池服務,同時中止池中全部線程的運行。而runTask(Task task)方法是將一個新的待執行任務交與線程池來運行。 
ThreadPoolService類的定義以下: 

import java.util.ArrayList;  
import java.util.List;  
public class ThreadPoolService {  
    // 線程數  
    public static final int THREAD_COUNT = 5;  
    // 線程池狀態  
    private Status status = Status.NEW;  
    private TaskQueue queue = new TaskQueue();  
    public enum Status {  
        /* 新建 */NEW, /* 提供服務中 */RUNNING, /* 中止服務 */TERMINATED,  
    }  
    private List<Thread> threads = new ArrayList<Thread>();  
    public ThreadPoolService() {  
        for (int i = 0; i < THREAD_COUNT; i++) {  
            Thread t = new TaskThread(this);  
            threads.add(t);  
        }  
    }  
    // 啓動服務  
    public void start() {  
        this.status = Status.RUNNING;  
        for (int i = 0; i < THREAD_COUNT; i++) {  
            threads.get(i).start();  
        }  
    }  
    // 中止服務  
    public void stop() {  
        this.status = Status.TERMINATED;  
    }  
    // 是否正在運行  
    public boolean isRunning() {  
        return status == Status.RUNNING;  
    }  
    // 執行任務  
    public void runTask(Task task) {  
        queue.addTask(task);  
    }  
    protected TaskQueue getTaskQueue() {  
        return queue;  
    }  
}  

完成了上面四個類,咱們就實現了一個簡單的線程池。如今咱們就可使用它了,下面的代碼作了一個簡單的示例:

public class SimpleTaskTest extends Task {  
    @Override  
    public void deal() {  
        // do something  
    }  
    public static void main(String[] args) throws InterruptedException {  
        ThreadPoolService service = new ThreadPoolService();  
        service.start();  
        // 執行十次任務  
        for (int i = 0; i < 10; i++) {  
            service.runTask(new SimpleTaskTest());  
        }  
        // 睡眠1秒鐘,等待全部任務執行完畢  
        Thread.sleep(1000);  
        service.stop();  
    }  
}

 

固然,咱們實現的是最簡單的,這裏只是爲了演示線程池的實現原理。在實際應用中,根據狀況的不一樣,能夠作不少優化。好比: 

  • 調整任務隊列的規則,給任務設置優先級,級別高的任務優先執行。
  • 動態維護線程池,當待執行任務數量較多時,增長線程的數量,加快任務的執行速度;當任務較少時,回收一部分長期閒置的線程,減小對系統資源的消耗。

事實上Java5.0及以上版本已經爲咱們提供了線程池功能,無需再從新實現。這些類位於java.util.concurrent包中。 

Executors類提供了一組建立線程池對象的方法,經常使用的有一下幾個: 

public static ExecutorService newCachedThreadPool() {  
    // other code  
}  
public static ExecutorService newFixedThreadPool(int nThreads) {  
    // other code  
}  
public static ExecutorService newSingleThreadExecutor() {  
    // other code  
}  

newCachedThreadPool()方法建立一個動態的線程池,其中線程的數量會根據實際須要來建立和回收,適合於執行大量短時間任務的狀況;newFixedThreadPool(int nThreads)方法建立一個包含固定數量線程對象的線程池,nThreads表明要建立的線程數,若是某個線程在運行的過程當中由於異常而終止了,那麼一個新的線程會被建立和啓動來代替它;而newSingleThreadExecutor()方法則只在線程池中建立一個線程,來執行全部的任務。 

這三個方法都返回了一個ExecutorService類型的對象。實際上,ExecutorService是一個接口,它的submit()方法負責接收任務並交與線程池中的線程去運行。submit()方法可以接受Callable和Runnable兩種類型的對象。它們的用法和區別以下: 
Runnable接口:繼承Runnable接口的類要實現它的run()方法,並將執行任務的代碼放入其中,run()方法沒有返回值。適合於只作某種操做,不關心運行結果的狀況。
Callable接口:繼承Callable接口的類要實現它的call()方法,並將執行任務的代碼放入其中,call()將任務的執行結果做爲返回值。適合於執行某種操做後,須要知道執行結果的狀況。

不管是接收Runnable型參數,仍是接收Callable型參數的submit()方法,都會返回一個Future(也是一個接口)類型的對象。該對象中包含了任務的執行狀況以及結果。調用Future的boolean isDone()方法能夠獲知任務是否執行完畢;調用Object get()方法能夠得到任務執行後的返回結果,若是此時任務尚未執行完,get()方法會保持等待,直到相應的任務執行完畢後,纔會將結果返回。 

咱們用下面的一個例子來演示Java5.0中線程池的使用: 

import java.util.concurrent.*;  
public class ExecutorTest {  
    public static void main(String[] args) throws InterruptedException,  
            ExecutionException {  
        ExecutorService es = Executors.newSingleThreadExecutor();  
        Future fr = es.submit(new RunnableTest());// 提交任務  
        Future fc = es.submit(new CallableTest());// 提交任務  
        // 取得返回值並輸出  
        System.out.println((String) fc.get());  
        // 檢查任務是否執行完畢  
        if (fr.isDone()) {  
            System.out.println("執行完畢-RunnableTest.run()");  
        } else {  
            System.out.println("未執行完-RunnableTest.run()");  
        }  
        // 檢查任務是否執行完畢  
        if (fc.isDone()) {  
            System.out.println("執行完畢-CallableTest.run()");  
        } else {  
            System.out.println("未執行完-CallableTest.run()");  
        }  
        // 中止線程池服務  
        es.shutdown();  
    }  
}  
class RunnableTest implements Runnable {  
    public void run() {  
        System.out.println("已經執行-RunnableTest.run()");  
    }  
}  
class CallableTest implements Callable {  
    public Object call() {  
        System.out.println("已經執行-CallableTest.call()");  
        return "返回值-CallableTest.call()";  
    }  
}  

運行結果: 
已經執行-RunnableTest.run()
已經執行-CallableTest.call()
返回值-CallableTest.call()
執行完畢-RunnableTest.run()
執行完畢-CallableTest.run()

使用完線程池以後,須要調用它的shutdown()方法中止服務,不然其中的全部線程都會保持運行,程序不會退出。

相關文章
相關標籤/搜索