Java並發編程:Java創建線程的三種方式


Java並發編程:Java創建線程的三種方式

目錄

引言

在日常開發工作中,多線程開發可以說是必備技能,好的程序員是一定要對線程這塊有深入了解的,我是Java程序員,並且Java語言本身對於線程開發的支持是非常成熟的,所以今天我們就來入個門,學一下Java怎么創建線程。

創建線程的三種方式

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

1、繼承Thread類

2、實現Runnable接口

3、使用Callable和Future創建線程

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

一、繼承Thread類

步驟:

1、創建一個線程子類繼承Thread類

2、重寫run() 方法,把需要線程執行的程序放入run方法,線程啟動后方法里的程序就會運行

2、創建該類的實例,並調用對象的start()方法啟動線程

示例代碼如下:

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();
}

只是這樣是普通的方法調用,並沒有新起一個線程,也就失去了線程本身的意義。

二、實現Runnable接口

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

2、創建一個Runnable實現類實例,將其作為target參數傳入,並創建Thread類實例。

3、調用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();
        }
    }

三、使用Callable和Future創建線程

使用Callable創建線程和Runnable接口方式創建線程比較相似,不同的是,Callable接口提供了一個call() 方法作為線程執行體,而Runnable接口提供的是run()方法,同時,call()方法可以有返回值,而且需要用FutureTask類來包裝Callable對象。

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

步驟:

1、創建Callable接口的實現類,實現call() 方法

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

3、將創建的FutureTask對象作為target參數傳入,創建Thread線程實例並啟動新線程。

4、調用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類是使用繼承方式,所以,其實三種方法歸為兩類來分析即可。

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

  • 優勢:編碼簡單,並且,當需要獲取當前線程,可以直接用this
  • 劣勢:由於Java支持單繼承,所以繼承Thread后就不能繼承其他父類

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

  • 優勢:

比較靈活,線程只是實現接口,還可以繼承其他父類。

這種方式下,多個線程可以共享一個target對象,非常適合多線程處理同一份資源的情形。

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

  • 劣勢:

編碼稍微復雜了點,需要創建更多對象。

如果想訪問當前線程,需要用Thread.currentThread()方法。

總的來說,兩種分類都有各自的優劣勢,但其實后者的劣勢相對優勢來說不值一提,一般情況下,還是建議直接用接口的方式來創建線程,畢竟單一繼承的劣勢還是比較大的。







免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM