Java:多線程概述與建立方式

Java:多線程概述與建立方式

在以前的學習過程當中,已經不止一次地提到了併發啊,線程啊,同步異步的內容,可是出於內容的局部一體,以前老是幾筆帶過,並附上:之後學習的時候再細說。編程

那麼,如今到了細說的時候,在翻閱並參考了介紹Java併發編程的書以後,忽然感受壓力有些大,由於有些概念確實比較抽象。因此以後的內容不定長短,可是天天都會試着輸出一些。設計模式

進程和線程

一個進程能夠擁有多個線程,一個線程必須擁有一個父進程。
進程:當前操做系統正在執行的任務,也是操做系統運行程序的一次執行過程。
線程:是進程的執行單元,是進程中正在執行的子任務。
就好像咱們正在使用的QQ,正在放歌的音樂軟件,正在打的遊戲,就是一個個的進程。咱們在QQ進程中執行的各類操做,就是一個個的線程。安全

每一個Java的應用程序運行的時候其實就是個進程,JVM啓動以後,會建立一些進行自身常規管理的線程,如垃圾回收和終結管理,和一個運行main函數的主線程多線程

併發與並行

如今大部分的操做系統都是支持多進程併發運行的,就像咱們如今正在使用電腦,能夠經過任務管理器查查看,會發現有幾十個幾百個進程在「同時執行」。」同時執行「被打上了引號,顯然事實上並非。併發

併發:就拿進程來講,在同一個時刻,只能有一條指令執行,可是多個進程能夠被快速地輪換執行,CPU的執行速度之快,讓人產生這些個進程就是在同時執行。
並行:就是同一時刻,多條進程指令在多個處理器上同時執行異步

看看下面的圖就懂了:ide

接下來是我對於併發和並行假想場景:
併發場景:假設如今有一臺只能一我的玩的電腦,老大和老二兄弟倆都想玩一小會兒,那沒辦法,得想辦法解決啊。打一架吧,誰搶到算誰的。不論是誰搶到,他們必定玩到知足纔會罷休,這就是如今操做系統所採用的高效率的搶佔式多任務操做策略
並行場景:如今有兩臺電腦,老大老二都各自玩各自的電腦,不爭也不搶。函數

多線程的優點

線程被稱爲輕量級進程,大多數狀況下,進程中的多線程的執行是搶佔式的,就和操做系統的併發多進程同樣。學習

線程擁有本身的堆棧程序計數器局部變量,容許程序控制流的多重分支同時存在於一個線程,共享進程範圍內的資源,所以,同一進程中的線程訪問相同的變量,並從同一個堆中分配對象,實現良好的數據共享,可是若是處理不當,會爲線程安全形成必定的隱患。

多線程相比於多進程的優點:

  • 多個線程之間能夠共享內存,而進程之間不能夠。
  • 操做系統建立線程的代價比進程小,實現多任務併發效率更高

如下參考自《JAVA併發編程實戰》:

  • 一個單線程應用程序一次之能運行在一個處理器上。在雙處理器系統中只運行一個應用程序,至關於其中一個處理器空閒,50%的CPU資源沒有利用上。隨着處理器的增多,單線程的應用程序放棄的CPU資源將會更多。這一點,正好也側面反映了多線程可以更有效地利用空閒的處理器資源
  • 處理器在某些狀況是空閒的,如在等待一個同步IO操做完成的時候。這個時候,暫且不論多處理器,僅僅針對單處理器,多線程的優點也是至關明顯的,能夠很好地利用處理器空閒的時間運行另一個線程

線程的建立和啓動

先來看看多線程編程中這個至關關鍵的類,java.lang.Thread,官方文檔說了:有兩種方式建立線程,就是下面這倆:

繼承Thread類

  • 將一個類聲明爲Thread的子類。
  • 這個子類應該覆蓋類Threadrun()方法。

建立線程以下:

/*建立線程*/
//建立一個類繼承Thread類
class TDemo extends Thread{
    //線程要執行的任務在run方法中
    @Override
    public void run(){
        for (int i = 0; i < 5; i++) {
            System.out.println(i);
        }
    }
}

啓動線程以下:

public static void main(String[] args){
        //建立了TDemo的實例
        TDemo t1 = new TDemo();
        //啓動線程,並調用run方法
        t1.start();
        System.out.print("main");
    }
    //輸出結果:main01234

建立TDemo的實例對象不等於啓動了該實例所對應的線程,啓動須要調用線程對象的start()方法。

start()和run()

  • new建立了TDemo的實例,只是建立了一個線程,此時它處於新建狀態,有JVM分配內存,並初始化成員變量的值,是個配置的過程。
  • 線程對象調用start()方法以後,線程就會處於就緒狀態,JVM會爲其建立方法調用棧和程序計數器,表示這個線程能夠執行,但真正啥時候開始執行取決於JVM中線程調度器的調度

  • 以後才進入運行狀態,執行run()方法中的方法體。

