java建立線程的兩種方式及源碼解析

建立線程的方式有不少種,下面咱們就最基本的兩種方式進行說明。主要先介紹使用方式,再從源碼角度進行解析。java

  • 繼承Thread類的方式
  • 實現Runnable接口的方式

這兩種方式是最基本的建立線程的方式,其實核心也就是Thread類,後面分析源碼會講到,下面先介紹使用方式。編程

一:繼承Thread類的方式建立線程


1,建立線程步驟設計模式

    • 建立一個子類繼承於Thread類
    • 子類重寫Thread類的run方法,方法內實現子線程要完成的功能
    • 建立一個子類的對象
    • 調用子類對象的start()的方法,該方法有兩個做用:啓動此線程;調用重寫的run方法。

代碼以下:多線程

package com.yefengyu.thread;

//1,建立一個子類繼承於Thread類 
public class SubThread extends Thread {

    //2,子類重寫Thread類的run方法,方法內實現子線程要完成的功能 
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("Thread ...." + i);
        }
    }
}
package com.yefengyu.thread;


public class TestThread {

    public static void main(String[] args) {
        //3,建立一個子類的對象。
        SubThread subThread = new SubThread();
        //4,調用線程的start()的方法。該方法有兩個做用:啓動此線程;調用響應的run方法。不能顯示調用run方法,由於這樣不能開啓線程
        subThread.start();

        for (int i = 0; i < 5; i++) {
            System.out.println("main ...." + i);
        }
    }
}

每次執行結果都不相同,這是由於多個線程都在獲取cpu的執行權,cpu執行到誰,就執行誰。可是要明確一點,在某個時刻,單個cpu只能執行一個線程,cpu進行着快速切換,已達到看上去是並行執行的效果,這就是線程的一個特色:隨機性,哪一個線程搶到cpu資源,就執行該線程,至於執行多長時間,是cpu說了算。 ide

main ....0
Thread ....0
Thread ....1
Thread ....2
main ....1
main ....2
main ....3
main ....4
Thread ....3
Thread ....4

2,設置與獲取線程名稱函數

因爲默認的線程名稱沒有可讀性,所以設置一個線程名稱仍是比較重要的,Thread類有個構造方法:源碼分析

    public Thread(String name) {
        init(null, null, name, 0);
    }

所以子類增長線程名稱則比較簡單:學習

package com.yefengyu.thread;

//1,建立一個子類繼承於Thread類
public class SubThread extends Thread {

    //經過構造函數設置線程名稱,固然使用set方法也能夠
    public SubThread(String name) {
        super(name);
    }

    //2,子類重寫Thread類的run方法,方法內實現子線程要完成的功能
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            //下面兩種方法都會獲取線程名稱
            System.out.println(this.getName()+" ... " + i);
            System.out.println(Thread.currentThread().getName()+" *** " + i);
        }
    }
}
package com.yefengyu.thread;


public class TestThread {

    public static void main(String[] args) {
        //(3)建立一個子類的對象。
        SubThread subThread1 = new SubThread("my-thread11111");
        SubThread subThread2 = new SubThread("my-thread22222");
        //(4)調用線程的start()的方法。該方法有兩個做用:啓動此線程;調用響應的run方法。不能顯示調用run方法,由於這樣不能開啓線程
        subThread1.start();
        subThread2.start();

        for (int i = 0; i < 5; i++) {
            System.out.println("main ...." + i);
        }
    }
}

結果:this

main ....0
my-thread22222 ... 0
my-thread11111 ... 0
my-thread22222 *** 0
my-thread22222 ... 1
my-thread22222 *** 1
main ....1
my-thread22222 ... 2
my-thread11111 *** 0
my-thread22222 *** 2
main ....2
main ....3
main ....4
my-thread22222 ... 3
my-thread11111 ... 1
my-thread11111 *** 1
my-thread11111 ... 2
my-thread22222 *** 3
my-thread22222 ... 4
my-thread22222 *** 4
my-thread11111 *** 2
my-thread11111 ... 3
my-thread11111 *** 3
my-thread11111 ... 4
my-thread11111 *** 4

3,實戰:汽車票買票程序spa

假如車站有3張票,三個窗口,多線程如何賣票?

