實現多線程的幾種方式

版權聲明:本文爲博主原創文章,未經博主容許不得轉載。 https://blog.csdn.net/king_kgh/article/details/78213576

多線程的形式上實現方式主要有兩種,一種是繼承Thread類,一種是實現Runnable接口。本質上實現方式都是來實現線程任務,而後啓動線程執行線程任務(這裏的線程任務實際上就是run方法)。這裏所說的6種,實際上都是在以上兩種的基礎上的一些變形。java

下面分別就這6中實現方式一一介紹。數據庫

第一種方式:繼承Thread類

萬物皆對象,那麼線程也是對象,對象就應該可以抽取其公共特性封裝成爲類,使用類能夠實例化多個對象,那麼實現線程的第一種方式就是繼承Thread類。繼承Thread類是最簡單的一種實現線程的方式,經過JDK提供的Thread類,重寫Thread類的run方法便可,那麼當線程啓動的時候,就會執行run方法體的內容。代碼以下:設計模式

package com.kingh.thread.create;

/** * 繼承Thread類的方式建立線程 * * @author <a href="https://blog.csdn.net/king_kgh>Kingh</a> * @version 1.0 * @date 2019/3/13 19:19 */
public class CreateThreadDemo1 extends Thread {

    public CreateThreadDemo1() {
        // 設置當前線程的名字
        this.setName("MyThread");
    }

