Java中實現多線程的方式

[TOC]

一、繼承Thread類

繼承Thread類,重寫run方法,調用Thread的start()方法啟動線程:

    /**
     * 實現線程方式:1、繼承Thread類
     */
    @Slf4j
    public static class ThreadTarget extends Thread {
        @Override
        public void run() {
            while (true) {
                log.info("Thread extentions: " + System.currentTimeMillis());
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

new一個Thread實例的時候,所有構造方法最終都是調用Thread的init方法, init方法中,設置線程屬性,校驗相關權限:

    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc) {
        /**設置線程名字,匿名線程,名字自動生成:Thread-nextThreadNum()**/
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }
        this.name = name;
        /**設置線程組**/
        Thread parent = currentThread();
        SecurityManager security = System.getSecurityManager();
        if (g == null) {
            if (security != null) {
                g = security.getThreadGroup();
            }
            if (g == null) {
                g = parent.getThreadGroup();
            }
        }
        g.checkAccess(); //檢查當前線程(parent)是否有權限修改線程組g
        if (security != null) { //校驗是否thread子類的getContextClassLoader被覆蓋
            if (isCCLOverridden(getClass())) { //并且擁有enableContextClassLoaderOverride權限
                security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
            }
        }
        g.addUnstarted(); //增加線程組的unstarted線程計數
        this.group = g; //設置線程組
        /**繼承當前線程(parant)的是否后臺線程、設置優先級的值**/
        this.daemon = parent.isDaemon();
        this.priority = parent.getPriority();
        /**設置contextClassLoader和繼承accessControlContext**/
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;
        this.inheritedAccessControlContext =
                acc != null ? acc : AccessController.getContext();
        /**設置Runnable接口的實例,作為target對象**/        
        this.target = target;
        /**這里才是真正設置優先級的地方**/
        setPriority(priority);
        /**繼承當前線程(parent)的ThreadLocal.ThreadLocalMap**/
        if (parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        this.stackSize = stackSize;
        /**設置線程id, 這里設置的是tid,跟nextThreadNum()設置的threadInitNumber是兩個值**/
        tid = nextThreadID();
    }

啟動線程需要調用start方法,方法中調用native方法start0真正啟動線程,
虛擬機會在線程啟動過程中回調Thread的run方法:

    public synchronized void start() {
        /**threadStatus不等于0, 狀態異常,非NEW狀態的線程*/
        if (threadStatus != 0)
            throw new IllegalThreadStateException();
        /**將線程加入到線程組,減線程組的unstarted計數器**/
        group.add(this);
        boolean started = false;
        try {
            /**native方法啟動線程**/
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    /**移出線程組,增加unstarted計數器**/
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /** do nothing. start0的異常會直接拋出 **/
            }
        }
    }
    /**啟動線程的native方法**/
    private native void start0();

  • Thread本身也實現了Runnable接口, 實現run方法,虛擬機啟動時,調用的就是該實現方法。
  • 單純從target看,Thread和Runnable的關系看,這是簡單的模板方法模式的實現。
  • 這也引出了第二種實現方式,實現Runnable接口或者實現/繼承Runnable接口的子接口/子類。
  • 所以繼承Thread實現run和實現Runnable接口實現run方法是有本質區別的,Thread的run是被虛擬機調用的,Runnable的run是作為thread的target屬性的模板方法被調用的。
    @Override
    public void run() {
        /**如果target不為空,調用target的run方法**/
        if (target != null) {
            target.run();
        }
    }

二、實現或繼承Runnable接口的子接口或實現類

實現多線程的另外方式是實現或繼承Runnable接口的子接口或實現類

1、繼承Runnable接口

直接實現Runnable接口的問題是,線程執行完成后,無法獲取到線程執行結果。

        /**
         * 實現線程方式: 2、實現Runnable接口
         */
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    log.info("Runnable implements 1: " + System.currentTimeMillis());
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        log.error(e.getMessage(), e);
                    }
                }
            }
        }); 
        thread2.start();
        /**
         * 實現線程方式: jdk1.8之后,可以更簡單的寫法
         */
        Thread thread3 = new Thread(() -> {
            while (true) {
                log.info("Runnable implements 2: " + System.currentTimeMillis());
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    log.error(e.getMessage(), e);
                }
            }
        }); 
        thread3.start();

2、繼承FutureTask接口

如果需要獲取線程執行結果, 可以實現FutureTask接口, 并通過FutureTask的get方法獲取執行結果:

        /**
         * 實現線程方式: 3、實現Callable接口,配合FutureTask,獲取線程執行結果
         */
        FutureTask<Integer> task = new FutureTask<>(new Fibonacci(10));
        Thread thread4 = new Thread(task);
        thread4.start();
        try {
            Integer result = task.get();
            log.info("FutureTask result: {}" , result);
        } catch (ExecutionException e) {
            log.error(e.getMessage(), e);
        }

FutureTask相關的源碼研究,后續分析。

3、繼承TimerTask接口

