1. 區分概念:併發和並行
- 併發是邏輯上的同時發生,並行更多是側重於物理上的同時發生。
- 併發往往是指程序代碼的結構支持併發,併發的程序在多cpu上運行起來纔有可能達到並行,並行往往是描述運行時的狀態。
- 在操作系統中,併發是指一個時間段中有幾個程序都處於已啓動運行到運行完畢之間,且這幾個程序都是在同一個處理機上運行,但任一個時刻點上只有一個程序在處理機上運行。
- 併發關注的三個問題:
- 安全性,也就是正確性,指的是程序在併發情況下執行的結果和預期一致
- 活躍性,比如死鎖,活鎖
- 性能,減少上下文切換,減少內核調用,減少一致性流量等等
2. 線程定義
- 進程:運行中的程序。
- 線程:是進程中的一個實體,作爲系統調度和分派的基本單位。
- 現代操作系統調度的最小單元是線程,也叫輕量級進程(Light Weight Process,簡稱LWP),在一個進程裏可以創建多個線程,這些線程都擁有各自的計數器、堆棧和局部變量等屬性,並且能夠訪問共享的內存變量。處理器在這些線程上高速切換,讓使用者感覺到這些線程在同時執行。
當一個java程序從main()方法開始執行的時候,即啓動了一個名字爲main的線程。
3. 多線程的優勢以及風險
優勢:
- 更好的利用多處理器核心
- 縮短響應時間,提升用戶體驗
- 多線程爲開發人員提供更好的編程模型
風險:
- 安全風險:資源競爭造成的安全問題
- 活躍度的危險:某部分代碼永遠不執行,例如死鎖、飢餓、活鎖
- 性能的風險:上下文切換的開銷
線程安全問題:
- 線程安全的本質,其實是共享變量,也就是狀態,有狀態的多線程訪問就需要同步機制來保證線程安全。
- 如果多線程訪問一個類時,若該類在沒有額外的同步代碼時,行爲仍然是正確的,則這個類時線程安全的。例如servlet這種無狀態的類永遠是線程安全的(servlet沒有任何域(成員變量),只有存儲在每個線程棧中的局部變量,所以是無狀態的)
4. 守護線程(Daemon)和用戶線程(User)
- 守護線程(後臺線程)的定義:
是程序運行時在後臺提供服務的線程,並不屬於程序中不可或缺的部分。當所有非後臺線程結束時,程序也就終止,同時會殺死所有後臺線程 - 任何線程都可以設置爲守護線程和用戶線程,通過方法Thread.setDaemon(bool on);參數爲true則把該線程設置爲守護線程,反之則爲用戶線程;Thread.setDaemon()必須在Thread.start()之前調用,否則運行時會拋出異常。
- Main線程是非守護線程,即用戶線程。
- 守護線程與用戶線程的區別
唯一的區別是判斷虛擬機(JVM)何時離開,Daemon是爲其他線程提供服務,如果全部的User Thread已經撤離,Daemon 沒有可服務的線程,JVM撤離。也可以理解爲守護線程是JVM自動創建的線程(但不一定),用戶線程是程序創建的線程;比如JVM的垃圾回收線程是一個守護線程,當所有線程已經撤離,不再產生垃圾,守護線程自然就沒事可幹了,當垃圾回收線程是Java虛擬機上僅剩的線程時,Java虛擬機會自動離開。
5. 使用線程
有三種使用線程的方法
- 實現 Runnable 接口;
- 實現 Callable 接口;
- 繼承 Tread 類;
1. 實現Runnable接口
public class MyRunnable implements Runnable {
public void run() {
// ...
}
public static void main(String[] args) {
MyRunnable instance = new MyRunnable();
Tread thread = new Thread(instance);
thread.start();
}
}
2. 實現 Callable 接口
- 與 Runnable 相比,Callable 可以有返回值,返回值通過 FutureTask 進行封裝。
public class MyCallable implements Callable<Integer> {
public Integer call() {
// ...
}
public static void main(String[] args) {
MyCallable mc = new MyCallable();
FutureTask<Integer> ft = new FutureTask<>(mc);
Thread thread = new Thread(ft);
thread.start();
System.out.println(ft.get());
}
}
3. 繼承 Tread 類
- 同樣也是需要實現 run() 方法,並且最後也是調用 start() 方法來啓動線程。
class MyThread extends Thread {
public void run() {
// ...
}
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();
}
}
5. 實現接口 vs 繼承 Thread
- 實現接口會更好一些,因爲:
- Java 不支持多重繼承,因此繼承了 Thread 類就無法繼承其它類,但是可以實現多個接口。
- 類可能只要求可執行即可,繼承整個 Thread 類開銷會過大。
6. 線程相關易錯點
- 啓動線程通過在線程的Thread對象上調用start()方法,而不是run()或者別的方法。
- 在調用start()方法之前:線程處於新狀態中,新狀態指有一個Thread對象,但還沒有一個真正的線程。
- 在調用start()方法之後:發生了一系列複雜的事情
啓動新的執行線程(具有新的調用棧);該線程從新建狀態轉移到可運行狀態;當該線程獲得機會執行時,其目標run()方法將運行; - 注意:對Java來說,run()方法沒有任何特別之處。像main()方法一樣,它只是新線程知道調用的方法名稱(和簽名)。因此,在Runnable上或者Thread上調用run方法是合法的。但並不啓動新的線程。
6. 線程的生命週期與調度
1. 線程的生命週期
- NEW(新建)
- RUNNABLE(當線程正在運行或者已經就緒正等待 CPU 時間片)(正在運行和就緒放用同一個狀態表示)
- BLOCKED(阻塞,線程在等待獲取對象同步鎖)
- WAITING(調用不帶超時的 wait() 或 join())
- TIMED_WAITING(調用 sleep()、帶超時的 wait() 或者 join())
- TERMINATED(死亡)
2. 線程休眠sleep
- Thread.sleep(long millis)方法,使線程轉到阻塞狀態。millis參數設定睡眠的時間,以毫秒爲單位。當睡眠結束後,就轉爲就緒(Runnable)狀態。sleep()平臺移植性好。
- sleep() 可能會拋出 InterruptedException。因爲異常不能跨線程傳播回 main() 中,因此必須在本地進行處理。線程中拋出的其它異常也同樣需要在本地進行處理。
public void run() {
try {
// ...
Thread.sleep(1000);
// ...
} catch(InterruptedException e) {
System.err.println(e);
}
}
2. 線程讓步yield
- Thread.yield() 方法,讓線程由運行狀態變爲就緒狀態,暫停當前正在執行的線程對象,把執行機會讓給相同或者更高優先級的線程。但是,實際中無法保證yield()達到讓步目的,因爲讓步的線程還有可能被線程調度程序再次選中。
- 實際上yield的流程是: 先檢測當前是否有相同優先級的線程處於同可運行狀態,如有,則把 CPU的佔有權交給此線程,否則,繼續運行原來的線程
public void run() {
// ...
Thread.yield();
}
2. 線程合併join
- join()方法,等待其他線程終止。在當前線程中調用另一個線程的join()方法,則當前線程轉入阻塞狀態,直到另一個進程運行結束,當前線程再由阻塞轉爲就緒狀態。
- Join常用來讓主線程等待子線程執行完畢才繼續執行,例如下面這個例子:
public class Main {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName()+"主線程運行開始!");
Thread1 mTh1=new Thread1("A");
Thread1 mTh2=new Thread1("B");
mTh1.start();
mTh2.start();
try {
mTh1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
mTh2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+ "主線程運行結束!");
}
}
- 打印結果如下:join掛起後,主線程要等到子線程結束後才繼續運行
main主線程運行開始!
A 線程運行開始!
子線程A運行 : 0
B 線程運行開始!
子線程B運行 : 0
子線程A運行 : 1
子線程B運行 : 1
子線程A運行 : 2
子線程B運行 : 2
子線程A運行 : 3
子線程B運行 : 3
子線程A運行 : 4
子線程B運行 : 4
A 線程運行結束!
3. 線程中斷interrupt
涉及到的三個方法:
- void interrupt() : 中斷線程
- static boolean interrupted() : 測試當前線程是否已經中斷
- boolean isInterrupted() : 測試線程是否已經中斷(注意與上一個區分)
interrupt()的實際用處:(容易誤解)
- 不要以爲它是中斷某個線程!它只是讓線程發送一箇中斷信號,讓線程在無限等待時(如死鎖時)能拋出拋出,從而結束線程,但是如果你喫掉了這個異常,那麼這個線程還是不會中斷的!
- 代碼實例如下:
public class InterruptTest{
public static void main(String[] args) throws Interruption {
MyThread t = new MyThread("MyThread");
t.start();
Thread.sleep(100);//睡眠100毫秒
t.interrupt();//通知線程t中斷
}
}
class MyThread extends Thread{
int i = 0;
public MyThread(String name){
super(name);
}
public void run(){
while(!isInterrupted()){
System.out.println(getName() + "執行了" + ++i + "次");
}
}
}
- 通過interrupt()方法,線程t收到中斷信號,設置中斷狀態,接着在0.1秒後響應中斷而停止
- 注意:如果線程在調用 Object 類的 wait()、wait(long) 或 wait(long, int) 方法,或者該類的 join()、join(long)、join(long, int)、sleep(long) 或 sleep(long, int) 方法過程中受阻,則其中斷狀態將被清除,並拋出一個 InterruptedException。 我們可以捕獲該異常,並且做一些處理。
- 另外,Thread.interrupted()方法是一個靜態方法,它是判斷當前線程的中斷狀態,需要注意的是,線程的中斷狀態會由該方法清除。換句話說,如果連續兩次調用該方法,則第二次調用將返回 false(在第一次調用已清除了其中斷狀態之後,且第二次調用檢驗完中斷狀態前,當前線程再次中斷的情況除外)。
- interrupt()有設置中斷狀態的作用,對於沒有被阻塞的線程,通過isInterrupt()判斷它可以起幫助我們自行結束代碼;對於被阻塞的線程,它可以讓線程從停止阻塞,並拋出InterruptedException異常。
7. 線程之間的通信
以下幾個方法屬於基類(Object)的一部分,而不獨屬於 Thread,注意要與線程調度那幾個方法區分開。
1. 線程等待wait
- wait() 會在等待時將線程掛起,而不是忙等待,並且只有在 notify() 或者 notifyAll() 到達時才喚醒。
- sleep() 和 yield() 並沒有釋放鎖,但是 wait() 會釋放鎖。實際上,只有在同步控制方法或同步控制塊裏才能調用 wait() 、notify() 和 notifyAll()。
2. 線程喚醒notify與notifyAll
Object類中的notify()方法,喚醒在此對象監視器上等待的單個線程。如果所有線程都在此對象上等待,則會選擇喚醒其中一個線程。選擇是任意性的,並在對實現做出決定時發生。線程通過調用其中一個 wait 方法,在對象的監視器上等待。 直到當前的線程放棄此對象上的鎖定,才能繼續執行被喚醒的線程。被喚醒的線程將以常規方式與在該對象上主動同步的其他所有線程進行競爭。類似的方法還有一個notifyAll(),喚醒在此對象監視器上等待的所有線程。
Wait和sleep經典示例:三個線程交替 打印ABCABC。。。
public class MyThreadPrinter2 implements Runnable {
private String name;
private Object prev;
private Object self;
private MyThreadPrinter2(String name, Object prev, Object self) {
this.name = name;
this.prev = prev;
this.self = self;
}
@Override
public void run() {
int count = 10;
while (count > 0) {
synchronized (prev) {
synchronized (self) {
System.out.print(name);
count--;
self.notify();//喚醒當前被self對象鎖阻塞的線程
}
try {
prev.wait();//讓當前持有prev鎖的(自己)線程阻塞掉,釋放鎖
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws Exception {
Object a = new Object();
Object b = new Object();
Object c = new Object();
MyThreadPrinter2 pa = new MyThreadPrinter2("A", c, a);
MyThreadPrinter2 pb = new MyThreadPrinter2("B", a, b);
MyThreadPrinter2 pc = new MyThreadPrinter2("C", b, c);
new Thread(pa).start();
Thread.sleep(100); //確保按順序A、B、C執行
new Thread(pb).start();
Thread.sleep(100);
new Thread(pc).start();
Thread.sleep(100);
}
}
- 程序運行的主要過程:A線程最先運行,持有C,A對象鎖,後釋放A,C鎖,喚醒B。線程B等待A鎖,再申請B鎖,後打印B,再釋放B,A鎖,喚醒C,線程C等待B鎖,再申請C鎖,後打印C,再釋放C,B鎖,喚醒A。
3. wait、sleep、yield方法的區別
- sleep()是Thread類的static(靜態)的方法;wait()方法是Object類裏的方法;有一個易錯的地方,當調用t.sleep()的時候,會暫停線程t。這是不對的,因爲Thread.sleep是一個靜態方法,它會使當前線程而不是線程t進入休眠狀態。
- sleep()睡眠時,保持對象鎖,仍然佔有該鎖;wait()睡眠時,釋放對象鎖
- 在sleep()休眠時間期滿後,該線程不一定會立即執行,這是因爲其它線程可能正在運行而且沒有被調度爲放棄執行,除非此線程具有更高的優先級;
- wait()必須放在同步代碼塊中,否則會在runtime時扔出IllegalMonitorStateException異常; 而sleep()沒有這個要求,但是也可能會拋出InterruptedException異常。
- yield()是也是屬於Thread類的方法,讓線程由運行狀態變爲就緒狀態,暫停當前正在執行的線程對象,把執行機會讓給相同或者更高優先級的線程,這就意味着yield不會讓調用者阻塞,而是繼續保持就緒狀態,甚至繼續運行。