    @Override
    public void run() {
        // 每隔1s中輸出一次當前線程的名字
        while (true) {
            // 輸出線程的名字,與主線程名稱相區分
            printThreadInfo();
            try {
                // 線程休眠一秒
                Thread.sleep(1000);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }

    public static void main(String[] args) throws Exception {
        // 注意這裏,要調用start方法才能啓動線程,不能調用run方法
        new CreateThreadDemo1().start();

        // 演示主線程繼續向下執行
        while (true) {
            printThreadInfo();
            Thread.sleep(1000);
        }
    }

    /** * 輸出當前線程的信息 */
    private static void printThreadInfo() {
        System.out.println("當前運行的線程名爲: " + Thread.currentThread().getName());
    }
}

運行結果以下緩存

當前運行的線程名爲: main
當前運行的線程名爲: MyThread
當前運行的線程名爲: main
當前運行的線程名爲: MyThread
當前運行的線程名爲: MyThread
當前運行的線程名爲: main

這裏要注意,在啓動線程的時候,並非調用線程類的run方法,而是調用了線程類的start方法。那麼咱們能不能調用run方法呢?答案是確定的,由於run方法是一個public聲明的方法,所以咱們是能夠調用的,可是若是咱們調用了run方法,那麼這個方法將會做爲一個普通的方法被調用,並不會開啓線程。這裏其實是採用了設計模式中的模板方法模式,Thread類做爲模板,而run方法是在變化的,所以放到子類來實現。markdown

1. 建立多個線程

上面的例子中除了咱們建立的一個線程之外其實還有一個主線程也在執行。那麼除了這兩個線程之外還有沒有其餘的線程在執行了呢,實際上是有的,好比咱們看不到的垃圾回收線程,也在默默的執行。這裏咱們並不去考慮有多少個線程在執行,上面咱們本身建立了一個線程,那麼能不能多建立幾個一塊兒執行呢,答案是確定的。一個Thread類就是一個線程對象,那麼多建立幾個Thread類,並調用其start方法就能夠啓動多個線程了。代碼以下多線程

package com.kingh.thread.create;

/** * 建立多個線程同時執行 * * @author <a href="https://blog.csdn.net/king_kgh>Kingh</a> * @version 1.0 * @date 2019/3/18 9:46 */
public class CreateMultiThreadDemo2 extends Thread {

    public CreateMultiThreadDemo2(String name) {
        // 設置當前線程的名字
        this.setName(name);
    }

    @Override
    public void run() {
        // 每隔1s中輸出一次當前線程的名字
        while (true) {
            // 輸出線程的名字,與主線程名稱相區分
            printThreadInfo();
            try {
                // 線程休眠一秒
                Thread.sleep(1000);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }

    public static void main(String[] args) throws Exception {
        // 注意這裏,要調用start方法才能啓動線程,不能調用run方法
        new CreateMultiThreadDemo2("MyThread-01").start();

        // 建立多個線程實例,同時執行
        new CreateMultiThreadDemo2("MyThread-02").start();

        // 演示主線程繼續向下執行
        while (true) {
            printThreadInfo();
            Thread.sleep(1000);
        }
    }

    /** * 輸出當前線程的信息 */
    private static void printThreadInfo() {
        System.out.println("當前運行的線程名爲: " + Thread.currentThread().getName());
    }
}

運行結果以下app

當前運行的線程名爲: main
當前運行的線程名爲: MyThread-02
當前運行的線程名爲: MyThread-01
當前運行的線程名爲: main
當前運行的線程名爲: MyThread-01
當前運行的線程名爲: MyThread-02
當前運行的線程名爲: main

2. 指定線程名稱

能夠看到,經過建立多個Thread類,而且調用其start方法,啓動了多個線程。每一個線程都有本身的名字,在上述代碼中,分別給建立的線程指定了MyThread-01和MyThread-02這個名字,而後構造方法中經過調用父類的setName方法給線程名字賦值。若是不指定線程名字,系統會默認指定線程名,命名規則是Thread-N的形式。可是爲了排查問題方便,建議在建立線程的時候指定一個合理的線程名字。下面的代碼是不使用線程名的樣子框架

package com.kingh.thread.create;

/** * 建立多個線程同時執行,使用系統默認線程名 * * @author <a href="https://blog.csdn.net/king_kgh>Kingh</a> * @version 1.0 * @date 2019/3/18 9:46 */
public class CreateMultiThreadDemo3 extends Thread {

    @Override
    public void run() {
        // 每隔1s中輸出一次當前線程的名字
        while (true) {
            // 輸出線程的名字,與主線程名稱相區分
            printThreadInfo();
            try {
                // 線程休眠一秒
                Thread.sleep(1000);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }

    public static void main(String[] args) throws Exception {
        // 注意這裏,要調用start方法才能啓動線程,不能調用run方法
        new CreateMultiThreadDemo3().start();

        // 建立多個線程實例,同時執行
        new CreateMultiThreadDemo3().start();

        // 演示主線程繼續向下執行
        while (true) {
            printThreadInfo();
            Thread.sleep(1000);
        }
    }

    /** * 輸出當前線程的信息 */
    private static void printThreadInfo() {
        System.out.println("當前運行的線程名爲: " + Thread.currentThread().getName());
    }
}

運行的結果以下:less

當前運行的線程名爲: main
當前運行的線程名爲: Thread-1
當前運行的線程名爲: Thread-0
當前運行的線程名爲: main
當前運行的線程名爲: Thread-1
當前運行的線程名爲: Thread-0

第二種方式:實現Runnable接口

實現Runnable接口也是一種常見的建立線程的方式,使用接口的方式可讓咱們的程序下降耦合度。Runnable接口中僅僅定義了一個方法,就是run。咱們來看一下Runnable接口的代碼。異步

package java.lang;

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

其實Runnable就是一個線程任務,線程任務和線程的控制分離,這也就是上面所說的解耦。咱們要實現一個線程,能夠藉助Thread類,Thread類要執行的任務就能夠由實現了Runnable接口的類來處理。 這就是Runnable的精髓之所在!

Runnable 是一個@FunctionalInterface 函數式接口,也就意味了能夠利用JDK8提供的lambda的方式來建立線程任務,後面的代碼中會給讀者演示具體如何使用。

使用Runnable實現上面的例子步驟以下:

  • 定義一個類實現Runnable接口,做爲線程任務類
  • 重寫run方法,並實現方法體,方法體的代碼就是線程所執行的代碼
  • 定義一個能夠運行的類,並在main方法中建立線程任務類
  • 建立Thread類,並將線程任務類作爲Thread類的構造方法傳入
  • 啓動線程

1. 建立線程任務

線程任務就是線程要作的事情,這裏咱們讓這個線程每隔1s中打印本身的名字

package com.kingh.thread.create;

/** * 線程任務 * * @author <a href="https://blog.csdn.net/king_kgh>Kingh</a> * @version 1.0 * @date 2019/3/18 10:04 */
public class CreateThreadDemo4_Task implements Runnable {

    @Override
    public void run() {
		// 每隔1s中輸出一次當前線程的名字
        while (true) {
            // 輸出線程的名字,與主線程名稱相區分
            printThreadInfo();
            try {
                // 線程休眠一秒
                Thread.sleep(1000);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }

    /** * 輸出當前線程的信息 */
    private static void printThreadInfo() {
        System.out.println("當前運行的線程名爲: " + Thread.currentThread().getName());
    }
}

2. 建立可運行類

在這裏建立線程,並把任務交給線程處理,而後啓動線程。

package com.kingh.thread.create;

/** * 建立線程 * * @author <a href="https://blog.csdn.net/king_kgh>Kingh</a> * @version 1.0 * @date 2019/3/18 10:04 */
public class CreateThreadDemo4_Main {

    public static void main(String[] args) throws Exception {
        // 實例化線程任務類
        CreateThreadDemo4_Task task = new CreateThreadDemo4_Task();

        // 建立線程對象,並將線程任務類做爲構造方法參數傳入
        new Thread(task).start();

        // 主線程的任務,爲了演示多個線程一塊兒執行
        while (true) {
            printThreadInfo();
            Thread.sleep(1000);
        }
    }

    /** * 輸出當前線程的信息 */
    private static void printThreadInfo() {
        System.out.println("當前運行的線程名爲: " + Thread.currentThread().getName());
    }
}

線程任務和線程的控制分離,那麼一個線程任務能夠提交給多個線程來執行。這是頗有用的,好比車站的售票窗口,每一個窗口能夠看作是一個線程,他們每一個窗口作的事情都是同樣的,也就是售票。這樣咱們程序在模擬現實的時候就能夠定義一個售票任務,讓多個窗口同時執行這一個任務。那麼若是要改動任務執行計劃,只要修改線程任務類,全部的線程就都會按照修改後的來執行。相比較繼承Thread類的方式來建立線程的方式,實現Runnable接口是更爲經常使用的。

3. lambda方式建立線程任務

這裏就是爲了簡化內部類的編寫,簡化了大量的模板代碼,顯得更加簡潔。若是讀者看不明白,能夠讀完內部類方式以後,回過來再看這段代碼。

package com.kingh.thread.create;

/** * 建立線程with lambda * * @author <a href="https://blog.csdn.net/king_kgh>Kingh</a> * @version 1.0 * @date 2019/3/18 10:04 */
public class CreateThreadDemo5_Lambda {

    public static void main(String[] args) throws Exception {
        // 使用lambda的形式實例化線程任務類
        Runnable task = () -> {
            while (true) {
                // 輸出線程的名字
                printThreadInfo();
            }
        };

        // 建立線程對象,並將線程任務類做爲構造方法參數傳入
        new Thread(task).start();

        // 主線程的任務,爲了演示多個線程一塊兒執行
        while (true) {
            printThreadInfo();
            Thread.sleep(1000);
        }
    }

    /** * 輸出當前線程的信息 */
    private static void printThreadInfo() {
        System.out.println("當前運行的線程名爲: " + Thread.currentThread().getName());
        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

第三種方式:使用內部類的方式

這並非一種新的實現線程的方式,只是另外的一種寫法。好比有些狀況咱們的線程就想執行一次,之後就用不到了。那麼像上面兩種方式(繼承Thread類和實現Runnable接口)都還要再定義一個類,顯得比較麻煩,咱們就能夠經過匿名內部類的方式來實現。使用內部類實現依然有兩種,分別是繼承Thread類和實現Runnable接口。代碼以下:

package com.kingh.thread.create;

/** * 匿名內部類的方式建立線程 * * @author <a href="https://blog.csdn.net/king_kgh>Kingh</a> * @version 1.0 * @date 2019/3/18 10:04 */
public class CreateThreadDemo6_Anonymous {

    public static void main(String[] args) {
        // 基於子類的方式
        new Thread() {
            @Override
            public void run() {
                while (true) {
                    printThreadInfo();
                }
            }
        }.start();

        // 基於接口的實現
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    printThreadInfo();
                }
            }
        }).start();
    }

    /** * 輸出當前線程的信息 */
    private static void printThreadInfo() {
        System.out.println("當前運行的線程名爲: " + Thread.currentThread().getName());
        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }
}

能夠想象一下,我能不能既基於接口,又基於子類呢?像下面的代碼會執行出什麼樣子呢?

package com.kingh.thread.create;

/** * 匿名內部類的方式建立線程 * * @author <a href="https://blog.csdn.net/king_kgh>Kingh</a> * @version 1.0 * @date 2019/3/18 10:04 */
public class CreateThreadDemo7_Anonymous {

    public static void main(String[] args) {
        // 基於子類和接口的方式
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    printInfo("interface");
                }
            }
        }) {
            @Override
            public void run() {
                while (true) {
                    printInfo("sub class");
                }
            }
        }.start();
    }

    /** * 輸出當前線程的信息 */
    private static void printInfo(String text) {
        System.out.println(text);
        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }
}

運行結果以下:

sub class
sub class

咱們能夠看到,實際上是基於子類的執行了,爲何呢,其實很簡單,咱們先來看一下爲何不基於子類的時候Runnable的run方法能夠執行。這個要從Thread的源碼看起,下面是我截取的代碼片斷。

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

private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize) {
    init(g, target, name, stackSize, null, true);
}

private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,
                  boolean inheritThreadLocals) {
    if (name == null) {
        throw new NullPointerException("name cannot be null");
    }

    this.name = name;

    Thread parent = currentThread();
    SecurityManager security = System.getSecurityManager();
    if (g == null) {
        /* Determine if it's an applet or not */

        /* If there is a security manager, ask the security manager what to do. */
        if (security != null) {
            g = security.getThreadGroup();
        }

        /* If the security doesn't have a strong opinion of the matter use the parent thread group. */
        if (g == null) {
            g = parent.getThreadGroup();
        }
    }

    /* checkAccess regardless of whether or not threadgroup is explicitly passed in. */
    g.checkAccess();

    /* * Do we have the required permissions? */
    if (security != null) {
        if (isCCLOverridden(getClass())) {
            security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
        }
    }

    g.addUnstarted();

    this.group = g;
    this.daemon = parent.isDaemon();
    this.priority = parent.getPriority();
    if (security == null || isCCLOverridden(parent.getClass()))
        this.contextClassLoader = parent.getContextClassLoader();
    else
        this.contextClassLoader = parent.contextClassLoader;
    this.inheritedAccessControlContext =
        acc != null ? acc : AccessController.getContext();
    this.target = target; // 注意這裏
    setPriority(priority);
    if (inheritThreadLocals && 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();
}

其實上面的衆多代碼就是爲了表現 this.target = target 那麼target是什麼呢,是Thread類的成員變量。那麼在什麼地方用到了target呢?下面是run方法的內容。

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

咱們能夠看到,若是經過上面的構造方法傳入target,那麼就會執行target中的run方法。可能有朋友就會問了,咱們同時繼承Thread類和實現Runnable接口,target不爲空,那麼爲什麼不執行target的run呢。不要忘記了,咱們在子類中已經重寫了Thread類的run方法,所以run方法已經不在是咱們看到的這樣了。那固然也就不回執行target的run方法。

1. lambda 方式改造

剛纔使用匿名內部類,會發現代碼仍是比較冗餘的,lambda能夠大大簡化代碼的編寫。用lambda來改寫上面的基於接口的形式的代碼,以下

// 使用lambda的形式
new Thread(() -> {
    while (true) {
        printThreadInfo();
    }
}).start();


// 對比不使用lambda的形式
new Thread(new Runnable() {
    @Override
    public void run() {
        while (true) {
            printThreadInfo();
        }
    }
}).start();

第四種方式:定時器

定時器能夠說是一種基於線程的一個工具類,能夠定時的來執行某個任務。在應用中常常須要按期執行一些操做,好比要在凌晨的時候彙總一些數據,好比要每隔10分鐘抓取一次某個網站上的數據等等,總之計時器無處不在。

在Java中實現定時任務有不少種方式,JDK提供了Timer類來幫助開發者建立定時任務,另外也有不少的第三方框架提供了對定時任務的支持,好比Spring的schedule以及著名的quartz等等。由於Spring和quartz實現都比較重,依賴其餘的包,上手稍微有些難度,不在本篇博客的討論範圍以內,這裏就看一下JDK所給咱們提供的API來實現定時任務。

1. 指定時間點執行

package com.kingh.thread.create;

import java.text.SimpleDateFormat;
import java.util.Timer;
import java.util.TimerTask;

/** * 定時任務 * * @author <a href="https://blog.csdn.net/king_kgh>Kingh</a> * @version 1.0 * @date 2019/3/18 10:04 */
public class CreateThreadDemo9_Timer {

    private static final SimpleDateFormat format =
            new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

    public static void main(String[] args) throws Exception {

        // 建立定時器
        Timer timer = new Timer();

        // 提交計劃任務
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("定時任務執行了...");
            }
        }, format.parse("2017-10-11 22:00:00"));
    }
}

