1 引言
線程是進程中可獨立執行的最小單位,也是CPU資源分配的基本單位。
線程的四個基本屬性:
屬性 | 描述 |
---|---|
編號id | 線程的唯一標識 |
名稱 | 線程的名字,默認“Thread-編號id”,可自定義 |
類別 | 分爲守護線程和用戶線程,可以通過setDaemon(true) 設置爲守護線程 |
優先級 | 表示希望哪個線程優先執行,Java中優先級取值範圍是1~10,默認5 |
2 Java線程對象Thread常用的方法
1 start()
表示啓動線程,讓CPU調度器調度開啓線程持行run()方法。而直接調用run()方法只是運行當前run方法,不會開啓線程。
附上源碼:
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
private native void start0();
通過源碼,不難發現start()
方法實際上是調用start0()
方法啓動線程的,而start0()
是native
方法。
簡單說下native的原理及實現機制:
native修飾的表示本地方法,一個Native Method就是一個java調用非java代碼(如C代碼)的接口,Java中的Native Method並不提供實現體。
當一個類第一次被使用到時,這個類的字節碼會被加載到內存,並且只會會被載一次。在這個被加載的字節碼的入口維持着一個該類所有方法描述符的list,這些方法描述符包含這樣一些信息:方法代碼存於何處,它有哪些參數,方法的描述符(public之類)等等。
如果一個方法描述符內有native,這個描述符塊將有一個指向該方法的實現的指針。這些實現在一些DLL文件內,但是它們會被操作系統加載到Java程序的本地方法棧。當一個帶有本地方法的類被加載時,其相關的DLL並未被加載,因此指向方法實現的指針並不會被設置。只有當本地方法被調用時,這些DLL纔會被加載,這是通過調用java.system.loadLibrary()實現的。
此外,不可不說的是,本地方法意味着和平臺有關,因此使用了native的程序可移植性都不太高,並且本地方法是有開銷的,它喪失了Java的很多優勢。
2 sleep()
線程暫停阻塞等待一段時間,時間過了就繼續。該方法是不釋放鎖的。
附源碼:
public static native void sleep(long millis) throws InterruptedException
同樣的,sleep()
也是本地方法,底層也是通過C代碼調用操作系統實現的
3 yield()
t.yield()
表示t線程主動讓出CPU執行權進入就緒態
,所有線程重新爭搶CPU(可能出現又是t線程搶到CPU執行權)。
附源碼:
public static native void yield();
4 join()
t2線程正在運行,這時在t2線程中調用 t1.join()
,這是讓線程t2等待線程t1執行結束再運行。
此方法可以讓線程按順序持行。自己線程中調用自己的join()方法是無實際意義的。
附源碼:
public final void join() throws InterruptedException {
join(0);
}
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
join(long timeout)
方法底層調用的是Object
類的wait(long timeout)
方法。
public final native void wait(long timeout) throws InterruptedException;
Object
類的wait()
方法是釋放鎖的。
3 Java線程運行狀態
過程分析:
-
new 的狀態(NEW)
-
start() 之後線程到達就緒狀態等待CPU調度,CPU調度了 就是運行狀態。(RUNNABLE)
-
調用了 TImedWaiting 就回到就緒狀態,結束之後自動到運行狀態(TIMED_WAITING)
-
調用 Waiting 就緒然後持行相應的方法到運行(WAITING)
-
調用 Blocked 等待進入同步代碼快的鎖。(BLOCKED)
-
結束狀態Teminated(TERMINATED)
線程被掛起:正在CPU中調度的線程,CPU把該線程丟出去,持行其他線程,這就是線程被掛起。之後又回來持續該線程。(每個CPU每次只能持行一個線程)
4 Java線程調度原理
我們知道,在任意時刻,操作系統的CPU 只能執行一條機器指令,並且每個線程只有獲取到 CPU 的使用權後,纔可以執行指令(也就是說在任意時刻,只有一個線程佔用 CPU,處於運行的狀態
)。
多線程併發運行實際上是指多個線程輪流獲取 CPU 使用權,分別執行各自的任務。
線程的調度由 JVM 負責。
線程調度模型分爲兩類:時間片輪轉調度模型和搶佔式調度模型。JVM採用的是搶佔式調度模型。
- 時間片輪轉調度模型
時間片輪轉調度模型是讓所有線程輪流獲取 CPU 使用權,並且爲每個線程分配相同的佔用 CPU 的時間片,時間片用完了則線程自動放棄CPU。
- 搶佔式調度模型
JVM 採用的是搶佔式調度模型,根據優先級來選擇佔用CPU,優先級越高線程被持行的機率就越大,如果線程的優先級都一樣,那就隨機選擇一個線程,並讓該線程佔用 CPU。也就是如果我們同時啓動多個線程,並不能保證它們能輪流獲取到均等的時間片。
如果我們的程序想幹預線程的調度過程,最簡單的辦法就是給每個線程設定一個優先級。
5 線程創建的四種方式
5.1 繼承Thread類創建線程
/**
* @author Carson
* @date 2020/4/3 10:44
*/
public class MyThread {
public static void main(String[] args) {
new ThreadGenerator().start();
}
}
class ThreadGenerator extends Thread {
@Override
public void run() {
System.out.println("Starting thread>>>>>>>>>>>>>>>>>>>>>>");
}
}
該方式的優缺點在於:
- 優點:編寫簡單
- 缺點:線程類已經繼承了
Thread
,不能再繼承別的類。
5.2 實現Runnable接口創建線程
/**
* @author Carson
* @date 2020/4/3 10:44
*/
public class MyThread {
public static void main(String[] args) {
new Thread(new ThreadGenerator()).start();
}
}
class ThreadGenerator implements Runnable {
@Override
public void run() {
System.out.println("Starting thread>>>>>>>>>>>>>>>>>>>>>>");
}
}
當然,在使用Java8及更高版本時,可以直接使用Lambda表達式創建:
/**
* @author Carson
* @date 2020/4/3 11:22
*/
public class MyThread {
public static void main(String[] args) throws Exception {
new Thread(() -> {
System.out.println("Starting thread>>>>>>>>>>>>>>>>>>>>>>");
}).start();
}
}
5.3 使用Callable和Future創建線程
/**
* @author Carson
* @date 2020/4/3 10:44
*/
public class MyThread {
public static void main(String[] args) throws Exception {
ThreadGenerator threadGenerator = new ThreadGenerator();
FutureTask futureTask = new FutureTask(threadGenerator);
new Thread(futureTask).start();
}
}
class ThreadGenerator implements Callable {
@Override
public Object call() throws Exception {
System.out.println("Starting thread>>>>>>>>>>>>>>>>>>>>>>");
return true;
}
}
同樣的,也可以直接使用Lambda表達式創建:
/**
* @author Carson
* @date 2020/4/3 11:22
*/
public class MyThread {
public static void main(String[] args) {
FutureTask<Boolean> futureTask = new FutureTask<>(() -> {
System.out.println("Starting thread>>>>>>>>>>>>>>>>>>>>>>");
return true;
});
new Thread(futureTask).start();
}
}
實現Callable
接口和實現Runnable
接口的不同點在於:
- call()方法可以有返回值
- call()方法可以聲明並拋出異常
5.4 使用線程池,例如用Executor框架
/**
* @author Carson
* @date 2020/4/3 11:22
*/
public class MyThread {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(6);
/* execute方法不關心返回值。 */
executorService.execute(new ThreadGenerator());
/* submit方法有返回值-->Future */
executorService.submit(new ThreadGenerator());
}
}
class ThreadGenerator implements Runnable {
@Override
public void run() {
System.out.println("Starting thread>>>>>>>>>>>>>>>>>>>>>>");
}
}
在開發中推薦使用線程池的方式,用線程池管理線程既高效風險也較低。