咱們試着把start()方法換成run()方法看看結果:01234main


咱們經過輸出結果能夠看到,調用start()方法,系統會把run()方法當成線程執行體處理,主線程和咱們建立的線程將併發執行。但若是單純調用run()方法,系統會把線程對象當成一個普通的對象,run()方法也只是普通對象方法的一部分,是主線程的一部分

實現Runnable接口

這是Runnable接口的內容,@FunctionalInterface註解表示函數式接口,和Java8新特性lambda表達式相關,以後再作學習總結。

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}
  • 建立線程的另外一種方法是聲明一個實現Runnable接口的類。
  • 而後,該類實現run方法。而後能夠分配類的實例,
  • 在建立線程時做爲參數傳遞,並啓動它。
//實現Runnable接口
class RDemo implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(i);
        }
    }
}

//建立並啓動線程
Thread t = new Thread(new RDemo());
t.start();

調用public Thread(Runnable target)構造器,將Runnble接口類型對象傳入做爲參數,構建線程對象。
固然還能夠用匿名內部類的形式:

//匿名內部類建立並啓動線程
    new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                System.out.print(i);
            }
        }
    }).start();

實現Callable接口

這是Callable接口的內容:

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

除了上面兩種方法以外,從書上看到還有一種Java5新增的方法,利用Callable接口,官方文檔是這樣描述的:

  • Callable接口相似於Runnable,須要實現接口中的call()方法。可是,Runnable不返回結果,也不能拋出已檢查的異常。
  • Runnable接口提供run()方法支持用戶定義線程的執行體,而Callable中提供call()方法。
    • 擁有返回值
    • 容許拋出異常
  • 經過泛型咱們能夠知道,Callable接口中的形參類型須要和call方法返回值類型相同:

光有Callable接口還不行,畢竟隔了5年纔出來,爲了儘可能避免修改以前的代碼,適應當前環境,Java5還新增了配套的Future接口:

public interface Future<V> {

    //試圖取消Callable中任務的執行,若是任務已經完成、已經被取消、或因其餘緣由沒法被取消,返回false。
    boolean cancel(boolean mayInterruptIfRunning);

    //若是此任務在正常完成以前被取消,則返回true
    boolean isCancelled();

    //若是此任務已完成(正常的終止、異常或取消),則返回true
    boolean isDone();

    //若是須要,則等待計算完成,而後檢索其結果。
    V get() throws InterruptedException, ExecutionException;
    
    //若是須要,將等待最多給定的時間以完成計算,而後檢索其結果。
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

經過繼承關係能夠發現,RunnableFuture接口同時繼承了RunnableFuture接口,意味着實現RunnableFuture接口的類既是Runnable的是實現類,又是Future的實現類。FutureTask就是充當這樣的角色,它的實例能夠做爲target傳入Thread的構造器中。

經過查看源碼,能夠發現FutureTask內部維護了一個Callable的對象,能夠經過下面的這個構造器初始化Callable對象。

public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;       // ensure visibility of callable
    }
  • 用匿名內部類的方式,將實現call()方法的Callable實現類對象做爲參數傳遞給FutureTask的構造器中,構建一個FutureTask類的對象。
FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
        @Override
        public Integer call() throws Exception {
            int i = 0;
            while(i<10){
                System.out.println(Thread.currentThread().getName());
                i++;
            }
            return i;
        }
    });
  • 以後能夠經過Thread類構造器:public Thread(Runnable target, String name)將task對象做爲參數建立新線程並啓動。name參數是能夠自定義線程的名字。
new Thread(task,"name").start();
  • 最後能夠經過task對象調用get()方法獲得call()方法的返回值,須要注意處理拋出的異常。
try {
        System.out.println(task.get());
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }

建立方式的區別

繼承類Thread實現接口(Runnable或Callable)這兩種方式的區別?

  • 前者須要定義子類繼承Thread類,能夠直接經過建立子類對象做爲線程對象,然後者建立的Runnable對象只是線程對象的target。
  • 一樣的,獲取當前對象的方法也不一樣,前者能夠直接使用this獲取當前對象的引用。後者則須要調用Thread的靜態方法currentThread()
    下面是兩個獲取當前線程名的示例:
//繼承Thread
System.out.print(this.getName()+i);
//實現Runnable接口
System.out.print(Thread.currentThread().getName()+i);
  • 前者線程類每建立一個線程都須要建立一個對象,對象之間不能共享實例變量。然後者經過接口的實現類建立的多個線程能夠共享同一個Runnable類型的target,也就是這個線程類的實例變量。

  • 前者定義線程類須要繼承Thread,而Java只支持單繼承,支持接口多實現,顯然在靈活性方面,後者優於前者。


本文做爲我的學習筆記,仍停留在比較淺顯的層面,還須要大量的實踐去感悟併發編程的奧義。

參考資料:《JAVA併發編程實戰》、《瘋狂Java講義》、《JAVA多線程設計模式》

相關文章
相關標籤/搜索