2.間隔時間重複執行

package com.kingh.thread.create;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

/** * 定時任務 * * @author <a href="https://blog.csdn.net/king_kgh>Kingh</a> * @version 1.0 * @date 2019/3/18 10:04 */
public class CreateThreadDemo10_Timer {

    public static void main(String[] args){

        // 建立定時器
        Timer timer = new Timer();

        // 提交計劃任務
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("定時任務執行了...");
            }
        },
                new Date(), 1000);
    }
}

關於Spring的定時任務,能夠參考 《Spring計劃任務》

第五種方式:帶返回值的線程實現方式

咱們發現上面提到的不論是繼承Thread類仍是實現Runnable接口,發現有兩個問題,第一個是沒法拋出更多的異常,第二個是線程執行完畢以後並沒有法得到線程的返回值。那麼下面的這種實現方式就能夠完成咱們的需求。這種方式的實現就是咱們後面要詳細介紹的Future模式,只是在jdk5的時候,官方給咱們提供了可用的API,咱們能夠直接使用。可是使用這種方式建立線程比上面兩種方式要複雜一些,步驟以下。

  • 建立一個類實現Callable接口,實現call方法。這個接口相似於Runnable接口,但比Runnable接口更增強大,增長了異常和返回值。

  • 建立一個FutureTask,指定Callable對象,作爲線程任務。

  • 建立線程,指定線程任務。

  • 啓動線程

