CPU 核心數和線程數的關係
目前的 CPU 有雙核,四核,八核,一般情況下,它和線程數是1:1的對應關係,也就是四核 CPU 一般就能並行執行 4 個線程。但 Intel 引入超線程技術後,使核心數與線程數形成1:2的關係,也就是我們常說的
4核8線程
線程調度與線程調度模型
任意時刻,只有一個線程佔用 CPU,處於運行狀態。而多線程併發執行就是輪流獲取 CPU 執行權。
- 分時調用模型
輪流獲取 CPU 執行權,均分 CPU 執行時間。
- 搶佔式調度模型
優先級高的線程優先獲取 CPU 執行權,這也是
JVM
採用的線程調度模型。
進程與線程
進程是程序運行資源分配
的最小單位。
這些資源就包括
CPU
,內存空間,磁盤 IO 等。同一進程中的多個線程共享該進程的所有資源,而不同進程是彼此獨立的。舉個栗子,在手機開啓一個 APP 實際上就是開啓了一個進程了,而每一個 APP 程序會有很多線程在跑,例如刷新 UI 的線程等,所以說進程是包含線程的。
線程是 CPU 調度
的最小單位,必須依賴於進程而存在。
線程是比進程更小的,能獨立運行的基本單位,每一個線程都有一個程序計數器,虛擬機棧等,它可以與同一進程下的其它線程共享該進程的資源。
並行與併發
- 並行
指應用能夠同時執行不同的任務。例如多輛汽車可以同時在同一條公路上的不同車道上並行通行。
- 併發
指應用能夠交替執行不同的任務,因爲一般的計算機只有一個 CPU 也就是隻有一顆心,如果一個 CPU 要運行多個進程,那就需要使用到併發技術了,例如時間片輪轉進程調度算。比如單 CPU 核心下執行多線程任務時並非同時執行多個任務,而是以一個非常短的時間不斷地切換執行不同的任務,這個時間是我們無法察覺的出來的。
兩者的區別:並行是同時執行,併發是交替執行。
高併發編程的意義
- 充分利用 CPU 資源
線程是 CPU 調度的最小的單位,我們的程序是跑在 CPU 的一個核中的某一個線程中的,如果在程序中只有一個線程,那麼對於雙核心4線程的CPU來說就要浪費了 3/4 的 CPU 性能了,所以在創建線程的時候需要合理的利用 CPU 資源,具體可以看看 AsyncTask 內部的線程池是如何設計的。
- 加快響應用戶的時間
如果多個任務時串行執行的話,那麼效果肯定不好,在移動端開發中,併發執行多個任務是很常見的操作,最常見的就是多線程下載了。
- 可以使代碼模塊化,異步化,簡單化
在 Android 應用程序開發中的,一般 UI 線程負責去更新界面相關的工作,而一些 IO,網絡等操作一般會放在異步的工作線程去執行,這樣使得不同的線程各司其職,異步化。
線程之間的安全性問題
問題1:同一進程間的多個線程是可以共享該進程的資源的,當多個線程訪問共享變量時,就會線程安全問題。
問題2:爲了解決線程之間的同步問題,一般會引入鎖機制,對於線程之間搶奪鎖時也是有可能造成死鎖問題。
問題3:在 JVM 內存模型中,每一個線程都會分配一個虛擬機棧,這個虛擬機棧是需要佔用內存空間的,如果無限制的創建線程的話,會耗盡系統的內存。
線程的開啓與關閉
線程的啓動
- 派生 Thread 類
//開啓一個線程
Thread thread = new Thread() {
@Override
public void run() {
super.run();
System.out.println("thread started");
}
};
thread.start();
- 實現 Runnable 接口,將其交給 Thread 類去執行
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("runnable run invoked");
}
});
thread.start();
- 實現 Callable 接口
因爲 Thread 構造中只接收 Runnable 類型的接口,需要實現將 Callable 的實現類包裝爲 FutureTask 之後交給 Thread 類去執行。
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 方法執行完畢。
String resule = futureTask.get();
} catch (ExecutionException e) {
e.printStackTrace();
}
再來看 FutureTask 的應用:我們觀察到 AsyncTask 內部就使用到了 FutureTask ,因爲在 doInBackground() 需要有一個返回值,而恰好 Callable 就可以實現子線程執行完有返回值。使用 FutureTask 來封裝 WorkRunnable 對象,然後再交給對應的線程池去執行。具體的代碼如下:
總結:對於第1,2兩種方式是在線程執行完畢後,無法得到執行的結果,而第三種方式是可以獲取執行結果的。
線程的終止
- 線程自然終止
也就是 run 執行完畢,或者是內部出現一個異常導致線程提前結束。
-
暴力終止
-
suspend() 使線程掛起,並且不會釋放線程佔有的資源(例如鎖),resume() 使掛起的線程恢復。
-
stop() 暴力停止,立刻釋放鎖,導致共享資源可能不同步。
-
以上幾個方法已經被 JDK 標記爲廢棄狀態。
- interrupt() 安全終止
第一種情況:如果線程處於正常運行狀態,那麼線程的中斷狀態會被置爲 true ,並且線程還是會正常執行,僅此而已。
第二種情況:如果當前線程如果是處於阻塞狀態,例如調用了 wait,join,sleep 等方法,那麼則會拋出InterruptedException
異常。
總結: interrupt() 並不能中斷線程,需要線程配合才能實現中斷操作。
示例01
public class EndThread extends Thread {
@Override
public void run() {
super.run();
System.out.println("isInterrupted:"+isInterrupted());
while (true) {//儘管在其他線程中調用了 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);
//在其他線程中去調用線程的interrupt方法給線程打一個終止的標記
endThread.interrupt();
}
}
上面的代碼時在線程體中執行一個 while(true)的死循環,然後在其他線程中調用 endThread.interrupt()
觀察當前線程是否會執行完畢。
經測試:在調用線程的 interrupt()方法之後,while(true)是不會結束循環的,也就是線程還是一直在運行着。所以說 interrupt() 並不會應用 run 方法的執行
示例02
下面再來看看另一個關於 interrupt 方法的使用
在線程體內部 sleep(2000) 並且 try catch 對應的 InterruptedException 異常,如果在其他線程調用了 endThread.interrupt() 那麼此處就會拋出 InterruptedException 異常,並且會isInterrupted() 會返回 false 。
public class EndThread2 extends Thread {
@Override
public void run() {
super.run();
System.out.println("isInterrupted:" + isInterrupted());
try {
//在其他線程調用 endThread.interrupt() 之後,會拋出 InterruptedException 異常並且線程的
// isInterrupted 會被標記爲 false。因此最後輸出的結果還是 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);
//在其他線程中去調用線程的interrupt方法給線程打一個終止的標記
endThread.interrupt();
}
}
執行結果:
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()){},在外界調用了 endThread.interrupt()
之後,線程的 isInterrupted() 就會返回 true,標記着你可以結束線程了。
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);
//在其他線程中去調用線程的interrupt方法給線程打一個終止的標記
endThread.interrupt();
)
}
}
示例04
還有一種方式是設置一個 boolean
類型的變量 mIsExit
標記,當線程體內部判斷到 mIsExit
爲 false 那麼就跳出循環
。具體示例代碼如下:
public class EndThread extends Thread {
//這個變量需要在其他線程中判斷,因此需要設置爲線程可見的
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);
//設置標記爲退出狀態
endThread.mIsExit = false;
}
}
線程其他 API
Thread#start() 與 Thread#run()
start() 方法調用之後會讓一個線程進入就緒等待隊列
,當獲取到 CPU 執行權之後會執行線程體 run()方法。
run() 方法只是 Thread 類中一個普通方法
,如果手動去調用,跟調用普通方法沒有什麼區別。
Thread#run() 與 Runnable#run()
在 Java 中只有 Thread 才能表示一個線程,而 Runnable 只能表示一個任務
,任務是需要交給線程去執行的,當出現如下代碼時,你看到可能會懵逼,執行結果到底是什麼?
//接受一個 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();
以上方式的輸出結果是:
如果線程 run 方法內部調用了 super.run() 那麼輸出結果如下:
runnable run invoked
thread started
如果線程 run 方法內部不調用 super.run() 那麼輸出結果如下:
thread started
我們可以通過源碼來解答這個問題:在創建線程時,如果往構造函數中傳入一個
Runnable
對象,那麼它會給線程target
屬性賦值,並且在線程體
執行時先判斷target
是否爲空,不爲空,則先執行Runnable
的run
方法,再執行當前線程體的子類中的 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 實例,那麼會調用調用 runnable 實例的 run 方法
target.run();
}
}
yield()
Java線程中的 Thread.yield()
方法,譯爲線程讓步。顧名思義,就是說當一個線程使用了這個方法之後,它會放棄自己CPU執行權,讓
自己或者
其它的線程運行,注意是讓自己或者其他線程運行,並不是單純的讓給其他線程。
yield()的作用是
讓步。它能讓當前線程由“運行狀態”進入到“就緒狀態”,從而讓其它具有相同優先級的等待線程獲取執行權;但是,並不能保證在當前線程調用
yield()`之後,其它具有相同優先級的線程就一定能獲得執行權;也有可能是當前線程又進入到“運行狀態”繼續運行!
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();
}
}
}
}
}
從運行結果可以看到,調用 yield()
方法的線程之後,CPU 執行權不一定會給其他線程
搶到,有可能還是當前線程
搶到 CPU 執行權。
...
A-0
D-4
D-5
D-6//在這裏,還是執行 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() 把指定的線程加入到當前線程,可以將兩個交替執行的線程合併爲順序執行的線程。比如在線程B中調用了線程A的Join()方法,直到線程A執行完畢後,纔會繼續執行線程B。
這裏舉一個栗子:午飯時間到了,老王(線程A)調用了微波爐熱飯的方法,預約了4分鐘,當微波爐跑了2分鐘,這時老王看到一個美女(線程B)過來,這時主動調用了線程B.join()方法,此時把微波爐讓給了美女,這時老王就等待美女熱飯,直到熱好美女的飯之後,才輪到老王去繼續熱飯,這就是一個 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() 會阻塞當前線程,等待子線程執行完畢
//在這裏主線程會等待子線程執行完畢之後才能往下執行。
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();
}
}
}
執行結果:
Thread-0 done!
main done!
線程狀態
在 Java Thread 類中定義了一個 State 枚舉,內部定義以下6個線程狀態。
在線程的生命週期中,它要經過新建(New)、就緒(Runnable)、運行(Running)、阻塞(TIME_WAITING,WAITING)和死亡(TERMINATED)五種狀態。
我這裏繪製一個草圖,描述了各個狀態之前的切換:
- 新建狀態(NEW)
新建一個線程對象,此時並沒有執行 start() 方法,這時的線程狀態就是處於新建狀態。
Thread thread = new Thread(){
public void run() {...}
};
- 就緒狀態(RUNNABLE)
start() 方法調用之後會讓一個線程進入就緒等待隊列,JVM 會爲其創建對應的虛擬機棧,本地方法棧和程序計數器。處於這個狀態的線程還沒開始運行,需要等待 CPU 的執行權。
- 運行狀態(RUNNING)
處於就緒狀態的線程在搶到 CPU 執行權之後,就處於運行狀態,執行該線程的 run 方法。
- 阻塞狀態(BLOCKED)
TIME_WAIT/WAIT:
運行的線程執行 wait() /wait(long),join()/join(long)方法,那麼這些線程放棄 CPU 執行和線程持有的鎖,並且 JVM 會將這些線程放入到
等待池
中。
BLOCKED:
運行時的線程在獲取同步鎖時,發現鎖被其他線程持有,這時 JVM 會將當前線程放入
鎖池
中。
- 結束(TERMINATED)
線程
run
方法執行完畢,或者線程被異常終止。
總結
以上是關於 Java 多線程的一些基本知識點的總結,有任何不對的地方,請多多指正。
記錄於2019年4月7日