如果需要在主線程外執行一些任務的話,可以使用TimerTask接口,配合定時器工具Timer實現,Timer內部維護一個TimerThread線程,用于執行調度任務:

        /**
         * 實現線程方式:4、調度執行任務
         */
        Timer timer = new Timer(); 
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                log.info("TimerTask execute: {}", System.currentTimeMillis());
            }
        }, 5000, 3000);

簡單的應用可以使用Timer,復雜需求應該引入框架。

三、Executor框架

JDK1.5引入Executor異步執行框架,靈活強大,支持多種任務執行策略,將任務提交和執行解耦,可以通過submit和execute提交任務給線程池執行。Executors提供一系列創建線程池的工廠方法。

后續對Executor框架進行分析

        /**
         * 實現多線程的方式: 5、java.util.concurrent包提供的Executor框架,創建線程池,執行任務
         */
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        //通過execute執行實現Runnable接口的任務
        for(int i = 0; i < 100; i++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    log.info("ThreadPool execute task: {} {}", 
                            Thread.currentThread().getName(), 
                            System.currentTimeMillis());
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        log.error(e.getMessage(), e);
                    }
                }
            });
        }
        //也可以通過submit實現Callable接口的任務,以便獲取線程執行結果
        Future<Integer> fiboResult = executorService.submit(new Fibonacci(10)); 
        try {
            log.info("ThreadPool submit task: {}",fiboResult.get());
        } catch (ExecutionException e) {
            log.error(e.getMessage(), e);
        }

四、ForkJoin框架

如果大任務可以分解成小任務并行計算,可以實現RecursiveTask接口,提交到ForJoin框架執行。

    public static void useForkJoinFramework() {
        long start = System.currentTimeMillis();
        ForkJoinPool pool = new ForkJoinPool(); 
        Long result = pool.invoke(createNewTask(0L, 10000000000L, 1000000000L));
        log.info("ForkJoinPool execute result: {} {}", result , (System.currentTimeMillis() - start));
        
        start = System.currentTimeMillis();
        long sum = 0; 
        for(long i = 0;i <= 10000000000L; i++) {
            sum += i; 
        }
        log.info("Simple sum: {} {}", sum, (System.currentTimeMillis() - start));
    }
    
    @SuppressWarnings("serial")
    public static RecursiveTask<Long> createNewTask(final Long start, final Long end, final Long critical){
        return new RecursiveTask<Long>() {
            @Override
            protected Long compute() {
                if(end - start <= critical) {
                    long sum = 0L; 
                    for(long l = start; l <= end; l++) {
                        sum += l; 
                    }
                    return sum; 
                }else {
                    Long middle = (end + start) / 2; 
                    RecursiveTask<Long> left = createNewTask(start, middle, critical);
                    RecursiveTask<Long> right = createNewTask(middle+1, end, critical);
                    left.fork();
                    right.fork();
                    return left.join()+right.join(); 
                }
            }
        };
    }

RecursiveTask接口繼承自ForkJoinTask接口, ForkJoinTask繼承Future接口。

五、總結

Java中多線程編程主要分兩類:

  1. 通過創建Thread實例創建線程和start()方法啟動線程,自己管理線程,執行任務;
  2. 通過Executor框架創建線程池,或者實現相關接口,通過線程池管理管理線程執行任務;
  3. 通過ForkJoin框架并行執行任務。
    不管是通過哪種方式, 都有支持Runnable接口提交任務和支持Callable接口的方式,jdk1.8之后,還可以通過lambda表達式提交任務,
    編程上弱化了Runnable和Callable的區別,所以選用哪種方式去創建線程和管理線程,取決于:
  • 場景是否足夠簡單,只需簡單管理線程,還是需要強大的線程管理功能?
  • 是否需要線程執行結果,可以Future相關的API?
  • 是否可以并行執行?
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370

推薦閱讀更多精彩內容

  • 進程和線程 進程 所有運行中的任務通常對應一個進程,當一個程序進入內存運行時,即變成一個進程.進程是處于運行過程中...
    勝浩_ae28閱讀 5,129評論 0 23
  • 本文是我自己在秋招復習時的讀書筆記,整理的知識點,也是為了防止忘記,尊重勞動成果,轉載注明出處哦!如果你也喜歡,那...
    波波波先森閱讀 11,289評論 4 56
  • 進程和線程 進程 所有運行中的任務通常對應一個進程,當一個程序進入內存運行時,即變成一個進程.進程是處于運行過程中...
    小徐andorid閱讀 2,825評論 3 53
  • 在Java中,使用線程來異步執行任務。Java線程的創建與銷毀需要一定的開銷,如果我們為每一個任務創建一個新線程來...
    Steven1997閱讀 760評論 0 0
  • 最近我和數學老師發現,我們班學生會忽然就表現好了,忽然就明白某件事該怎么做了,比如:我教他們背古詩時拍手,總有一些...
    木魚飛木魚飛閱讀 388評論 0 1