代碼以下:

package com.kingh.thread.create;

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

/** * 帶返回值的方式 * * @author <a href="https://blog.csdn.net/king_kgh>Kingh</a> * @version 1.0 * @date 2019/3/18 10:04 */
public class CreateThreadDemo11_Callable {

    public static void main(String[] args) throws Exception {

        // 建立線程任務
        Callable<Integer> call = () -> {
            System.out.println("線程任務開始執行了....");
            Thread.sleep(2000);
            return 1;
        };

        // 將任務封裝爲FutureTask
        FutureTask<Integer> task = new FutureTask<>(call);

        // 開啓線程,執行線程任務
        new Thread(task).start();

        // ====================
        // 這裏是在線程啓動以後,線程結果返回以前
        System.out.println("這裏能夠隨心所欲....");
        // ====================

        // 隨心所欲完畢以後,拿到線程的執行結果
        Integer result = task.get();
        System.out.println("主線程中拿到異步任務執行的結果爲:" + result);
    }
}

執行結果以下:

這裏能夠隨心所欲....
線程任務開始執行了....
主線程中拿到異步任務執行的結果爲:1

Callable中能夠經過範型參數來指定線程的返回值類型。經過FutureTask的get方法拿到線程的返回值。

第六種方式:基於線程池的方式

