Java併發編程:Java建立線程的三種方式

引言

在平常開發工做中,多線程開發能夠說是必備技能,好的程序員是必定要對線程這塊有深刻了解的,我是Java程序員,而且Java語言自己對於線程開發的支持是很是成熟的,因此今天咱們就來入個門,學一下Java怎麼建立線程。程序員

建立線程的三種方式

Java建立線程主要有三種方式:bash

一、繼承Thread類多線程

二、實現Runnable接口ide

三、使用Callable和Future建立線程ui

下面分別討論這三種方法的實現方式,以及它們之間的對比。this

1、繼承Thread類

步驟:編碼

一、建立一個線程子類繼承Thread類spa

二、重寫run() 方法,把須要線程執行的程序放入run方法,線程啓動後方法裏的程序就會運行線程

二、建立該類的實例,並調用對象的start()方法啓動線程code

示例代碼以下:

public class ThreadDemo extends Thread{
    @Override
    public void run() {
        super.run();
        System.out.println("須要運行的程序。。。。。。。。");
    }

    public static void main(String[] args) {
        Thread thread = new ThreadDemo();
        thread.start();
    }
}
複製代碼

當運行main方法後,程序就會執行run()方法裏面的內容,執行完以後,線程也就隨之消亡,爲何必定要重寫run()方法呢?

點擊方法的源碼後,發現Thread的run()方法其實什麼都沒有作

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

public abstract void run();
複製代碼

若是run()裏沒有須要運行的程序,那麼線程啓動後就直接消亡了。想讓線程作點什麼就必須重寫run()方法。同時,還須要注意的是,線程啓動須要調用start()方法,但直接調用run() 方法也能編譯經過,也能正常運行:

public static void main(String[] args) {
    Thread thread = new ThreadDemo();
    thread.run();
}
複製代碼

只是這樣是普通的方法調用,並無新起一個線程,也就失去了線程自己的意義。

2、實現Runnable接口

一、定義一個線程類實現Runnable接口,並重寫該接口的run()方法,方法中依然是包含指定執行的程序。

二、建立一個Runnable實現類實例,將其做爲target參數傳入,並建立Thread類實例。

三、調用Thread類實例的start()方法啓動線程。

public class RunnableDemo implements Runnable{
    @Override
    public void run() {
        System.out.println("我是Runnable接口......");
    }
    public static void main(String[] args) {

        RunnableDemo demo = new RunnableDemo();
        Thread thread = new Thread(demo);
        thread.start();
    }
}
複製代碼

這是基於接口的方式,比起繼承Thread的方式要靈活不少,但須要多建立一個線程對象,打開源碼能夠發現,當把Runnable實現類的實例做爲參數target傳入後,賦值給當前線程類的target,而run()裏執行的程序就是賦值進去的target的run()方法。

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

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc) {
                      
        ...........這裏省略部分源碼..........
        
        this.target = target;
        setPriority(priority);
        if (parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

        /* Set thread ID */
        tid = nextThreadID();
    }
    
    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }
複製代碼

3、使用Callable和Future建立線程

使用Callable建立線程和Runnable接口方式建立線程比較類似,不一樣的是,Callable接口提供了一個call() 方法做爲線程執行體,而Runnable接口提供的是run()方法,同時,call()方法能夠有返回值,並且須要用FutureTask類來包裝Callable對象。

public interface Callable<V> {
    
    V call() throws Exception;
}
複製代碼

步驟:

一、建立Callable接口的實現類,實現call() 方法

二、建立Callable實現類實例,經過FutureTask類來包裝Callable對象,該對象封裝了Callable對象的call()方法的返回值。

三、將建立的FutureTask對象做爲target參數傳入,建立Thread線程實例並啓動新線程。

四、調用FutureTask對象的get方法獲取返回值。

