CPU 核心數(shù)和線程數(shù)的關(guān)系
目前的 CPU 有雙核,四核筹煮,八核遮精,一般情況下,它和線程數(shù)是1:1的對應(yīng)關(guān)系败潦,也就是四核 CPU 一般就能并行執(zhí)行 4 個線程本冲。但 Intel 引入超線程技術(shù)后,使核心數(shù)與線程數(shù)形成1:2的關(guān)系,也就是我們常說的
4核8線程
線程調(diào)度與線程調(diào)度模型
任意時刻劫扒,只有一個線程占用 CPU檬洞,處于運行狀態(tài)。而多線程并發(fā)執(zhí)行就是輪流獲取 CPU 執(zhí)行權(quán)沟饥。
- 分時調(diào)用模型
輪流獲取 CPU 執(zhí)行權(quán)添怔,均分 CPU 執(zhí)行時間。
- 搶占式調(diào)度模型
優(yōu)先級高的線程優(yōu)先獲取 CPU 執(zhí)行權(quán)贤旷,這也是
JVM
采用的線程調(diào)度模型广料。
進程與線程
進程是程序運行資源分配
的最小單位。
這些資源就包括
CPU
幼驶,內(nèi)存空間艾杏,磁盤 IO 等。同一進程中的多個線程共享該進程的所有資源县遣,而不同進程是彼此獨立的糜颠。舉個栗子汹族,在手機開啟一個 APP 實際上就是開啟了一個進程了萧求,而每一個 APP 程序會有很多線程在跑,例如刷新 UI 的線程等顶瞒,所以說進程是包含線程的夸政。
線程是 CPU 調(diào)度
的最小單位,必須依賴于進程而存在榴徐。
線程是比進程更小的守问,能獨立運行的基本單位匀归,每一個線程都有一個程序計數(shù)器,虛擬機棧等耗帕,它可以與同一進程下的其它線程共享該進程的資源穆端。
并行與并發(fā)
- 并行
指應(yīng)用能夠同時執(zhí)行不同的任務(wù)。例如多輛汽車可以同時在同一條公路上的不同車道上并行通行仿便。
- 并發(fā)
指應(yīng)用能夠交替執(zhí)行不同的任務(wù)体啰,因為一般的計算機只有一個 CPU 也就是只有一顆心,如果一個 CPU 要運行多個進程嗽仪,那就需要使用到并發(fā)技術(shù)了荒勇,例如時間片輪轉(zhuǎn)進程調(diào)度算。比如單 CPU 核心下執(zhí)行多線程任務(wù)時并非同時執(zhí)行多個任務(wù)闻坚,而是以一個非常短的時間不斷地切換執(zhí)行不同的任務(wù)沽翔,這個時間是我們無法察覺的出來的。
兩者的區(qū)別:并行是同時執(zhí)行窿凤,并發(fā)是交替執(zhí)行仅偎。
高并發(fā)編程的意義
- 充分利用 CPU 資源
線程是 CPU 調(diào)度的最小的單位,我們的程序是跑在 CPU 的一個核中的某一個線程中的雳殊,如果在程序中只有一個線程哨颂,那么對于雙核心4線程的CPU來說就要浪費了 3/4 的 CPU 性能了,所以在創(chuàng)建線程的時候需要合理的利用 CPU 資源相种,具體可以看看 AsyncTask 內(nèi)部的線程池是如何設(shè)計的威恼。
- 加快響應(yīng)用戶的時間
如果多個任務(wù)時串行執(zhí)行的話,那么效果肯定不好寝并,在移動端開發(fā)中箫措,并發(fā)執(zhí)行多個任務(wù)是很常見的操作,最常見的就是多線程下載了衬潦。
- 可以使代碼模塊化斤蔓,異步化,簡單化
在 Android 應(yīng)用程序開發(fā)中的镀岛,一般 UI 線程負責(zé)去更新界面相關(guān)的工作弦牡,而一些 IO,網(wǎng)絡(luò)等操作一般會放在異步的工作線程去執(zhí)行漂羊,這樣使得不同的線程各司其職驾锰,異步化。
線程之間的安全性問題
問題1:同一進程間的多個線程是可以共享該進程的資源的走越,當(dāng)多個線程訪問共享變量時椭豫,就會線程安全問題。
問題2:為了解決線程之間的同步問題,一般會引入鎖機制赏酥,對于線程之間搶奪鎖時也是有可能造成死鎖問題喳整。
問題3:在 JVM 內(nèi)存模型中,每一個線程都會分配一個虛擬機棧裸扶,這個虛擬機棧是需要占用內(nèi)存空間的框都,如果無限制的創(chuàng)建線程的話,會耗盡系統(tǒng)的內(nèi)存呵晨。
線程的開啟與關(guān)閉
線程的啟動
- 派生 Thread 類
//開啟一個線程
Thread thread = new Thread() {
@Override
public void run() {
super.run();
System.out.println("thread started");
}
};
thread.start();
- 實現(xiàn) Runnable 接口瞬项,將其交給 Thread 類去執(zhí)行
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("runnable run invoked");
}
});
thread.start();
- 實現(xiàn) Callable 接口
因為 Thread 構(gòu)造中只接收 Runnable 類型的接口,需要實現(xiàn)將 Callable 的實現(xiàn)類包裝為 FutureTask 之后交給 Thread 類去執(zhí)行何荚。
Callable<String> callable = new Callable<String>() {
@Override
public String call() throws Exception {
Thread.sleep(1500);
return "work done!";
}
};
FutureTask<String> futureTask = new FutureTask(callable);
Thread thread = new Thread(futureTask);
thread.start();
try {
//get() 是一個阻塞式的操作囱淋,一直等待 call 方法執(zhí)行完畢。
String resule = futureTask.get();
} catch (ExecutionException e) {
e.printStackTrace();
}
再來看 FutureTask 的應(yīng)用:我們觀察到 AsyncTask 內(nèi)部就使用到了 FutureTask 餐塘,因為在 doInBackground() 需要有一個返回值妥衣,而恰好 Callable 就可以實現(xiàn)子線程執(zhí)行完有返回值。使用 FutureTask 來封裝 WorkRunnable 對象戒傻,然后再交給對應(yīng)的線程池去執(zhí)行税手。具體的代碼如下:
總結(jié):對于第1,2兩種方式是在線程執(zhí)行完畢后需纳,無法得到執(zhí)行的結(jié)果芦倒,而第三種方式是可以獲取執(zhí)行結(jié)果的。
線程的終止
- 線程自然終止
也就是 run 執(zhí)行完畢不翩,或者是內(nèi)部出現(xiàn)一個異常導(dǎo)致線程提前結(jié)束兵扬。
-
暴力終止
suspend() 使線程掛起,并且不會釋放線程占有的資源(例如鎖)口蝠,resume() 使掛起的線程恢復(fù)器钟。
stop() 暴力停止,立刻釋放鎖妙蔗,導(dǎo)致共享資源可能不同步傲霸。
以上幾個方法已經(jīng)被 JDK 標記為廢棄狀態(tài)。
- interrupt() 安全終止
第一種情況:如果線程處于正常運行狀態(tài)眉反,那么線程的中斷狀態(tài)會被置為 true 昙啄,并且線程還是會正常執(zhí)行,僅此而已寸五。
第二種情況:如果當(dāng)前線程如果是處于阻塞狀態(tài)梳凛,例如調(diào)用了 wait,join,sleep 等方法,那么則會拋出InterruptedException
異常播歼。
總結(jié): interrupt() 并不能中斷線程伶跷,需要線程配合才能實現(xiàn)中斷操作掰读。
示例01
public class EndThread extends Thread {
@Override
public void run() {
super.run();
System.out.println("isInterrupted:"+isInterrupted());
while (true) {//盡管在其他線程中調(diào)用了 interrupt() 方法秘狞,但是線程并不會終止
//while (!isInterrupted()){
System.out.println("I am Thread body");
}
// System.out.println("isInterrupted:"+isInterrupted());
}
public static void main(String[] args) throws InterruptedException {
final EndThread endThread = new EndThread();
endThread.start();
Thread.sleep(10);
//在其他線程中去調(diào)用線程的interrupt方法給線程打一個終止的標記
endThread.interrupt();
}
}
上面的代碼時在線程體中執(zhí)行一個 while(true)的死循環(huán)叭莫,然后在其他線程中調(diào)用 endThread.interrupt()
觀察當(dāng)前線程是否會執(zhí)行完畢。
經(jīng)測試:在調(diào)用線程的 interrupt()方法之后烁试,while(true)是不會結(jié)束循環(huán)的雇初,也就是線程還是一直在運行著。所以說 interrupt() 并不會應(yīng)用 run 方法的執(zhí)行
示例02
下面再來看看另一個關(guān)于 interrupt 方法的使用
在線程體內(nèi)部 sleep(2000) 并且 try catch 對應(yīng)的 InterruptedException 異常减响,如果在其他線程調(diào)用了 endThread.interrupt() 那么此處就會拋出 InterruptedException 異常靖诗,并且會isInterrupted() 會返回 false 。
public class EndThread2 extends Thread {
@Override
public void run() {
super.run();
System.out.println("isInterrupted:" + isInterrupted());
try {
//在其他線程調(diào)用 endThread.interrupt() 之后支示,會拋出 InterruptedException 異常并且線程的
// isInterrupted 會被標記為 false刊橘。因此最后輸出的結(jié)果還是 false
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("catch InterruptedException");
}
System.out.println("isInterrupted:" + isInterrupted());
}
public static void main(String[] args) throws InterruptedException {
final EndThread2 endThread = new EndThread2();
endThread.start();
Thread.sleep(300);
//在其他線程中去調(diào)用線程的interrupt方法給線程打一個終止的標記
endThread.interrupt();
}
}
執(zhí)行結(jié)果:
isInterrupted:false
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at endthread.EndThread.run(EndThread.java:28)
catch InterruptedException
isInterrupted:false
示例03
將示例01
中的 while(true) 修改為 while(!isInterrupted()){},在外界調(diào)用了 endThread.interrupt()
之后颂鸿,線程的 isInterrupted() 就會返回 true促绵,標記著你可以結(jié)束線程了。
public class EndThread extends Thread {
@Override
public void run() {
super.run();
System.out.println("isInterrupted:"+isInterrupted());
while (!isInterrupted()){
System.out.println("I am Thread body");
}
System.out.println("isInterrupted:"+isInterrupted());
}
public static void main(String[] args) throws InterruptedException {
final EndThread endThread = new EndThread();
endThread.start();
Thread.sleep(10);
//在其他線程中去調(diào)用線程的interrupt方法給線程打一個終止的標記
endThread.interrupt();
)
}
}
示例04
還有一種方式是設(shè)置一個 boolean
類型的變量 mIsExit
標記嘴纺,當(dāng)線程體內(nèi)部判斷到 mIsExit
為 false 那么就跳出循環(huán)
败晴。具體示例代碼如下:
public class EndThread extends Thread {
//這個變量需要在其他線程中判斷,因此需要設(shè)置為線程可見的
private volatile boolean mIsExit = true;
@Override
public void run() {
super.run();
while(mIsExit){
System.out.println("I am Thread body");
}
}
public static void main(String[] args) throws InterruptedException {
final EndThread endThread = new EndThread();
endThread.start();
Thread.sleep(3);
//設(shè)置標記為退出狀態(tài)
endThread.mIsExit = false;
}
}
線程其他 API
Thread#start() 與 Thread#run()
start() 方法調(diào)用之后會讓一個線程進入就緒等待隊列
栽渴,當(dāng)獲取到 CPU 執(zhí)行權(quán)之后會執(zhí)行線程體 run()方法尖坤。
run() 方法只是 Thread 類中一個普通方法
,如果手動去調(diào)用闲擦,跟調(diào)用普通方法沒有什么區(qū)別慢味。
Thread#run() 與 Runnable#run()
在 Java 中只有 Thread 才能表示一個線程,而 Runnable 只能表示一個任務(wù)
墅冷,任務(wù)是需要交給線程去執(zhí)行的贮缕,當(dāng)出現(xiàn)如下代碼時,你看到可能會懵逼俺榆,執(zhí)行結(jié)果到底是什么感昼?
//接受一個 runnable 接口
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("runnable run invoked");
}
}) {
@Override
public void run() {
super.run();
System.out.println("thread started");
}
};
thread.start();
以上方式的輸出結(jié)果是:
如果線程 run 方法內(nèi)部調(diào)用了 super.run() 那么輸出結(jié)果如下:
runnable run invoked
thread started
如果線程 run 方法內(nèi)部不調(diào)用 super.run() 那么輸出結(jié)果如下:
thread started
我們可以通過源碼來解答這個問題:在創(chuàng)建線程時,如果往構(gòu)造函數(shù)中傳入一個
Runnable
對象罐脊,那么它會給線程target
屬性賦值定嗓,并且在線程體
執(zhí)行時先判斷target
是否為空,不為空萍桌,則先執(zhí)行Runnable
的run
方法宵溅,再執(zhí)行當(dāng)前線程體的子類中的 run 方法。
//Thread.java
private Runnable target;
public Thread(ThreadGroup group, Runnable target, String name,
long stackSize) {
init(group, target, name, stackSize);
}
@Override
public void run() {
if (target != null) {
//如果傳入的 Runnable 實例上炎,那么會調(diào)用調(diào)用 runnable 實例的 run 方法
target.run();
}
}
yield()
Java線程中的 Thread.yield()
方法恃逻,譯為線程讓步雏搂。顧名思義,就是說當(dāng)一個線程使用了這個方法之后寇损,它會放棄自己CPU執(zhí)行權(quán)凸郑,讓
自己或者
其它的線程運行,注意是讓自己或者其他線程運行矛市,并不是單純的讓給其他線程芙沥。
yield()的作用是
讓步。它能讓當(dāng)前線程由“運行狀態(tài)”進入到“就緒狀態(tài)”浊吏,從而讓其它具有相同優(yōu)先級的等待線程獲取執(zhí)行權(quán)而昨;但是,并不能保證在當(dāng)前線程調(diào)用
yield()`之后找田,其它具有相同優(yōu)先級的線程就一定能獲得執(zhí)行權(quán)歌憨;也有可能是當(dāng)前線程又進入到“運行狀態(tài)”繼續(xù)運行!
public class YieldDemo {
public static void main(String[] args) {
Yieldtask yieldtask = new Yieldtask();
Thread t1 = new Thread(yieldtask, "A");
Thread t2 = new Thread(yieldtask, "B");
Thread t3 = new Thread(yieldtask, "C");
Thread t4 = new Thread(yieldtask, "D");
t1.start();
t2.start();
t3.start();
t4.start();
}
public static class Yieldtask implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "-" + i);
if (i == 5) {
Thread.yield();
}
}
}
}
}
從運行結(jié)果可以看到墩衙,調(diào)用 yield()
方法的線程之后务嫡,CPU 執(zhí)行權(quán)不一定會給其他線程
搶到,有可能還是當(dāng)前線程
搶到 CPU 執(zhí)行權(quán)底桂。
...
A-0
D-4
D-5
D-6//在這里植袍,還是執(zhí)行 D 這個線程
D-7
D-8
D-9
B-2
B-3
B-4
B-5
C-6
B-6
A-1
A-2
A-3
A-4
...
join()
join() 把指定的線程加入到當(dāng)前線程,可以將兩個交替執(zhí)行的線程合并為順序執(zhí)行的線程籽懦。比如在線程B中調(diào)用了線程A的Join()方法于个,直到線程A執(zhí)行完畢后,才會繼續(xù)執(zhí)行線程B暮顺。
這里舉一個栗子:午飯時間到了厅篓,老王(線程A)調(diào)用了微波爐熱飯的方法,預(yù)約了4分鐘捶码,當(dāng)微波爐跑了2分鐘羽氮,這時老王看到一個美女(線程B)過來,這時主動調(diào)用了線程B.join()方法惫恼,此時把微波爐讓給了美女档押,這時老王就等待美女熱飯,直到熱好美女的飯之后祈纯,才輪到老王去繼續(xù)熱飯令宿,這就是一個 join()
方法的作用。
下面畫了一個草圖:
public class JoinDemo implements Runnable {
public static void main(String[] args) throws InterruptedException, ExecutionException {
JoinDemo joinDemo = new JoinDemo();
Thread thread = new Thread(joinDemo);
thread.start();
//join() 會阻塞當(dāng)前線程腕窥,等待子線程執(zhí)行完畢
//在這里主線程會等待子線程執(zhí)行完畢之后才能往下執(zhí)行粒没。
thread.join();
System.out.println(Thread.currentThread().getName() + " " + " done!");
}
@Override
public void run() {
try {
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName()+" done!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
執(zhí)行結(jié)果:
Thread-0 done!
main done簇爆!
線程狀態(tài)
在 Java Thread 類中定義了一個 State 枚舉癞松,內(nèi)部定義以下6個線程狀態(tài)爽撒。
在線程的生命周期中,它要經(jīng)過新建(New)响蓉、就緒(Runnable)硕勿、運行(Running)、阻塞(TIME_WAITING,WAITING)和死亡(TERMINATED)五種狀態(tài)厕妖。
我這里繪制一個草圖首尼,描述了各個狀態(tài)之前的切換:
- 新建狀態(tài)(NEW)
新建一個線程對象挑庶,此時并沒有執(zhí)行 start() 方法言秸,這時的線程狀態(tài)就是處于新建狀態(tài)。
Thread thread = new Thread(){
public void run() {...}
};
- 就緒狀態(tài)(RUNNABLE)
start() 方法調(diào)用之后會讓一個線程進入就緒等待隊列迎捺,JVM 會為其創(chuàng)建對應(yīng)的虛擬機棧举畸,本地方法棧和程序計數(shù)器。處于這個狀態(tài)的線程還沒開始運行凳枝,需要等待 CPU 的執(zhí)行權(quán)抄沮。
- 運行狀態(tài)(RUNNING)
處于就緒狀態(tài)的線程在搶到 CPU 執(zhí)行權(quán)之后,就處于運行狀態(tài)岖瑰,執(zhí)行該線程的 run 方法叛买。
- 阻塞狀態(tài)(BLOCKED)
TIME_WAIT/WAIT:
運行的線程執(zhí)行 wait() /wait(long),join()/join(long)方法,那么這些線程放棄 CPU 執(zhí)行和線程持有的鎖蹋订,并且 JVM 會將這些線程放入到
等待池
中率挣。
BLOCKED:
運行時的線程在獲取同步鎖時,發(fā)現(xiàn)鎖被其他線程持有露戒,這時 JVM 會將當(dāng)前線程放入
鎖池
中椒功。
- 結(jié)束(TERMINATED)
線程
run
方法執(zhí)行完畢,或者線程被異常終止智什。
總結(jié)
以上是關(guān)于 Java 多線程的一些基本知識點的總結(jié)动漾,有任何不對的地方,請多多指正荠锭。
記錄于2019年4月7日