咱們知道,線程和數據庫鏈接這些資源都是很是寶貴的資源。那麼每次須要的時候建立,不須要的時候銷燬,是很是浪費資源的。那麼咱們就可使用緩存的策略,也就是使用線程池。固然了,線程池也不須要咱們來實現,jdk的官方也給咱們提供了API。

代碼以下:

package com.kingh.thread.create;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/** * 線程池 * * @author <a href="https://blog.csdn.net/king_kgh>Kingh</a> * @version 1.0 * @date 2019/3/18 10:04 */
public class CreateThreadDemo12_ThreadPool {

    public static void main(String[] args) throws Exception {

        // 建立固定大小的線程池
        ExecutorService threadPool = Executors.newFixedThreadPool(10);

        while (true) {
            // 提交多個線程任務,並執行
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    printThreadInfo();
                }
            });
        }
    }

    /** * 輸出當前線程的信息 */
    private static void printThreadInfo() {
        System.out.println("當前運行的線程名爲: " + Thread.currentThread().getName());
        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }
}

執行結果以下:

當前運行的線程名爲: pool-1-thread-1
當前運行的線程名爲: pool-1-thread-2
當前運行的線程名爲: pool-1-thread-4
當前運行的線程名爲: pool-1-thread-3
當前運行的線程名爲: pool-1-thread-7
當前運行的線程名爲: pool-1-thread-8
當前運行的線程名爲: pool-1-thread-9
當前運行的線程名爲: pool-1-thread-6
當前運行的線程名爲: pool-1-thread-5
當前運行的線程名爲: pool-1-thread-10

線程池的內容還有很是多,這裏再也不詳細地講解。

Spring方式:使用Spring來實現多線程

這種方式依賴於Spring3以上版本,咱們能夠經過Spring的@Async註解很是方便的實現多線程。具體的使用方式見 Spring實現多線程

相關文章
相關標籤/搜索