多線程篇 (一)基礎篇

多線程篇 (一)基礎篇

把學習當糖喫~會發現學習是一件很快樂的事情

目錄

多線程學習 (一)基礎篇

多線程的基本概念

1 Thread類和Runnable接口

1.1 繼承Thread類

1.2 實現Runnable接口

1.3 Thread類構造方法

1.4 Thread類的幾個常用方法

1.5 Thread類與Runnable接口的比較

2 Callable、Future與FutureTask

2.1 Callable接口

2.2 Future接口

2.3 FutureTask類

注:


多線程的基本概念

多線程:  嗯哼,說你呢是不是 一邊聽歌,一邊扣腳,一邊閱讀着文章,甚至還不忘記筆記~

單線程:   當然你也可以 先扣完腳,然後聽完歌,再去閱讀文章,完了最後記筆記~

1 Thread類和Runnable接口

如何使用多線程的呢?

  • 繼承Thread類,並重寫run方法;
  • 實現Runnable接口的run方法;

1.1 繼承Thread類

先學會怎麼用,再學原理。首先我們來看看怎麼用ThreadRunnable來寫一個Java多線程程序。

首先是繼承Thread類:

public class Demo {
    public static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("MyThread");
        }
    }

    public static void main(String[] args) {
        Thread myThread = new MyThread();
        myThread.start();
    }
}

注意要調用start()方法後,該線程纔算啓動!

我們在程序裏面調用了start()方法後,虛擬機會先爲我們創建一個線程,然後等到這個線程第一次得到時間片時再調用run()方法。

注意不可多次調用start()方法。在第一次調用start()方法後,再次調用start()方法會拋出異常。

1.2 實現Runnable接口

接着我們來看一下Runnable接口(JDK 1.8 +):

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

可以看到Runnable是一個函數式接口,這意味着我們可以使用Java 8的函數式編程來簡化代碼。

示例代碼:

public class Demo {
    public static class MyThread implements Runnable {
        @Override
        public void run() {
            System.out.println("MyThread");
        }
    }

    public static void main(String[] args) {

        new Thread(new MyThread()).start();

        // Java 8 函數式編程,可以省略MyThread類
        new Thread(() -> {
            System.out.println("Java 8 匿名內部類");
        }).start();
    }
}

1.3 Thread類構造方法

Thread類是一個Runnable接口的實現類,我們來看看Thread類的源碼。

查看Thread類的構造方法,發現其實是簡單調用一個私有的init方法來實現初始化。init的方法簽名:

// Thread類源碼 

// 片段1 - init方法
private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals)

// 片段2 - 構造函數調用init方法
public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
}

// 片段3 - 使用在init方法裏初始化AccessControlContext類型的私有屬性
this.inheritedAccessControlContext = 
    acc != null ? acc : AccessController.getContext();

// 片段4 - 兩個對用於支持ThreadLocal的私有屬性
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

init方法的參數:

  • g:線程組,指定這個線程是在哪個線程組下;

  • target:指定要執行的任務;

  • name:線程的名字,多個線程的名字是可以重複的。如果不指定名字,見片段2;

  • acc:見片段3,用於初始化私有變量inheritedAccessControlContext

    這個變量有點神奇。它是一個私有變量,但是在Thread類裏只有init方法對它進行初始化,在exit方法把它設爲null。其它沒有任何地方使用它。一般我們是不會使用它的,那什麼時候會使用到這個變量呢?可以參考這個stackoverflow的問題:Restrict permissions to threads which execute third party software

  • inheritThreadLocals:可繼承的ThreadLocal,見片段4,Thread類裏面有兩個私有屬性來支持ThreadLocal,我們會在後面的章節介紹ThreadLocal的概念。

實際情況下,我們大多是直接調用下面兩個構造方法:

Thread(Runnable target)
Thread(Runnable target, String name)

1.4 Thread類的幾個常用方法

這裏介紹一下Thread類的幾個常用的方法:

  • currentThread():靜態方法,返回對當前正在執行的線程對象的引用;
  • start():開始執行線程的方法,java虛擬機會調用線程內的run()方法;
  • yield():yield在英語裏有放棄的意思,同樣,這裏的yield()指的是當前線程願意讓出對當前處理器的佔用。這裏需要注意的是,就算當前線程調用了yield()方法,程序在調度的時候,也還有可能繼續運行這個線程的;
  • sleep():靜態方法,使當前線程睡眠一段時間;
  • join():使當前線程等待另一個線程執行完畢之後再繼續執行,內部調用的是Object類的wait方法實現的;

1.5 Thread類與Runnable接口的比較:

實現一個自定義的線程類,可以有繼承Thread類或者實現Runnable接口這兩種方式,它們之間有什麼優劣呢?

  • 由於Java“單繼承,多實現”的特性,Runnable接口使用起來比Thread更靈活。
  • Runnable接口出現更符合面向對象,將線程單獨進行對象的封裝。
  • Runnable接口出現,降低了線程對象和線程任務的耦合性。
  • 如果使用線程時不需要使用Thread類的諸多方法,顯然使用Runnable接口更爲輕量。

