掃二維碼與項(xiàng)目經(jīng)理溝通
我們在微信上24小時(shí)期待你的聲音
解答本文疑問/技術(shù)咨詢/運(yùn)營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流
本文轉(zhuǎn)載自微信公眾號「Android開發(fā)編程」,作者Android開發(fā)編程。轉(zhuǎn)載本文請聯(lián)系A(chǔ)ndroid開發(fā)編程公眾號。

創(chuàng)新互聯(lián)主要從事網(wǎng)站制作、成都網(wǎng)站設(shè)計(jì)、網(wǎng)頁設(shè)計(jì)、企業(yè)做網(wǎng)站、公司建網(wǎng)站等業(yè)務(wù)。立足成都服務(wù)托克托,十年網(wǎng)站建設(shè)經(jīng)驗(yàn),價(jià)格優(yōu)惠、服務(wù)專業(yè),歡迎來電咨詢建站服務(wù):18982081108
在平時(shí)工作中如若使用不當(dāng)會出現(xiàn)數(shù)據(jù)錯亂、執(zhí)行效率低(還不如單線程去運(yùn)行)或者死鎖程序掛掉等等問題,所以掌握了解多線程至關(guān)重要;
線程有很多優(yōu)勢:
1、提高多處理器的利用效率;
2、簡化業(yè)務(wù)功能設(shè)計(jì);
3、實(shí)現(xiàn)異步處理;
多線程的風(fēng)險(xiǎn):
1、共享數(shù)據(jù)的線程安全性;
2、多線程執(zhí)行時(shí)的活躍性問題;
3、多線程的所帶來的性能損失問題;
多線程相對于其他知識點(diǎn)來講,有一定的學(xué)習(xí)門檻,并且了解起來比較費(fèi)勁;
線程的優(yōu)勢我們很清楚,線程的風(fēng)險(xiǎn)我們也都知道,但是要做好風(fēng)險(xiǎn)控制就沒有那么簡單了;
本文從基礎(chǔ)概念開始到最后的并發(fā)模型由淺入深,講解下線程方面的知識;
線程有編號、名字、類別以及優(yōu)先級四個屬性,除此之外,線程的部分屬性還具有繼承性,下面我們就來看看線程的四個屬性的作用和線程的繼承性;
①編號
線程的編號(id)用于標(biāo)識不同的線程,每條線程擁有不同的編號;
注意事項(xiàng):不能作為唯一標(biāo)識,某個編號的線程運(yùn)行結(jié)束后,該編號可能被后續(xù)創(chuàng)建的線程使用,因此編號不適合用作唯一標(biāo)識,編號是只讀屬性,不能修改;
②名字
③類別
④優(yōu)先級
作用:線程的優(yōu)先級(Priority)用于表示應(yīng)用希望優(yōu)先運(yùn)行哪個線程,線程調(diào)度器會根據(jù)這個值來決定優(yōu)先運(yùn)行哪個線程;
⑤取值范圍
Java 中線程優(yōu)先級的取值范圍為 1~10,默認(rèn)值是 5,Thread 中定義了下面三個優(yōu)先級常量;
注意事項(xiàng):不保證,線程調(diào)度器把線程的優(yōu)先級當(dāng)作一個參考值,不一定會按我們設(shè)定的優(yōu)先級順序執(zhí)行線程;
⑥線程饑餓
優(yōu)先級使用不當(dāng)會導(dǎo)致某些線程永遠(yuǎn)無法執(zhí)行,也就是線程饑餓的情況;
⑦繼承性
線程的繼承性指的是線程的類別和優(yōu)先級屬性是會被繼承的,線程的這兩個屬性的初始值由開啟該線程的線程決定;
假如優(yōu)先級為 5 的守護(hù)線程 A 開啟了線程 B,那么線程 B 也是一個守護(hù)線程,而且優(yōu)先級也是 5 ;
這時(shí)我們就把線程 A 叫做線程 B 的父線程,把線程 B 叫做線程 A 的子線程;
線程的常用方法有六個,它們分別是三個非靜態(tài)方法 start()、run()、join() 和三個靜態(tài)方法 currentThread()、yield()、sleep() ;
下面我們就來看下這六個方法都有哪些作用和注意事項(xiàng)
①start()
② run()
③ join()
④Thread.currentThread()
⑤Thread.yield()
⑥ Thread.sleep(ms)
作用:sleep(ms) 方法是一個靜態(tài)方法,用于使當(dāng)前線程在指定時(shí)間內(nèi)休眠(暫停)。
①線程的生命周期
線程的生命周期不僅可以由開發(fā)者觸發(fā),還會受到其他線程的影響,下面是線程各個狀態(tài)之間的轉(zhuǎn)換示意圖;
我們可以通過 Thread.getState() 獲取線程的狀態(tài),該方法返回的是一個枚舉類 Thread.State;
線程的狀態(tài)有新建、可運(yùn)行、阻塞、等待、限時(shí)等待和終止 6 種,下面我們就來看看這 6 種狀態(tài)之間的轉(zhuǎn)換過程;
新建狀態(tài):當(dāng)一個線程創(chuàng)建后未啟動時(shí),它就處于新建(NEW)狀態(tài);
②可運(yùn)行狀態(tài):當(dāng)我們調(diào)用線程的 start() 方法后,線程就進(jìn)入了可運(yùn)行(RUNNABLE)狀態(tài),可運(yùn)行狀態(tài)又分為預(yù)備(READY)和運(yùn)行(RUNNING)狀態(tài);
③預(yù)備狀態(tài):處于預(yù)備狀態(tài)的線程可被線程調(diào)度器調(diào)度,調(diào)度后線程的狀態(tài)會從預(yù)備轉(zhuǎn)換為運(yùn)行狀態(tài),處于預(yù)備狀態(tài)的線程也叫活躍線程,當(dāng)線程的 yield() 方法被調(diào)用后,線程的狀態(tài)可能由運(yùn)行狀態(tài)變?yōu)轭A(yù)備狀態(tài)
④運(yùn)行狀態(tài):運(yùn)行狀態(tài)表示線程正在運(yùn)行,也就是處理器正在執(zhí)行線程的 run() 方法;
⑤阻塞狀態(tài):當(dāng)下面幾種情況發(fā)生時(shí),線程就處于阻塞(BLOCKED)狀態(tài),發(fā)起阻塞式 I/O 操作、申請其他線程持有的鎖、進(jìn)入一個 synchronized 方法或代碼塊失敗;
⑥等待狀態(tài):一個線程執(zhí)行特定方法后,會等待其他線程執(zhí)行執(zhí)行完畢,此時(shí)線程進(jìn)入了等待(WAITING)狀態(tài);
⑦等待狀態(tài):下面的幾個方法可以讓線程進(jìn)入等待狀態(tài);
可運(yùn)行狀態(tài):下面的幾個方法可以讓線程從等待狀態(tài)轉(zhuǎn)變?yōu)榭蛇\(yùn)行狀態(tài),而這種轉(zhuǎn)變又叫喚醒;
⑧限時(shí)等待狀態(tài)
⑨ 終止?fàn)顟B(tài)
當(dāng)線程的任務(wù)執(zhí)行完畢或者任務(wù)執(zhí)行遇到異常時(shí),線程就處于終止(TERMINATED)狀態(tài);
線程調(diào)度原理相關(guān)的對 Java 內(nèi)存模型、高速緩存、Java 線程調(diào)度機(jī)制進(jìn)行一個簡單介紹;
①高速緩存簡介
現(xiàn)代處理器的處理能力要遠(yuǎn)勝于主內(nèi)存(DRAM)的訪問速率,主內(nèi)存執(zhí)行一次內(nèi)存讀/寫操作需要的時(shí)間,如果給處理器使用,處理器可以執(zhí)行上百條指令;
為了彌補(bǔ)處理器與主內(nèi)存之間的差距,硬件設(shè)計(jì)者在主內(nèi)存與處理器之間加入了高速緩存(Cache);
處理器執(zhí)行內(nèi)存讀寫操作時(shí),不是直接與主內(nèi)存打交道,而是通過高速緩存進(jìn)行的;
高速緩存相當(dāng)于是一個由硬件實(shí)現(xiàn)的容量極小的散列表,這個散列表的 key 是一個對象的內(nèi)存地址,value 可以是內(nèi)存數(shù)據(jù)的副本,也可以是準(zhǔn)備寫入內(nèi)存的數(shù)據(jù);
②高速緩存內(nèi)部結(jié)構(gòu)
從內(nèi)部結(jié)構(gòu)來看,高速緩存相當(dāng)于是一個鏈?zhǔn)缴⒘斜?Chained Hash Table),它包含若干個桶,每個桶包含若干個緩存條目(Cache Entry);
③緩存條目結(jié)構(gòu)
緩存條目可進(jìn)一步劃分為 Tag、Data Block 和 Flag 三個部分;
Tag:包含了與緩存行中數(shù)據(jù)對應(yīng)的內(nèi)存地址的部分信息(內(nèi)存地址的高位部分比特)
Data: Block 也叫緩存行(Cache Line),是高速緩存與主內(nèi)存之間數(shù)據(jù)交換的最小單元,可以存儲從內(nèi)存中讀取的數(shù)據(jù),也可以存儲準(zhǔn)備寫進(jìn)內(nèi)存的數(shù)據(jù);
Flag: 用于表示對應(yīng)緩存行的狀態(tài)信息
在任意時(shí)刻,CPU 只能執(zhí)行一條機(jī)器指令,每個線程只有獲取到 CPU 的使用權(quán)后,才可以執(zhí)行指令;
也就是在任意時(shí)刻,只有一個線程占用 CPU,處于運(yùn)行的狀態(tài);
多線程并發(fā)運(yùn)行實(shí)際上是指多個線程輪流獲取 CPU 使用權(quán),分別執(zhí)行各自的任務(wù);
線程的調(diào)度由 JVM 負(fù)責(zé),線程的調(diào)度是按照特定的機(jī)制為多個線程分配 CPU 的使用權(quán);
線程調(diào)度模型分為兩類:分時(shí)調(diào)度模型和搶占式調(diào)度模型;
①分時(shí)調(diào)度模型
分時(shí)調(diào)度模型是讓所有線程輪流獲取 CPU 使用權(quán),并且平均分配每個線程占用 CPU 的時(shí)間片;
②搶占式調(diào)度模型
線程安全問題不是說線程不安全,而是多個線程之間交錯操作有可能導(dǎo)致數(shù)據(jù)異常;
下面我們就來看下與線程安全相關(guān)的競態(tài)和實(shí)現(xiàn)線程安全要保證的三個點(diǎn):原子性、可見性和有序性;
①原子性
②可見性
③ 有序性
要實(shí)現(xiàn)線程安全就要保證上面說到的原子性、可見性和有序性;
常見的實(shí)現(xiàn)線程安全的辦法是使用鎖和原子類型,而鎖可分為內(nèi)部鎖、顯式鎖、讀寫鎖、輕量級鎖(volatile)四種;
下面我們就來看看這四種鎖和原子類型的用法和特點(diǎn);
是鎖(Lock)的作用,讓多個線程更好地協(xié)作,避免多個線程的操作交錯導(dǎo)致數(shù)據(jù)異常的問題;
鎖的五個特點(diǎn):
volatile 關(guān)鍵字可用于修飾共享變量,對應(yīng)的變量就叫 volatile 變量,volatile 變量有下面幾個特點(diǎn);
原子類型簡介:
在 JUC 下有一個 atomic 包,這個包里面有一組原子類,使用原子類的方法,不需要加鎖也能保證線程安全,而原子類是通過 Unsafe 類中的 CAS 指令從硬件層面來實(shí)現(xiàn)線程安全的;
這個包里面有如 AtomicInteger、AtomicBoolean、AtomicReference、AtomicReferenceFIeldUpdater 等;
我們先來看一個使用原子整型 AtomicInteger 自增的例子;
// 初始值為 1
AtomicInteger integer = new AtomicInteger(1);
// 自增
int result = integer.incrementAndGet();
// 結(jié)果為 2
System.out.println(result);
AtomicReference 和 AtomicReferenceFIeldUpdater 可以讓我們自己的類具有原子性,它們的原理都是通過 Unsafe 的 CAS 操作實(shí)現(xiàn)的;
我們下面看下它們的用法和區(qū)別;
①、AtomicReference 基本用法
- class AtomicReferenceValueHolder {
- AtomicReference
atomicValue = new AtomicReference<>("HelloAtomic"); - }
- public void getAndUpdateFromReference() {
- AtomicReferenceValueHolder holder = new AtomicReferenceValueHolder();
- // 對比并設(shè)值
- // 如果值是 HelloAtomic,就把值換成 World
- holder.atomicValue.compareAndSet("HelloAtomic", "World");
- // World
- System.out.println(holder.atomicValue.get());
- // 修改并獲取修改后的值
- String value = holder.atomicValue.updateAndGet(new UnaryOperator
() { - @Override
- public String apply(String s) {
- return "HelloWorld";
- }
- });
- // Hello World
- System.out.println(value);
- }
② AtomicReferenceFieldUpdater 基本用法
AtomicReferenceFieldUpdater 在用法上和 AtomicReference 有些不同,我們直接把 String 值暴露了出來,并且用 volatile 對這個值進(jìn)行了修飾;
并且將當(dāng)前類和值的類傳到 newUpdater ()方法中獲取 Updater,這種用法有點(diǎn)像反射,而且 AtomicReferenceFieldUpdater 通常是作為類的靜態(tài)成員使用;
- public class SimpleValueHolder {
- public static AtomicReferenceFieldUpdater
valueUpdater - = AtomicReferenceFieldUpdater.newUpdater(
- SimpleValueHolder.class, String.class, "value");
- volatile String value = "HelloAtomic";
- }
- public void getAndUpdateFromUpdater() {
- SimpleValueHolder holder = new SimpleValueHolder();
- holder.valueUpdater.compareAndSet(holder, "HelloAtomic", "World");
- // World
- System.out.println(holder.valueUpdater.get(holder));
- String value = holder.valueUpdater.updateAndGet(holder, new UnaryOperator
() { - @Override
- public String apply(String s) {
- return "HelloWorld";
- }
- });
- // HelloWorld
- System.out.println(value);
- }
③AtomicReference 與 AtomicReferenceFieldUpdater 的區(qū)別
AtomicReference 和 AtomicReferenceFieldUpdater 的作用是差不多的,在用法上 AtomicReference 比 AtomicReferenceFIeldUpdater 更簡單;
但是在內(nèi)部實(shí)現(xiàn)上,AtomicReference 內(nèi)部一樣是有一個 volatile 變量;
使用 AtomicReference 和使用 AtomicReferenceFIeldUpdater 比起來,要多創(chuàng)建一個對象;
對于 32 位的機(jī)器,這個對象的頭占 12 個字節(jié),它的成員占 4 個字節(jié),也就是多出來 16 個字節(jié);
對于 64 位的機(jī)器,如果啟動了指針壓縮,那這個對象占用的也是 16 個字節(jié);
對于 64 位的機(jī)器,如果沒啟動指針壓縮,那么這個對象就會占 24 個字節(jié),其中對象頭占 16 個字節(jié),成員占 8 個字節(jié);
當(dāng)要使用 AtomicReference 創(chuàng)建成千上萬個對象時(shí),這個開銷就會變得很大;
這也就是為什么 BufferedInputStream 、Kotlin 協(xié)程 和 Kotlin 的 lazy 的實(shí)現(xiàn)會選擇 AtomicReferenceFieldUpdater 作為原子類型;
因?yàn)殚_銷的原因,所以一般只有在原子類型創(chuàng)建的實(shí)例確定了較少的情況下,比如說是單例,才會選擇 AtomicReference,否則都是用 AtomicReferenceFieldUpdater;
死鎖是線程的一種常見多線程活躍性問題,如果兩個或更多的線程,因?yàn)橄嗷サ却龑Ψ蕉挥肋h(yuǎn)暫停,那么這就叫死鎖現(xiàn)象;
下面我們就來看看死鎖產(chǎn)生的四個條件和避免死鎖的三個方法;
當(dāng)多個線程發(fā)生了死鎖后,這些線程和相關(guān)共享變量就會滿足下面四個條件:
只要產(chǎn)生了死鎖,上面的條件就一定成立,但是上面的條件都成立也不一定會產(chǎn)生死鎖;
要想消除死鎖,只要破壞掉上面的其中一個條件即可;
由于鎖具有排他性,且無法被動釋放,所以我們只能破壞掉第三個和第四個條件;
①、粗鎖法
②鎖排序法
鎖排序法指的是相關(guān)線程使用全局統(tǒng)一的順序申請鎖;
假如有多個線程需要申請鎖,我們只需要讓這些線程按照一個全局統(tǒng)一的順序去申請鎖,這樣就能破壞“循環(huán)等待資源”這個條件;
③tryLock
顯式鎖 ReentrantLock.tryLock(long timeUnit) 這個方法允許我們?yōu)樯暾堟i的操作設(shè)置超時(shí)時(shí)間,這樣就能破壞“占用并等待資源”這個條件;
④開放調(diào)用
開放調(diào)用(Open Call)就是一個方法在調(diào)用外部方法時(shí)不持有鎖,開放調(diào)用能破壞“占用并等待資源”這個條件;
線程間的常見協(xié)作方式有兩種:等待和中斷;
當(dāng)一個線程中的操作需要等待另一個線程中的操作結(jié)束時(shí),就涉及到等待型線程協(xié)作方式;
常用的等待型線程協(xié)作方式有 join、wait/notify、await/signal、await/countDown 和 CyclicBarrier 五種,下面我們就來看看這五種線程協(xié)作方式的用法和區(qū)別;
下面是 join() 方法的簡單用法;
- public void tryJoin() {
- Thread threadA = new ThreadA();
- Thread threadB = new ThreadB(threadA);
- threadA.start();
- threadB.start();
- }
- public class ThreadA extends Thread {
- @Override
- public void run() {
- System.out.println("線程 A 開始執(zhí)行");
- ThreadUtils.sleep(1000);
- System.out.println("線程 A 執(zhí)行結(jié)束");
- }
- }
- public class ThreadB extends Thread {
- private final Thread threadA;
- public ThreadB(Thread thread) {
- threadA = thread;
- }
- @Override
- public void run() {
- try {
- System.out.println("線程 B 開始等待線程 A 執(zhí)行結(jié)束");
- threadA.join();
- System.out.println("線程 B 結(jié)束等待,開始做自己想做的事情");
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
下面是 wait/notify 使用的示例代碼;
- final Object lock = new Object();
- private volatile boolean conditionSatisfied;
- public void startWait() throws InterruptedException {
- synchronized (lock) {
- System.out.println("等待線程獲取了鎖");
- while(!conditionSatisfied) {
- System.out.println("保護(hù)條件不成立,等待線程進(jìn)入等待狀態(tài)");
- lock.wait();
- }
- System.out.println("等待線程被喚醒,開始執(zhí)行目標(biāo)動作");
- }
- }
- public void startNotify() {
- synchronized (lock) {
- System.out.println("通知線程獲取了鎖");
- System.out.println("通知線程即將喚醒等待線程");
- conditionSatisfied = true;
- lock.notify();
- }
- }
notify() 可能導(dǎo)致信號丟失,而 notifyAll() 雖然會把不需要喚醒的等待線程也喚醒,但是在正確性方面有保障;
所以一般情況下優(yōu)先使用 notifyAll() 保障正確性;
一般情況下,只有在下面兩個條件都實(shí)現(xiàn)時(shí),才會選擇使用 notify() 實(shí)現(xiàn)通知;
①只需喚醒一個線程
當(dāng)一次通知只需要喚醒最多一個線程時(shí),我們可以考慮使用 notify() 實(shí)現(xiàn)通知,但是光滿足這個條件還不夠;
在不同的等待線程使用不同的保護(hù)條件時(shí),notify() 喚醒的一個任意線程可能不是我們需要喚醒的那個線程,所以需要條件 2 來排除;
②對象的等待集中只包含同質(zhì)等待線程
同質(zhì)等待線程指的是線程使用同一個保護(hù)條件并且 wait() 調(diào)用返回后的邏輯一致;
最典型的同質(zhì)線程是使用同一個 Runnable 創(chuàng)建的不同線程,或者同一個 Thread 子類 new 出來的多個實(shí)例;
wait()/notify() 過于底層,而且還存在兩個問題,一是過早喚醒、二是無法區(qū)分 Object.wait(ms) 返回是由于等待超時(shí)還是被通知線程喚醒;
await/signal 基本用法
- private Lock lock = new ReentrantLock();
- private Condition condition = lock.newCondition();
- private volatile boolean conditionSatisfied = false;
- private void startWait() {
- lock.lock();
- System.out.println("等待線程獲取了鎖");
- try {
- while (!conditionSatisfied) {
- System.out.println("保護(hù)條件不成立,等待線程進(jìn)入等待狀態(tài)");
- condition.await();
- }
- System.out.println("等待線程被喚醒,開始執(zhí)行目標(biāo)動作");
- } catch (InterruptedException e) {
- e.printStackTrace();
- } finally {
- lock.unlock();
- System.out.println("等待線程釋放了鎖");
- }
- }
- public void startNotify() {
- lock.lock();
- System.out.println("通知線程獲取了鎖");
- try {
- conditionSatisfied = true;
- System.out.println("通知線程即將喚醒等待線程");
- condition.signal();
- } finally {
- System.out.println("通知線程釋放了鎖");
- lock.unlock();
- }
- }
awaitUntil(timeout, unit) 方法;
如果是由于超時(shí)導(dǎo)致等待結(jié)束,那么 awaitUntil() 會返回 false,否則會返回 true,表示等待是被喚醒的,下面我們就看看這個方法是怎么用的;
- private void startTimedWait() throws InterruptedException {
- lock.lock();
- System.out.println("等待線程獲取了鎖");
- // 3 秒后超時(shí)
- Date date = new Date(System.currentTimeMillis() + 3 * 1000);
- boolean isWakenUp = true;
- try {
- while (!conditionSatisfied) {
- if (!isWakenUp) {
- System.out.println("已超時(shí),結(jié)束等待任務(wù)");
- return;
- } else {
- System.out.println("保護(hù)條件不滿足,并且等待時(shí)間未到,等待進(jìn)入等待狀態(tài)");
- isWakenUp = condition.awaitUntil(date);
- }
- }
- System.out.println("等待線程被喚醒,開始執(zhí)行目標(biāo)動作");
- } finally {
- lock.unlock();
- }
- }
- public void startDelayedNotify() {
- threadSleep(4 * 1000);
- startNotify();
- }
使用 join() 實(shí)現(xiàn)的是一個線程等待另一個線程執(zhí)行結(jié)束,但是有的時(shí)候我們只是想要一個特定的操作執(zhí)行結(jié)束,不需要等待整個線程執(zhí)行結(jié)束,這時(shí)候就可以使用 CountDownLatch 來實(shí)現(xiàn);
await/countDown 基本用法
- public void tryAwaitCountDown() {
- startWaitThread();
- startCountDownThread();
- startCountDownThread();
- }
- final int prerequisiteOperationCount = 2;
- final CountDownLatch latch = new CountDownLatch(prerequisiteOperationCount);
- private void startWait() throws InterruptedException {
- System.out.println("等待線程進(jìn)入等待狀態(tài)");
- latch.await();
- System.out.println("等待線程結(jié)束等待");
- }
- private void startCountDown() {
- try {
- System.out.println("執(zhí)行先決操作");
- } finally {
- System.out.println("計(jì)數(shù)值減 1");
- latch.countDown();
- }
- }
有的時(shí)候多個線程需要互相等待對方代碼中的某個地方(集合點(diǎn)),這些線程才能繼續(xù)執(zhí)行,這時(shí)可以使用 CyclicBarrier(柵欄);
CyclicBarrier 基本用法
- final int parties = 3;
- final Runnable barrierAction = new Runnable() {
- @Override
- public void run() {
- System.out.println("人來齊了,開始爬山");
- }
- };
- final CyclicBarrier barrier = new CyclicBarrier(parties, barrierAction);
- public void tryCyclicBarrier() {
- firstDayClimb();
- secondDayClimb();
- }
- private void firstDayClimb() {
- new PartyThread("第一天爬山,老李先來").start();
- new PartyThread("老王到了,小張還沒到").start();
- new PartyThread("小張到了").start();
- }
- private void secondDayClimb() {
- new PartyThread("第二天爬山,老王先來").start();
- new PartyThread("小張到了,老李還沒到").start();
- new PartyThread("老李到了").start();
- }
- public class PartyThread extends Thread {
- private final String content;
- public PartyThread(String content) {
- this.content = content;
- }
- @Override
- public void run() {
- System.out.println(content);
- try {
- barrier.await();
- } catch (BrokenBarrierException e) {
- e.printStackTrace();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
Android 中常用的 7 種異步方式:Thread、HandlerThread、IntentService、AsyncTask、線程池、RxJava 和 Kotlin 協(xié)程;

我們在微信上24小時(shí)期待你的聲音
解答本文疑問/技術(shù)咨詢/運(yùn)營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流