實現多執行緒的方式:1、繼承Thread類,通過JDK提供的Thread類,重寫Thread類的run方法即可;2、實現Runnable介面,Runnable是一個「@FunctionalInterface」函數式介面,也就意味了可以利用JDK8提供的lambda的方式來建立執行緒任務;3、使用內部類的方式;4、利用定時器;5、帶返回值的執行緒實現方式;6、基於執行緒池實現多執行緒。
本教學操作環境:windows7系統、java8版、DELL G3電腦。
多執行緒的形式上實現方式主要有兩種,一種是繼承Thread類,一種是實現Runnable介面。本質上實現方式都是來實現執行緒任務,然後啟動執行緒執行執行緒任務(這裡的執行緒任務實際上就是run方法)。這裡所說的6種,實際上都是在以上兩種的基礎上的一些變形。
下面分別就這6中實現方式一一介紹。
萬物皆物件,那麼執行緒也是物件,物件就應該能夠抽取其公共特性封裝成為類,使用類可以範例化多個物件,那麼實現執行緒的第一種方式就是繼承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方法是在變化的,因此放到子類來實現。
上面的例子中除了我們建立的一個執行緒以外其實還有一個主執行緒也在執行。那麼除了這兩個執行緒以外還有沒有其他的執行緒在執行了呢,其實是有的,比如我們看不到的垃圾回收執行緒,也在默默的執行。這裡我們並不去考慮有多少個執行緒在執行,上面我們自己建立了一個執行緒,那麼能不能多建立幾個一起執行呢,答案是肯定的。一個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());
}
}
登入後複製
執行結果如下
當前執行的執行緒名為: main
當前執行的執行緒名為: MyThread-02
當前執行的執行緒名為: MyThread-01
當前執行的執行緒名為: main
當前執行的執行緒名為: MyThread-01
當前執行的執行緒名為: MyThread-02
當前執行的執行緒名為: main
登入後複製
可以看到,通過建立多個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());
}
}
登入後複製
執行的結果如下:
當前執行的執行緒名為: main
當前執行的執行緒名為: Thread-1
當前執行的執行緒名為: Thread-0
當前執行的執行緒名為: main
當前執行的執行緒名為: Thread-1
當前執行的執行緒名為: Thread-0
登入後複製
實現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實現上面的例子步驟如下:
執行緒任務就是執行緒要做的事情,這裡我們讓這個執行緒每隔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());
}
}
登入後複製
在這裡建立執行緒,並把任務交給執行緒處理,然後啟動執行緒。
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介面是更為常用的。
這裡就是為了簡化內部類的編寫,簡化了大量的模板程式碼,顯得更加簡潔。如果讀者看不明白,可以讀完內部類方式之後,回過來再看這段程式碼。
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方法。
剛才使用匿名內部類,會發現程式碼還是比較冗餘的,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來實現定時任務。
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"));
}
}
登入後複製
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的定時任務,可以參考
我們發現上面提到的不管是繼承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
登入後複製
執行緒池的內容還有非常多,這裡不再詳細地講解。
更多程式設計相關知識,請存取:!!
以上就是java實現多執行緒的幾種方式的詳細內容,更多請關注TW511.COM其它相關文章!