public class CallableDemo implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        int i = 1;
        return i;
    }

    public static void main(String[] args) {
        CallableDemo demo = new CallableDemo();
        FutureTask<Integer> task = new FutureTask<Integer>(demo);

        new Thread(task).start();
        try {

            System.out.println("task 返回值爲:" + task.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
複製代碼

執行main方法後,程序輸出以下結果:

task 返回值爲:1
複製代碼

說明,task.get()確實返回了call() 方法的結果。那麼其內部是怎麼實現的呢。先打開FutureTask的構造方法,能夠看到其內部是將Callable對象做爲參數傳遞給當前實例的Callable成員,

public FutureTask(Callable<V> callable) {
    if (callable == null)
        throw new NullPointerException();
    this.callable = callable;
    this.state = NEW;       // ensure visibility of callable
}

複製代碼

同時,將成員變量state置爲NEW,當啓動task後,其run方法就會執行Callable的call()方法,

public void run() {
    if (state != NEW ||
        !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                     null, Thread.currentThread()))
        return;
    try {
        Callable<V> c = callable;
        if (c != null && state == NEW) {
            V result;
            boolean ran;
            try {
            	//把call()的返回結果複製給result
                result = c.call();
                ran = true;
            } catch (Throwable ex) {
                result = null;
                ran = false;
                setException(ex);
            }
            if (ran)
            	//將結果設置給其餘變量
                set(result);
        }
    } finally {
        // runner must be non-null until state is settled to
        // prevent concurrent calls to run()
        runner = null;
        // state must be re-read after nulling runner to prevent
        // leaked interrupts
        int s = state;
        if (s >= INTERRUPTING)
            handlePossibleCancellationInterrupt(s);
    }
}
protected void set(V v) {
        if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        	//把傳過來的值賦值給outcome成員
            outcome = v;
            UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
            finishCompletion();
        }
    }
複製代碼

run()方法中通過一系列的程序運行後,把call()的返回結果賦值給了outcome,而後當調用task.get()方法裏獲取的就是outcome的值了,這樣一來,也就瓜熟蒂落的獲得了返回結果。

public V get() throws InterruptedException, ExecutionException {
    int s = state;
    if (s <= COMPLETING)
        s = awaitDone(false, 0L);
    return report(s);
}
private V report(int s) throws ExecutionException {
        Object x = outcome;
        if (s == NORMAL)
        	//返回outcome的值
            return (V)x;
        if (s >= CANCELLED)
            throw new CancellationException();
        throw new ExecutionException((Throwable)x);
    }
複製代碼

能夠看出,源碼的運行邏輯仍是比較清晰的,代碼也比較容易理解,因此,我比較建議讀者們有空能夠多看看Java底層的源碼,這樣能幫助咱們深刻的理解功能是怎麼實現的。

三種方式的對比

好了,建立線程的三種方式實例都說完了,接下來講下他們的對比。

從實現方式來講,使用Runnable接口和Callable接口的方式基本相同,區分的只是Callable實現的方法體能夠有返回值,而繼承Thread類是使用繼承方式,因此,其實三種方法歸爲兩類來分析便可。

一、使用繼承Thread類的方式:

  • 優點:編碼簡單,而且,當須要獲取當前線程,能夠直接用this
  • 劣勢:因爲Java支持單繼承,因此繼承Thread後就不能繼承其餘父類

二、使用Runnable接口和Callable接口的方式:

  • 優點:

比較靈活,線程只是實現接口,還能夠繼承其餘父類。

這種方式下,多個線程能夠共享一個target對象,很是適合多線程處理同一份資源的情形。

Callable接口的方式還能獲取返回值。

  • 劣勢:

編碼稍微複雜了點,須要建立更多對象。

若是想訪問當前線程,須要用Thread.currentThread()方法。

總的來講,兩種分類都有各自的優劣勢,但其實後者的劣勢相對優點來講不值一提,通常狀況下,仍是建議直接用接口的方式來建立線程,畢竟單一繼承的劣勢仍是比較大的。

相關文章
相關標籤/搜索