所以,我們通常優先使用“實現Runnable接口”這種方式來自定義線程類。

2 Callable、Future與FutureTask

通常來說,我們使用RunnableThread來創建一個新的線程。但是它們有一個弊端,就是run方法是沒有返回值的。而有時候我們希望開啓一個線程去執行一個任務,並且這個任務執行完成後有一個返回值。

JDK提供了Callable接口與Future接口爲我們解決這個問題,這也是所謂的“異步”模型。

2.1 Callable接口

CallableRunnable類似,同樣是只有一個抽象方法的函數式接口。不同的是,Callable提供的方法是有返回值的,而且支持泛型

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

那一般是怎麼使用Callable的呢?Callable一般是配合線程池工具ExecutorService來使用的。我們會在後續章節解釋線程池的使用。這裏只介紹ExecutorService可以使用submit方法來讓一個Callable接口執行。它會返回一個Future,我們後續的程序可以通過這個Futureget方法得到結果。

這裏可以看一個簡單的使用demo:

// 自定義Callable
class Task implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
        // 模擬計算需要一秒
        Thread.sleep(1000);
        return 2;
    }
    public static void main(String args[]){
        // 使用
        ExecutorService executor = Executors.newCachedThreadPool();
        Task task = new Task();
        Future<Integer> result = executor.submit(task);
        // 注意調用get方法會阻塞當前線程,直到得到結果。
        // 所以實際編碼中建議使用可以設置超時時間的重載get方法。
        System.out.println(result.get()); 
    }
}

輸出結果:

2

2.2 Future接口

Future接口只有幾個比較簡單的方法:

public abstract interface Future<V> {
    public abstract boolean cancel(boolean paramBoolean);
    public abstract boolean isCancelled();
    public abstract boolean isDone();
    public abstract V get() throws InterruptedException, ExecutionException;
    public abstract V get(long paramLong, TimeUnit paramTimeUnit)
            throws InterruptedException, ExecutionException, TimeoutException;
}

cancel方法是試圖取消一個線程的執行。

注意是試圖取消,並不一定能取消成功。因爲任務可能已完成、已取消、或者一些其它因素不能取消,存在取消失敗的可能。boolean類型的返回值是“是否取消成功”的意思。參數paramBoolean表示是否採用中斷的方式取消線程執行。

所以有時候,爲了讓任務有能夠取消的功能,就使用Callable來代替Runnable。如果爲了可取消性而使用 Future但又不提供可用的結果,則可以聲明 Future<?>形式類型、並返回 null作爲底層任務的結果。

2.3 FutureTask類

上面介紹了Future接口。這個接口有一個實現類叫FutureTaskFutureTask是實現的RunnableFuture接口的,而RunnableFuture接口同時繼承了Runnable接口和Future接口:

public interface RunnableFuture<V> extends Runnable, Future<V> {
    /**
     * Sets this Future to the result of its computation
     * unless it has been cancelled.
     */
    void run();
}

FutureTask類有什麼用?爲什麼要有一個FutureTask類?前面說到了Future只是一個接口,而它裏面的cancelgetisDone等方法要自己實現起來都是非常複雜的。所以JDK提供了一個FutureTask類來供我們使用。

示例代碼:

// 自定義Callable,與上面一樣
class Task implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
        // 模擬計算需要一秒
        Thread.sleep(1000);
        return 2;
    }
    public static void main(String args[]){
        // 使用
        ExecutorService executor = Executors.newCachedThreadPool();
        FutureTask<Integer> futureTask = new FutureTask<>(new Task());
        executor.submit(futureTask);
        System.out.println(futureTask.get());
    }
}

使用上與第一個Demo有一點小的區別。首先,調用submit方法是沒有返回值的。這裏實際上是調用的submit(Runnable task)方法,而上面的Demo,調用的是submit(Callable<T> task)方法。

然後,這裏是使用FutureTask直接取get取值,而上面的Demo是通過submit方法返回的Future去取值。

在很多高併發的環境下,有可能Callable和FutureTask會創建多次。FutureTask能夠在高併發環境下確保任務只執行一次。這塊有興趣的同學可以參看FutureTask源碼。

注:

部分摘抄 深入淺出Java多線程, 這個系列目前一共20章~我已經看完了 ~側重點在解析原理,也貼心的補了簡單的案例以及圖文,

在實戰及案例一塊就需要自己去實踐了~ 得是真的很棒

原文簡介:

站在巨人的肩上,我們可以看得更遠。本書內容的主要來源有博客、書籍、論文,對於一些已經敘述得很清晰的知識點我們直接引用在本書中;對於一些沒有講解清楚的知識點,我們加以畫圖或者編寫Demo進行加工;而對於一些模棱兩可的知識點,本書在查閱了大量資料的情況下,給出最合理的解釋。

算是記錄一下自己的學習, 把好東西分享給更多人,素質三連😄

深入淺出Java多線程 

深入淺出Java多線程  

深入淺出Java多線程

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章