package com.yefengyu.thread;

public class SubThread extends Thread {

    //票的總數
    private int ticket = 3;

    public SubThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        while (true) {
            if (ticket > 0) {
                System.out.println(Thread.currentThread().getName() + "賣票 " + ticket--);
            }
        }
    }
}
package com.yefengyu.thread;


public class TestThread {

    public static void main(String[] args) {
        //三個線程模擬三個窗口同時賣票
        SubThread subThread1 = new SubThread("my-thread11111");
        SubThread subThread2 = new SubThread("my-thread22222");
        SubThread subThread3 = new SubThread("my-thread33333");
        subThread1.start();
        subThread2.start();
        subThread3.start();
    }
}

結果和咱們想的大不相同,居然每一個線程都賣了3張票,一共賣了9張票,這是不能夠忍受的。

my-thread11111賣票 3
my-thread11111賣票 2
my-thread11111賣票 1
my-thread33333賣票 3
my-thread22222賣票 3
my-thread33333賣票 2
my-thread22222賣票 2
my-thread33333賣票 1
my-thread22222賣票 1

修改1:使用靜態變量:

//票的總數
private static int ticket = 3;

定義靜態能夠解決賣出多餘票的狀況,可是這種變量通常不定義靜態的,由於靜態屬性生命週期太長。

修改2:new一個SubThread實例,屢次啓動

package com.yefengyu.thread;

public class TestThread {
    public static void main(String[] args) {
       //一個線程對象屢次啓動
       SubThread subThread1 = new SubThread("my-thread11111");
       subThread1.start();
       subThread1.start();
       subThread1.start();
    }
}

出現異常:

my-thread11111賣票 3Exception in thread "main" 
my-thread11111賣票 2
my-thread11111賣票 1
java.lang.IllegalThreadStateException
    at java.lang.Thread.start(Thread.java:708)
    at com.yefengyu.thread.TestThread.main(TestThread.java:8)

源碼這樣說:線程不是NEW狀態是不能夠調用start方法的,調用會報異常,也就是一個線程啓動以後不能再啓動。關於線程狀態後面博文會講到。

