顯式使用線程實(shí)現(xiàn)異步編程
摘要
本章主要討論如何顯式的使用線程實(shí)現(xiàn)異步編程,這其中包含如何顯式使用線程實(shí)現(xiàn)異步編程的缺點(diǎn),如何顯式實(shí)現(xiàn)異步編程的原理。
在Java中實(shí)現(xiàn)異步編程最簡(jiǎn)單的方式是:每當(dāng)有異步任務(wù)要執(zhí)行時(shí),使用Thread來(lái)創(chuàng)建一個(gè)線程來(lái)進(jìn)行異步執(zhí)行。在講解如何顯式使用Thread實(shí)現(xiàn)異步編程前,我們先來(lái)看下在同步編程模型下,在一個(gè)線程中要做兩件事情的代碼是怎樣的:
/**
* Copyright 2020. javaymw.com Studio All Right Reserved
* <p>
* Create on 03-28 19:15
* Created by zhaoxinguo
* Version 2.0.0
*/
package com.mtons.mblog.javaymw;
/**
* @description: TODO
* @author zhaoxinguo
* @date 2020/3/28 19:15
*/
public class SyncExample {
public static void doSomethingA() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("---doSomethingA---");
}
public static void doSomethingB() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("---doSomethingB---");
}
public static void main(String[] args) {
long start = System.currentTimeMillis();
// 1.執(zhí)行任務(wù)A
doSomethingA();
// 2.執(zhí)行任務(wù)B
doSomethingB();
System.out.println(System.currentTimeMillis() - start);
}
}
運(yùn)行上面的代碼會(huì)啟動(dòng)一個(gè)Java虛擬機(jī)進(jìn)程,進(jìn)程內(nèi)會(huì)創(chuàng)建一個(gè)用戶線程來(lái)執(zhí)行main函數(shù)(main線程),main線程內(nèi)首先執(zhí)行doSometingA方法,然后執(zhí)行了doSometingB方法,那么整個(gè)過(guò)程耗時(shí)4s左右,這是因?yàn)閮蓚€(gè)方法是順序執(zhí)行的。
在Java中,Java虛擬機(jī)允許應(yīng)用程序同時(shí)運(yùn)行多個(gè)執(zhí)行線程,所以我們可在main函數(shù)內(nèi)開(kāi)啟一個(gè)線程來(lái)異步執(zhí)行任務(wù)doSometingA,而main函數(shù)所在線程執(zhí)行doSometingB,即可大大縮短整個(gè)任務(wù)處理耗時(shí)。
Java中有兩種方式來(lái)顯式開(kāi)啟一個(gè)線程進(jìn)行異步處理。第一種方式是實(shí)現(xiàn)java.lang.Runnable接口的run方法,然后傳遞Runnable接口的實(shí)現(xiàn)類作為創(chuàng)建Thread時(shí)
的參數(shù),啟動(dòng)線程,對(duì)應(yīng)這種方式的main函數(shù)代碼可以修改為如下所示:
/**
* Copyright 2020. javaymw.com Studio All Right Reserved
* <p>
* Create on 03-28 19:15
* Created by zhaoxinguo
* Version 2.0.0
*/
package com.mtons.mblog.javaymw;
/**
* @description: TODO
* @author zhaoxinguo
* @date 2020/3/28 19:15
*/
public class SyncExample {
public static void doSomethingA() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("---doSomethingA---");
}
public static void doSomethingB() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("---doSomethingB---");
}
public static void main(String[] args) throws InterruptedException {
long start = System.currentTimeMillis();
// 1.開(kāi)啟異步單元執(zhí)行任務(wù)A
Thread threadA = new Thread(() -> {
doSomethingA();
}, "threadA");
threadA.start();
// 2.執(zhí)行任務(wù)B
doSomethingB();
// 3.同步等待線程A運(yùn)行結(jié)束
threadA.join();
System.out.println(System.currentTimeMillis() - start);
}
}
如上面代碼1,我們?cè)趍ain函數(shù)所在線程內(nèi)首先使用lambda表達(dá)式創(chuàng)建了一個(gè)java.lang.Runnable接口的匿名實(shí)現(xiàn)類,用來(lái)異步執(zhí)行doSometingA任務(wù),然后將
其作為Thread的參數(shù)并啟動(dòng)。這時(shí)候線程A與main線程并發(fā)運(yùn)行,也就是任務(wù)doSometingA與任務(wù)doSometingB并發(fā)運(yùn)行,代碼3則等main線程運(yùn)行完doSometingB任務(wù)后同步等待線程A運(yùn)行完畢。運(yùn)行上面代碼,這時(shí)整個(gè)過(guò)程耗時(shí)大概2s,可知使用異步編程可以大大縮短任務(wù)運(yùn)行時(shí)間。
Java中第二種開(kāi)啟線程進(jìn)行異步執(zhí)行的方式是實(shí)現(xiàn)Thread類,并重寫(xiě)run方法,這種方式的代碼如下:
/**
* Copyright 2020. javaymw.com Studio All Right Reserved
* <p>
* Create on 03-28 19:15
* Created by zhaoxinguo
* Version 2.0.0
*/
package com.mtons.mblog.javaymw;
/**
* @description: TODO
* @author zhaoxinguo
* @date 2020/3/28 19:15
*/
public class SyncExample {
public static void doSomethingA() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("---doSomethingA---");
}
public static void doSomethingB() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("---doSomethingB---");
}
public static void main(String[] args) throws InterruptedException {
long start = System.currentTimeMillis();
// 1.開(kāi)啟異步單元執(zhí)行任務(wù)A
Thread thread = new Thread("threadA") {
@Override
public void run() {
try {
doSomethingA();
} catch (Exception e) {
e.printStackTrace();
}
}
};
thread.start();
// 2.執(zhí)行任務(wù)B
doSomethingB();
// 3.同步等待線程A運(yùn)行結(jié)束
thread.join();
System.out.println(System.currentTimeMillis() - start);
}
}
上面代碼1創(chuàng)建了Thread的匿名類的實(shí)現(xiàn),并重寫(xiě)了run方法,然后啟動(dòng)了線程執(zhí)行。
這里有必要提一下Java中線程是有Deamon與非Deamon之分的,默認(rèn)情況下我們創(chuàng)建的都是非Deamon線程,線程屬于什么類型的與JVM退出條件有一定的關(guān)系。在
Java中,當(dāng)JVM進(jìn)程內(nèi)不存在非Deamon的線程時(shí)JVM就退出了。那么如何創(chuàng)建一個(gè)Deamon線程呢?其實(shí)將調(diào)用線程的setDaemon(boolean on)方法設(shè)置為true就
可以了,更多詳細(xì)內(nèi)可以參考《Java并發(fā)編程之美》這本書(shū)。
上面我們介紹了顯式使用Thread創(chuàng)建異步任務(wù)的兩種方式,但是上述實(shí)現(xiàn)方式存在幾個(gè)問(wèn)題:
- 每當(dāng)執(zhí)行異步任務(wù)時(shí),會(huì)直接創(chuàng)建一個(gè)Thread來(lái)執(zhí)行異步任務(wù),這在生產(chǎn)實(shí)踐中是不建議使用的,因?yàn)榫€程創(chuàng)建于銷毀是有開(kāi)銷的,并且沒(méi)有限制線程的個(gè)數(shù)
,如果使用不當(dāng)可能會(huì)把系統(tǒng)線程用盡,從而造成錯(cuò)誤。在生產(chǎn)環(huán)境中一般創(chuàng)建一個(gè)線程池,然后使用線程池中的線程來(lái)執(zhí)行異步任務(wù),線程池中的線程是可以被
復(fù)用的,這可以大大減少線程創(chuàng)建與銷毀開(kāi)銷;另外線程池可以有效限制創(chuàng)建的線程個(gè)數(shù)。 - 上面使用Thread執(zhí)行的異步任務(wù)并沒(méi)有返回值,如果我們想異步執(zhí)行一個(gè)任務(wù),并且需要在任務(wù)執(zhí)行完畢后獲取任務(wù)執(zhí)行結(jié)果,則上面這個(gè)方式是滿足不了的
,這時(shí)候就需要用到JDK中的Future了。 - 另外,每當(dāng)需要異步執(zhí)行時(shí),我們需要顯式地創(chuàng)建線程并啟動(dòng),這是典型的命令式編程方式,增加了編程者的心智負(fù)擔(dān)。我們需要的是聲明式的異步編程方式,
即告訴程序我們要異步執(zhí)行,但是具體怎么實(shí)現(xiàn)異步應(yīng)該對(duì)我們透明。
針對(duì)第1個(gè)問(wèn)題我們可以使用線程池來(lái)解決;針對(duì)第2個(gè)問(wèn)題我們可以使用Future來(lái)解決;針對(duì)第3個(gè)問(wèn)題,Java中提供了很多封裝良好的類庫(kù)來(lái)解決,在下面章節(jié)我們會(huì)一一詳細(xì)介紹。