一、進程 線程
進程:一個進程來對應一個程序,
每個進程對應一定的內存地址空間,並且只能使用它自己的內存空間,各個進程間互不干擾。
進程保存了程序每個時刻的運行狀態,這樣就為進程切換提供了可能。當進程暫停時,它會保存當前進程的狀態(比如進程標識、進程的使用的資源等),在下一次重新切換回來時,便根據之前保存的狀態進行恢復,然后繼續執行。
對於單核計算機來講,在同一個時間點上,游戲進程和音樂進程是同時在運行嗎?
不是。 因為計算機的 CPU 只能在某個時間點上做一件事。
由於計算機將在“游戲進程”和“音樂進程”之間頻繁的切換執行,切換速度極高,人類感覺游戲和音樂在同時進行。 多進程的作用不是提高執行速度,而是提高 CPU 的使用率。
線程:一個進程就包括了多個線程,每個線程負責一個獨立的子任務。這些線程是共同享有進程占有的資源和地址空間的。
這樣在用戶點擊按鈕的時候,就可以暫停獲取圖像數據的線程,讓UI線程響應用戶的操作,響應完之后再切換回來,讓獲取圖像的線程得到CPU資源。從而讓用戶感覺系統是同時在做多件事情的,滿足了用戶對實時性的要求。
進程讓操作系統的並發性成為可能,而線程讓進程的內部並發成為可能。
多線程優點:參考多線程的優點
資源利用率更好
程序設計在某些情況下更簡單
程序響應更快
多線程缺點:參考多線程的代價
設計更復雜
上下文切換的開銷
增加資源消耗
上下文切換:
對於單核CPU來說(對於多核CPU,此處就理解為一個核),CPU在一個時刻只能運行一個線程,當在運行一個線程的過程中轉去運行另外一個線程,這個叫做線程上下文切換(對於進程也是類似)。
由於可能當前線程的任務並沒有執行完畢,所以在切換時需要保存線程的運行狀態,以便下次重新切換回來時能夠繼續切換之前的狀態運行。
所以一般來說,線程上下文切換過程中會記錄程序計數器、CPU寄存器狀態等數據。
說簡單點的:對於線程的上下文切換實際上就是存儲和恢復CPU狀態的過程,它使得線程執行能夠從中斷點恢復執行。
雖然多線程可以使得任務執行的效率得到提升,但是由於在線程切換時同樣會帶來一定的開銷代價,並且多個線程會導致系統資源占用的增加,所以在進行多線程編程時要注意這些因素。
二、創建、啟動線程
繼承Thread類
public class Test { public static void main(String[] args) { MyThread thread = new MyThread(); thread.start(); } } class MyThread extends Thread{ private static int num = 0; public MyThread(){ num++; } @Override public void run() { System.out.println("主動創建的第"+num+"個線程"); } }
實現Runnable接口
public class Test { public static void main(String[] args) { Thread thread = new Thread(new MyRunnable()); thread.start(); } } class MyRunnable implements Runnable{ public MyRunnable() { } @Override public void run() { System.out.println("子線程ID:"+Thread.currentThread().getId()); } }
注意:創建並運行一個線程調用start()方法 而不是run()
三、Thread類/線程狀態
1)getId 用來得到線程ID
2)getName和setName 用來得到或者設置線程名稱。
3)getPriority和setPriority 用來獲取和設置線程優先級。
4)setDaemon和isDaemon
用來設置線程是否成為守護線程和判斷線程是否是守護線程。
守護線程和用戶線程的區別在於:守護線程依賴於創建它的線程,而用戶線程則不依賴。
舉個簡單的例子:如果在main線程中創建了一個守護線程,當main方法運行完畢之后,守護線程也會隨着消亡。而用戶線程則不會,用戶線程會一直運行直到其運行完畢。在JVM中,像垃圾收集器線程就是守護線程。
5)Thread類有一個比較常用的靜態方法currentThread()用來獲取當前線程。
6)start方法
7)run方法
8)sleep方法 sleep相當於讓線程睡眠,交出CPU,讓CPU去執行其他的任務。sleep方法不會釋放鎖。
9)yield方法
調用yield方法會讓當前線程交出CPU權限,讓CPU去執行其他的線程。不會釋放鎖。
調用yield方法並不會讓線程進入阻塞狀態,而是讓線程重回就緒狀態。
10)join方法
假如在main線程中,調用myThread.join()方法,則main方法會等待thread線程執行完畢或者等待一定的時間。
如果調用的是無參join方法,則等待thread執行完畢,如果調用的是指定了時間參數的join方法,則等待一定的事件。
11)interrupt方法
詳細講解中斷:
一行一行源碼分析清楚 AbstractQueuedSynchronizer (二)
單獨調用interrupt方法可以使得處於阻塞狀態的線程拋出一個異常,它可以用來中斷一個正處於阻塞狀態的線程。
舉例:
控制台輸出:
進入睡眠狀態
得到中斷異常
run方法執行完畢
public class Test { public static void main(String[] args) throws IOException { Test test = new Test(); MyThread thread = test.new MyThread(); thread.start(); try { Thread.currentThread().sleep(2000); } catch (InterruptedException e) { } thread.interrupt(); } class MyThread extends Thread{ @Override public void run() { try { System.out.println("進入睡眠狀態"); Thread.currentThread().sleep(10000); System.out.println("睡眠完畢"); } catch (InterruptedException e) { System.out.println("得到中斷異常"); } System.out.println("run方法執行完畢"); } } }
另:wait() notify() notifyAll()是Object類的方法
12)wait()
線程阻塞,JVM將該線程放置在目標對象的等待集合中。
釋放調用wait()對象的同步鎖,但是除此之外的其他鎖依然由該線程持有。
即使是在wait()對象多次嵌套同步鎖,所持有的可重入鎖也會完整的釋放。這樣,后面恢復的時候,當前的鎖狀態能夠完全地恢復。
object.wait() object.notify() object.notifyAll() 調用之前需要先拿到object鎖。
13)notify()
Java虛擬機從目標對象的等待集合中隨意選擇一個線程(稱為T,前提是等待集合中還存在一個或多個線程)並從等待集合中移出T。當等待集合中存在多個線程時,並沒有機制保證哪個線程會被選擇到。
調用notify()的線程釋放鎖,線程T競爭鎖,如果競爭到鎖,線程T從之前wait的點開始繼續執行。
14)notifyAll()
notifyAll方法與notify方法的運行機制是一樣的。對象等待集合中的所有線程都移出,進入可運行狀態。
15)LookSupport.park()和unpark()
LockSupport類是Java6(JSR166-JUC)引入的一個類,提供了基本的線程同步原語。LockSupport實際上是調用了Unsafe類里的函數,歸結到Unsafe里,只有兩個函數:
public native void park(boolean isAbsolute, long time); 掛起
public native void unpark(Thread jthread); 喚醒
unpark函數為線程提供“許可(permit)”,park函數則等待“許可”。這個有點像信號量,但是這個“許可”是不能疊加的,“許可”是一次性的。
比如線程B連續調用了三次unpark函數,當線程A調用park函數就使用掉這個“許可”,如果線程A再次調用park,則進入等待狀態。
注意,unpark函數可以先於park調用。比如線程B調用unpark函數,給線程A發了一個“許可”,那么當線程A調用park時,它發現已經有“許可”了,那么它會馬上再繼續運行。
park/unpark模型真正解耦了線程之間的同步,線程之間不再需要一個Object或者其它變量來存儲狀態,不再需要關心對方的狀態。(JUC中用到)
Thread類源碼剖析 中對Thread介紹更加詳細。
四、線程安全
競態條件:當多個線程同時訪問同一個資源,其中的一個或者多個線程對這個資源進行了寫操作,對資源的訪問順序敏感,就稱存在競態條件。多個線程同時讀同一個資源不會產生競態條件。
臨界區:導致競態條件發生的代碼區稱作臨界區。在臨界區中使用適當的同步就可以避免競態條件。
基本上所有的並發模式在解決線程安全問題時,都采用“序列化訪問臨界資源”的方案,即在同一時刻,只能有一個線程訪問臨界資源,也稱作同步互斥訪問。
通常來說,是在訪問臨界資源的代碼前面加上一個鎖,當訪問完臨界資源后釋放鎖,讓其他線程繼續訪問。
在Java中,提供了兩種方式來實現同步互斥訪問:synchronized和Lock。
五、Java同步塊(synchronized block)
Java中的同步塊用synchronized標記。
同步塊在Java中是同步在某個對象上(監視器對象)。
所有同步在一個對象上的同步塊在同時只能被一個線程進入並執行操作。
所有其他等待進入該同步塊的線程將被阻塞,直到執行該同步塊中的線程退出。
(注:不要使用全局對象(常量等)做監視器。應使用唯一對應的對象)
public class MyClass { int count; // 1.實例方法 public synchronized void add(int value){ count += value; } // 2.實例方法中的同步塊 (等價於1) public void add(int value){ synchronized(this){ count += value; } } // 3.靜態方法 public static synchronized void add(int value){ count += value; } // 4.靜態方法中的同步塊 (等價於3) public static void add(int value){ synchronized(MyClass.class){ count += value; } } }
六、線程通信
線程通信的目標是使線程間能夠互相發送信號。另一方面,線程通信使線程能夠等待其他線程的信號。
通過共享對象通信
// 必須是同一個MySignal實例 public class MySignal{ protected boolean hasDataToProcess = false; public synchronized boolean hasDataToProcess(){ return this.hasDataToProcess; } public synchronized void setHasDataToProcess(boolean hasData){ this.hasDataToProcess = hasData; } }
wait() - notify()/notifyAll()
// A線程調用doWait()等待, B線程調用doNotify()喚醒A線程 public class MyWaitNotify{ MonitorObject myMonitorObject = new MonitorObject(); public void doWait(){ synchronized(myMonitorObject){ try{ myMonitorObject.wait(); } catch(InterruptedException e){...} } } public void doNotify(){ synchronized(myMonitorObject){ myMonitorObject.notify(); } } }
問題一 信號丟失:先調用了notify()后調用wait(),線程會一直等待下去
// 增加boolean wasSignalled, 記錄是否收到喚醒信號。只有沒收到過喚醒信號時才可以wait,避免信號丟失。 public class MyWaitNotify2 { MonitorObject myMonitorObject = new MonitorObject(); boolean wasSignalled = false; public void doWait(){ synchronized(myMonitorObject){ if(!wasSignalled){ try{ myMonitorObject.wait(); } catch(InterruptedException e){...} } wasSignalled = false;// wait+notify之后將信號清除 } } public void doNotify() { synchronized (myMonitorObject) { wasSignalled = true; myMonitorObject.notify(); } } }
問題二 虛假信號:
假設線程A1因為某種條件在條件隊列中等待,同時線程A2因為另外一種條件在同一個條件隊列中等待,也就是說線程A1/A2都被同一個Object.wait()掛起,但是等待的條件不同。
此時滿足A2的條件,允許線程B執行一個Object.notify()操作去喚醒A2,但是JVM從Object.wait()的多個線程(A1/A2)中隨機挑選一個喚醒,可能喚醒了A1。
A1線程即使沒有收到正確的信號,也能夠執行后續的操作。A1收到的就是虛假信號。
而此時A2仍然在傻傻的等待被喚醒的信號。A2則信號丟失。
public class MyWaitNotify3 { MonitorObject myMonitorObject = new MonitorObject(); boolean wasSignalled = false; public void doWait(){ synchronized(myMonitorObject){ while(!wasSignalled){ try{ myMonitorObject.wait();// 如果被虛假喚醒,再回while循環檢查條件wasSignalled } catch(InterruptedException e){...} } wasSignalled = false; } } public void doNotify() { synchronized (myMonitorObject) { wasSignalled = true; myMonitorObject.notify(); } } }
這樣的一個while循環叫做自旋鎖(注:這種做法要慎重,目前的JVM實現自旋會消耗CPU,如果長時間不調用doNotify方法,doWait方法會一直自旋,CPU會消耗太大)
問題三 多個線程等待相同信號,notifyAll() (while好處)
如果有多個線程在等待,被notifyAll()喚醒,第一個獲得鎖的線程被正確執行。
用if():其它線程獲得鎖后,不管條件wasSignalled是否滿足都會直接wait往后執行;
用while():其它線程獲得鎖后先檢查條件wasSignalled,如果不滿足就繼續wait
(注:不要使用全局對象(常量等)做監視器。應使用唯一對應的對象)
總之,用while()自旋鎖,線程被喚醒之后可以保證再次檢查條件是否滿足。
七、死鎖
多個線程同時但以不同的順序請求同一組鎖的時候可能死鎖。
如果線程1鎖住了A,然后嘗試對B進行加鎖,同時線程2已經鎖住了B,接着嘗試對A進行加鎖,這時死鎖就發生了。
如果線程1稍微領先線程2,然后成功地鎖住了A和B兩個對象,那么線程2就會在嘗試對B加鎖的時候被阻塞,這樣死鎖就不會發生。因為線程調度通常是不可預測的,因此沒有一個辦法可以准確預測什么時候死鎖會發生,僅僅是可能會發生。
/** * 如果線程1調用parent.addChild(child)方法的同時有另外一個線程2調用child.setParent(parent)方法, * 兩個線程中的parent表示的是同一個對象,child亦然,此時就可能發生死鎖。 */ public class TreeNode { TreeNode parent = null; List children = new ArrayList(); public synchronized void addChild(TreeNode child) {// parent對象鎖 if (!this.children.contains(child)) { this.children.add(child); child.setParentOnly(this);// child對象鎖 } } public synchronized void addChildOnly(TreeNode child){// parent對象鎖 if(!this.children.contains(child){ this.children.add(child); } } public synchronized void setParent(TreeNode parent) {// child對象鎖 this.parent = parent; parent.addChildOnly(this);// parent對象鎖 } public synchronized void setParentOnly(TreeNode parent) {// child對象鎖 this.parent = parent; } }
更復雜的死鎖
死鎖可能不止包含2個線程。四個線程死鎖的例子:
Thread 1 locks A, waits for B
Thread 2 locks B, waits for C
Thread 3 locks C, waits for D
Thread 4 locks D, waits for A
線程1等待線程2,線程2等待線程3,線程3等待線程4,線程4等待線程1。
避免死鎖
1、加鎖順序
多個線程請求的一組鎖按順序加鎖可以避免死鎖。
比如解決:如果線程1鎖住了A,然后嘗試對B進行加鎖,同時線程2已經鎖住了B,接着嘗試對A進行加鎖,這時死鎖就發生了。
線程1和線程2都先鎖A再鎖B,不會發生死鎖。
問題:這種方式需要你事先知道所有可能會用到的鎖,並對這些鎖做適當的排序。
2、加鎖時限(超時重試機制)
設置一個超時時間,在嘗試獲取鎖的過程中若超過了這個時限該線程則放棄對該鎖請求,回退並釋放所有已經獲得的鎖,然后等待一段隨機的時間再重試。
這段隨機的等待時間讓其它線程有機會嘗試獲取相同的這些鎖,並且讓該應用在沒有獲得鎖的時候可以繼續運行干點其它事情。
問題:1)當線程很多時,等待的這一段隨機的時間會一樣長或者很接近, 因此就算出現競爭而導致超時后,由於超時時間一樣,它們又會同時開始重試,導致新一輪的競爭,帶來了新的問題。
2)不能對synchronized同步塊設置超時時間。需要創建一個自定義鎖,或使用java.util.concurrent包下的工具。
3、死鎖檢測
主要是針對那些不可能實現按序加鎖並且鎖超時也不可行的情況。
每當一個線程獲得了鎖獲請求鎖,會在線程和鎖相關的數據結構中(比如map)將其記下。當一個線程請求鎖失敗時,這個線程可以遍歷鎖的關系圖看看是否有死鎖發生。
例如:線程A請求鎖7,但是鎖7這個時候被線程B持有,這時線程A就可以檢查一下線程B是否已經請求了線程A當前所持有的鎖。如果線程B確實有這樣的請求,那么就是發生了死鎖(線程A擁有鎖1,請求鎖7;線程B擁有鎖7,請求鎖1)。
當檢測出死鎖時,可以有兩種做法:
1)釋放所有鎖,回退,並且等待一段隨機的時間后重試。(類似超時重試機制)
2)給這些線程設置優先級,讓一個(或幾個)線程回退,剩下的線程就像沒發生死鎖一樣繼續保持着它們需要的鎖。
八、飢餓和公平
如果一個線程因為CPU時間全部被其他線程搶走而得不到CPU運行時間,這種狀態被稱之為飢餓。
解決飢餓的方案被稱之為公平性 – 即所有線程均能公平地獲得運行機會。
導致線程飢餓原因:
1)高優先級線程吞噬所有的低優先級線程的CPU時間。
2)線程始終競爭不到鎖。
3)線程調用object.wait()后沒有被喚醒。
Java中實現公平性方案
使用鎖方式替代同步塊(不能實現公平性)
/** * 第一個線程調用lock(),isLocked=true * 之后的線程調用lock(),都被wait() * 第一個線程執行完釋放鎖,調用unlock()。isLocked = false允許其它線程拿鎖,notify()喚醒一個線程 */ public class Lock { private boolean isLocked = false; private Thread lockingThread = null; public synchronized void lock() throws InterruptedException { while (isLocked) { wait(); } isLocked = true; lockingThread = Thread.currentThread(); } public synchronized void unlock() { if (this.lockingThread != Thread.currentThread()) { throw new IllegalMonitorStateException("Calling thread has not locked this lock"); } isLocked = false; lockingThread = null; notify(); } }
公平鎖:
/** * 第一個線程調用lock(),isLocked=true * 之后的線程調用lock()拿鎖,new queueObject()存入隊列並wait() * 第一個線程執行完畢調用unlock(),isLocked=false,取出隊列頭部的queueObject對象notify(),保證了先wait的線程按順序先notify(即公平性) * * 1.synchronized的同步塊走完就會釋放鎖,沒有嵌套,不存在死鎖和嵌套鎖死 */ public class FairLock { private boolean isLocked = false; private Thread lockingThread = null; private List<QueueObject> waitingThreads = new ArrayList<QueueObject>(); public void lock() throws InterruptedException { QueueObject queueObject = new QueueObject(); boolean isLockedForThisThread = true; synchronized (this) { waitingThreads.add(queueObject); } while (isLockedForThisThread) { synchronized (this) { isLockedForThisThread = isLocked || waitingThreads.get(0) != queueObject; if (!isLockedForThisThread) { isLocked = true; waitingThreads.remove(queueObject); lockingThread = Thread.currentThread(); return; } } try { queueObject.doWait(); } catch (InterruptedException e) { synchronized (this) { waitingThreads.remove(queueObject); } throw e; } } } public synchronized void unlock() { if (this.lockingThread != Thread.currentThread()) { throw new IllegalMonitorStateException("Calling thread has not locked this lock"); } isLocked = false; lockingThread = null; if (waitingThreads.size() > 0) { waitingThreads.get(0).doNotify(); } } } public class QueueObject { private boolean isNotified = false; public synchronized void doWait() throws InterruptedException { while (!isNotified) { this.wait(); } this.isNotified = false; } public synchronized void doNotify() { this.isNotified = true; this.notify(); } public boolean equals(Object o) { return this == o; } }
九、嵌套管程鎖死
線程1獲得A對象的鎖。
線程1獲得對象B的鎖(A對象鎖還未釋放)。
線程1調用B.wait(),從而釋放了B對象上的鎖,但仍然持有對象A的鎖。
線程2需要同時持有對象A和對象B的鎖,才能向線程1發信號B.notify()。
線程2無法獲得對象A上的鎖,因為對象A上的鎖當前正被線程1持有。
線程2一直被阻塞,等待線程1釋放對象A上的鎖。
線程1一直阻塞,等待線程2的信號,因此不會釋放對象A上的鎖。
舉例:
public class Lock { protected MonitorObject monitorObject = new MonitorObject(); protected boolean isLocked = false; public void lock() throws InterruptedException { synchronized (this) { while (isLocked) { synchronized (this.monitorObject) { this.monitorObject.wait(); } } isLocked = true; } } public void unlock() { synchronized (this) { this.isLocked = false; synchronized (this.monitorObject) { this.monitorObject.notify(); } } } }
十、滑動條件(Slipped Conditions)
一個線程檢查某一條件到該線程操作此條件期間,這個條件已經被其它線程改變,導致第一個線程在該條件上執行了錯誤的操作。
/** * 兩個線程同時調用lock() * 第一個線程執行到兩個同步塊之間時,此時isLocked=false * 第二個線程開始執行lock(),會跳過while循環,走出第一個同步塊。(正確執行:第一個線程lock了,第二個線程要wait) */ public class Lock { private boolean isLocked = false; public void lock() { synchronized (this) { while (isLocked) { try { this.wait(); } catch (InterruptedException e) { // do nothing, keep waiting } } } synchronized (this) { isLocked = true; } } public synchronized void unlock() { isLocked = false; this.notify(); } }
為避免slipped conditions,條件的檢查與設置必須是原子的,也就是說,在條件檢查和設置期間,不會有其它線程操作這個條件。
修改:
public class Lock { private boolean isLocked = false; public void lock() { synchronized (this) { while (isLocked) { try { this.wait(); } catch (InterruptedException e) { // do nothing, keep waiting } } isLocked = true;// isLocked的檢查和修改要具有原子性 } } public synchronized void unlock() { isLocked = false; this.notify(); } }
十一、重入鎖死
如果一個線程持有某個對象上的鎖,那么它就有權訪問所有在該對象上同步的塊。這就叫可重入。若線程已經持有鎖,那么它就可以重復訪問所有使用該鎖的代碼塊。
重入鎖死舉例:
/** * 如果一個線程兩次調用lock()間沒有調用unlock()方法,那么第二次調用lock()就會被阻塞,這就出現了重入鎖死。 */ public class Lock { private boolean isLocked = false; public synchronized void lock() throws InterruptedException { while (isLocked) { wait(); } isLocked = true; } public synchronized void unlock() { isLocked = false; notify(); } }
十二、並發編程模型
並行工作者模型
在並行工作者模型中,委派者(Delegator)將傳入的作業分配給不同的工作者。每個工作者完成整個任務。工作者們並行運作在不同的線程上,甚至可能在不同的CPU上。
如果在某個汽車廠里實現了並行工作者模型,每台車都會由一個工人來生產。工人們將拿到汽車的生產規格,並且從頭到尾負責所有工作。
優點:容易實現,容易理解
缺點:
(1)共享狀態可能會很復雜。
線程需要以某種方式存取共享數據,以確保某個線程的修改能夠對其他線程可見(數據修改需要同步到主存中,不僅僅將數據保存在執行這個線程的CPU的緩存中)。
在等待訪問共享數據結構時,線程之間的互相等待將會丟失部分並行性。
非阻塞並發算法和可持久化的數據結構 可以降低競爭並提升性能,但是實現比較困難。
(2)共享狀態能夠被系統中得其他線程修改。所以線程在每次需要的時候必須重讀狀態,會導致速度變慢,特別是狀態保存在外部數據庫中的時候。
(3)任務順序是不確定的
流水線模式
每個工作者只負責作業中的部分工作。當完成了自己的這部分工作時工作者會將作業轉發給下一個工作者。每個工作者在自己的線程中運行,並且不會和其他工作者共享狀態。
Actors 和 channels 是兩種比較類似的流水線(或反應器/事件驅動)模型。
優點:
(1)工作者之間無需共享狀態,意味着實現的時候無需考慮所有因並發訪問共享對象而產生的並發性問題。
這使得在實現工作者的時候變得非常容易。在實現工作者的時候就好像是單個線程在處理工作-基本上是一個單線程的實現。
(2)當工作者知道了沒有其他線程可以修改它們的數據,工作者可以變成有狀態的。
它們可以在內存中保存它們需要操作的數據,只需在最后將更改寫回到外部存儲系統。因此,有狀態的工作者通常比無狀態的工作者具有更高的性能。
(3)較好的硬件整合,更好的利用緩存。
(4)實現一個有保障的作業順序。
缺點:作業的執行往往分布到多個工作者上,並因此分布到項目中的多個類上。編碼難度大。
函數式並行
函數都是通過拷貝來傳遞參數的,所以除了接收函數外沒有實體可以操作數據。這對於避免共享數據的競態來說是很有必要的。同樣也使得函數的執行類似於原子操作。每個函數調用的執行獨立於任何其他函數的調用。
一旦每個函數調用都可以獨立的執行,它們就可以分散在不同的CPU上執行了。這也就意味着能夠在多處理器上並行的執行使用函數式實現的算法。