/**
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
    throw new IllegalThreadStateException();

如何解決賣票程序中的問題呢?使用Runnable接口。

二:實現Runnable接口的方式建立多線程


1,建立線程步驟

    • 編寫一個類實現Runnable接口
    • 該類實現run方法
    • 建立該類的對象
    • 在建立 Thread 時做爲一個參數來傳遞並啓動

2,代碼演示

package com.yefengyu.thread;

public class SubThread implements Runnable {

    //票的總數
    private int ticket = 3;

    @Override
    public void run() {
        while (true) {
            if (ticket > 0) {
                System.out.println(Thread.currentThread().getName() + "賣票 " + ticket--);
            }
        }
    }
}
package com.yefengyu.thread;

public class TestThread {
    public static void main(String[] args) {
        
        //建立該類的對象
       SubThread subThread = new SubThread();

        //在建立 Thread 時做爲一個參數來傳遞並啓動
       new Thread(subThread, "線程1").start();
       new Thread(subThread, "線程2").start();
       new Thread(subThread, "線程3").start();
    }
}

運行結果以下,是咱們想要的結果。

線程1賣票 3
線程3賣票 2
線程2賣票 1

      稍微研究一下,第一種繼承Thread類的方式,new了3次SubThread實例,那麼實例的變量ticket也有三個,線程分別擁有各自的ticket。而實現Runnable接口的方式,數據只存在與SubThread這個實例對象中,代碼中只需new一次,所以只有一份ticket數據,而將持有這份數據的對象經過構造方法傳入到多個線程中的時候,線程對象只是執行的載體,真實數據只有一份,所以Runnable接口的實現方式適合多個相同的程序代碼的線程去處理同一個資源。

3,本節小總結

  • 採用繼承Thread類方式 
    • 優勢:編寫簡單,若是須要訪問當前線程,無需使用Thread.currentThread()方法,直接使用this,便可得到當前線程。
    • 缺點:由於線程類已經繼承了Thread類,因此不能再繼承其餘的父類
  • 採用實現Runnable接口方式: 
    • 優勢:線程類只是實現了Runable接口,還能夠繼承其餘的類。能夠多個線程共享同一個目標對象,因此很是適合多個相同線程來處理同一份資源的狀況。
    • 缺點:編程稍微複雜,若是須要訪問當前線程,必須使用Thread.currentThread()方法。

三:源碼分析


1,理論分析

多線程是java開發中必不可少的一項技術點,下面主要研究Thread類,經過分析該類瞭解多線程執行的過程,爲之後的線程池等高級技術打下堅實基礎。

當咱們使用多線程進行開發的時候,最開始學習的例子就是使用Thread類。使用步驟以下,和上面演示的對比,簡化了步驟:

    • 編寫一個類,繼承Thread

    • 重寫run方法

    • 經過調用start方法啓動線程。

後來又有一種方法,實現Runnable接口,主要步驟以下:

    • 編寫一個類,實現Runnable 接口,重寫run方法

    • 將該類的對象傳入Thread類中

    • 經過調用start方法啓動線程。

經過上面,咱們來分析一下,線程執行離不開Thread類,最後都要使用start方法啓動線程。咱們能夠想到start方法是一個入口方法,它能夠作不少事。假如start方法作了以下的事情:

    • do x

    • do y

    • do run

    • do z

咱們不關心x、y、z具體是什麼,只要明白start方法是一個入口方法,它作了不少事情,可是在某一步,它調用了 run 方法。而run方法是咱們必須實現的,也就是咱們本身實現的邏輯在start裏面被執行了。接着咱們考慮下 run 方法,怎麼樣才能本身定義run方法,而後在run方法裏面寫本身的邏輯?

  • 一種方法是,在Thread類裏面,咱們定義一個抽象方法 run,這個時候,必須有子類來實現。這是模板設計模式思想。

  • 另外一種方法是提供一個接口,而且接口中有個方法 run,Thread類持有這個接口(經過屬性持有,再經過構造器傳入),而且Thread類也有個run方法(爲啥也要有個run方法後面會提到),該run方法調用接口的run方法。此時只要編寫一個類實現接口,重寫run方法,並傳入Thread類,那麼Thread類在執行start方法的時候,會調用自身的run方法,該run方法又會調用接口實現的run方法,這是策略設計模式思想。

以上兩種模式就是實現Thread類和實現Runnable接口的實現原理。須要注意的是,Thread類自己也實現了Runnable接口,那麼Thread類自己擁有run方法則水到渠成。

2,源碼分析

Runnable的源碼很簡單:

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

Thread類的屬性很是多,咱們暫時無論,注意有一個屬性,它是對Runnable接口的引用。

private Runnable target;

有了屬性,咱們須要看如何傳入這個屬性值:

public Thread() {
     init(null, null, "Thread-" + nextThreadNum(), 0);
}
public Thread(Runnable target) {
     init(null, target, "Thread-" + nextThreadNum(), 0);
}

init方法內容不少,可是關於target變量,只有一句:

this.target = target;

所以能夠判斷,上面兩個構造器,第一個沒有給Runnable賦值,值爲null,第二個經過參數進行賦值。

 

咱們再看下Thread類:

public class Thread implements Runnable

Thread類實現了Runnable接口,所以必須實現run方法:

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

對於這個run方法,若是是使用繼承Thread類重寫run方法;那麼這裏面的內容將會被覆蓋,若是是實現Runnable接口重寫run方法,那麼此處就會調用接口實現的run方法。這就讓兩種實現多線程的方式得以共存。

這個run方法什麼時候調用?在start方法中:

public synchronized void start() {

        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {

            }
        }
    }

private native void start0();

注意start方法中的start0方法和最後一行start0方法。該start0方法是native方法,實質是調用run方法,此處暫時不作詳解。

Thread類使用模板設計模式,模板方法是start,start方法裏面的start0方法才能真正啓動線程、調用了Thread類的run方法。

3,Runnable接口的好處:

  • 適合多個相同的程序代碼的線程去處理同一個資源
  • 能夠避免java中的單繼承的限制
  • 增長程序的健壯性,代碼能夠被多個線程共享,代碼和數據獨立
  • 線程池只能放入實現Runable或callable類線程,不能直接放入繼承Thread的類
相關文章
相關標籤/搜索