1. 線程與進程
1.1 進程與線程的區別
線程比進程更輕量級。
有進程的時候,一個進程(的資源)可以被多個線程共享。
沒有進程的時候,線程也可以被CPU獨立調度。
1.2 多進程場景下的解決方案(信號量方案)
在多道程序系統中存在許多進程,它們共享各種資源,然而有很多資源一次只能供一個進程使用,這便是臨界資源。
多進程中的臨界資源大致上可以分為兩類,
一類是物理上的真實資源,如打印機;
一類是硬盤或內存中的共享數據,如共享內存等。而進程內互斥訪問臨界資源的代碼被稱為臨界區。
針對臨界資源的互斥訪問,JVM層面的鎖就已經失去效力了。
在多進程的情況下,主要還是利用操作系統層面的進程間通信原理來解決臨界資源的搶占問題。比較常見的一種方法便是使用信號量(Semaphores)。
信號量在POSIX標準下有兩種,分別為有名信號量和無名信號量。
無名信號量通常保存在共享內存中,而有名信號量是與一個特定的文件名稱相關聯。
信號量是一個整數變量(類比為停車場的空余車位),有計數信號量和二值信號量兩種。對信號量的操作,主要是P操作(進入停車場)和V操作(出停車場)。
P操作:先檢查信號量的大小,若值大于零,則將信號量減1,同時進程獲得共享資源的訪問權限,繼續執行;若小于或者等于零,則該進程被阻塞后,進入等待隊列。
V操作:該操作將信號量的值加1,如果有進程阻塞著等待該信號量,那么其中一個進程將被喚醒。
舉個例子,設信號量為1,當一個進程A在進入臨界區之前,先進行P操作。
發現值大于零,那么就將信號量減為0,進入臨界區執行。
此時,若另一個進程B也要進去臨界區,進行P操作,發現信號量等于0,則會被阻塞。
當進程A退出臨界區時,會進行V操作,將信號量的值加1,并喚醒阻塞的進程B。此時B就可以進入臨界區了。
這種方式,其實和多線程環境下的加解鎖非常類似。因此用信號量處理臨界資源搶占,也可以簡單地理解為對臨界區進行加鎖。
1.3 程序使用線程操作CPU的三種實現 以及 JVM如何實現:
1. 程序(exe)直接調用Windows內核線程的本地方法做事,直接作用于CPU。
(雖然原理如此但是程序無法直接操作Windows內核線程,程序實際調用->Windows內核線程暴露的接口:輕量級進程,輕量級進程與Windows內核線程數量1:1)
? 缺點:運行程序時CPU處于用戶態,而調用輕量級進程時CPU必須處于核心態,所以程序運行期間將會不斷切換用戶態<—>核心態。
2. 程序(exe)只使用用戶線程,而不使用Windows內核線程做事。
? 缺點:用戶線程是程序內部創建的線程,對Windows內核不可見,所以無法使用Windows內核本地方法操作CPU。那程序怎么使用CPU呢:程序中重寫操作系統內核的本地方法,這會非常麻煩。
3. 程序——>用戶線程——>調用輕量級進程——>調用Windows內核線程——>CPU,
? 優點:用戶線程執行用戶態的事情,調用輕量級進程執行內核態的事情,避免頻繁切換。
4. JVM的實現方式:
java.exe和javac.exe等——>Thread.start啟動用戶線程——>Thread類中的native方法(不同OS不同JDK不同native方法)——>調用輕量級進程——>調用OS內核線程——>CPU。
1.4 線程的調度
1. 協同式調度:很古老,是等待一個線程干完活才把CPU給下一個,容易死機。
2. 搶占式調度:被普遍使用。系統通過綜合各種原因完成調度,我們也可以給出建議,即線程優先級,但是操作系統(Windows)在執行線程時是可以改變優先級的,所以優先級僅僅是個建議。
2. 線程的六種狀態之間的關系
2.0. 寫在前面:
Java的Thread類中記錄著線程的六種狀態:(Runnable狀態包括了Runnable和Running)
左:new
中:Runnnable
右:terminated
上:blocked
左下:waiting
右下:timed_waiting
Sleep和Yield不會釋放鎖。
Wait會釋放鎖。
2.1 左路:New—>Runnable(Runnable包括了Runnable和Running)
start方法:Thread.start()
2.2 中路:Runnable<—>Running
Runnable—>Running:CPU分配時間片。
Running—>Runnable:時間片用完 (或者叫線程上下文切換) 或者 本線程內調用Thread.yield (相當于時間片用完),
則會重新競爭CPU。(不會釋放鎖,ps.鎖指的是monitorexit)
2.3 上路:Running—>Blocked—>Runnable
Running—>Blocked:
即線程執行synchronized塊&方法時,執行monitorenter指令時,
發現棧頂元素的monitor對象已經被別的線程占用(monitor的_count字段不為0),則本線程進入Blocked狀態。
等待monitor的_count字段為0時獲取鎖成功,繼續執行synchronized塊&方法。
Blocked—>Runnable:等待monitor的_count字段為0時且獲取鎖成功時,從Blocked進入Runnable,
等待CPU分配時間片后,進入Running狀態,繼續執行synchronized塊&方法。
2.4 左下路開始等待:Running—>Waiting
途徑1. Object.wait():本線程中執行Object.wait()
(wait 使用前提是必須在synchronized中使用,且Object對象就是被鎖對象,因為wait的實現必須要先獲取被鎖對象的monitor)
(會釋放鎖,ps.鎖指的是monitorexit)
途徑2. t2.join():本線程t1中調用了另一個線程t2.join()方法,本線程t1被等待
(可能會釋放鎖,ps.鎖指的是monitorexit:釋放this的鎖,前提是this已被鎖)
途徑3. LockSupport.park():本線程中執行LockSupport.park()
(不會釋放鎖,ps.鎖指的是monitorexit。注意不要把park和monitorenter混淆)
2.5 左下路解放等待:Waiting—>Runnable
途徑1. Object.notify / notifyAll():
在另一個線程執行Object.notify / notifyAll(),使Object.wait()那個線程重新monitorenter開始競爭鎖,當競爭成功后,恢復為Runnable。
然后等CPU分配時間片,從wait之后開始執行。
(notify 使用前提是必須在synchronized中使用,且Object對象就是被鎖對象,因為 notify 的實現必須要先獲取被鎖對象的monitor)
途徑2. t2.join():
等t2執行完,t1恢復到Runnable。
途徑3. LockSupport.unpark(線程):
在另一個線程中執行LockSupport.unpark(被park的那個線程)
2.6 右下路開始等待:Running—>Timed_Waiting
途徑1. Thread.sleep(時間):
在本線程內執行Thread.sleep(時間)。(不會釋放鎖,ps.鎖指的是monitorexit)
途徑2. t2.join(時間):另一個線程t2在本線程t1中,并執行t2.join(時間),所以本線程t1被等待(時間)
(可能會釋放鎖,ps.鎖指的是monitorexit:會釋放this的鎖,前提是this已被鎖)
途徑3. Object.wait(時間):本線程中執行Object.wait(時間)
(wait 使用前提是必須在synchronized中使用,且Object對象就是被鎖對象,因為wait的實現必須要先獲取被鎖對象的monitor)
(會釋放鎖,ps.鎖指的是monitorexit)
途徑4. LockSupport.parkNanos(時間):本線程中執行LockSupport.parkNanos(時間)
(不會釋放鎖,ps.鎖指的是monitorexit。注意不要把park和monitorenter混淆)
途徑5. LockSupport.parkUtil(時間):本線程中執行LockSupport.parkUtil(時間)
(不會釋放鎖,ps.鎖指的是monitorexit。注意不要把park和monitorenter混淆)
2.7 右下路解放等待:Timed_Waiting—>Runnable
途徑1. Thread.sleep(時間)+時間到期。
途徑2. t2.join(時間)+時間到期。
途徑3. Object.wait(時間)+時間到期,使Object.wait()那個線程重新monitorenter開始競爭鎖,當競爭成功后,恢復為Runnable。
然后等CPU分配時間片,從wait之后開始執行。
途徑4. LockSupport.parkNanos(時間)+時間到期。
途徑5. LockSupport.parkUtil(時間)+時間到期。
2.8 右路:Running—>Terminated
run方法執行完。
3. 線程安全的五個維度
3.1 不可變:
final變量(基本類型Final是常量,值和引用都不可變。引用類型Final的值可變+引用不可變)
3.2 絕對安全:
不存在。
因為即使Vector單獨使用(remove方法、get方法、size方法等方法都是synchronized)時可以保證使用方法期間是不可以另一個線程操作該vector對象的,
但是組合使用無法保證原子性(場景:Vector.size()遍歷集合+然后刪除),意思是size方法之后+remove方法之前可能會被別的線程插進來。
Thread removedThread = new Thread(new Runnable() { @Override public void run() { for(int i=0;i<vector.size();i++){ vector.remove(i); } } });
3.3 相對安全:
普遍存在。Vector+Synchronized修飾。
Thread removedThread=new Thread(new Runnable() { @Override public void run() { /** * 使用synchronized鎖機制將操作對象進行鎖定, * 當執行size方法之后+remove方法之前,其他線程不可以訪問這行vector */ synchronized (vector) { for (int i = 0; i < vector.size(); i++) { vector.remove(i); } } } });
3.4 線程兼容:
普遍存在。ArrayList+Synchronized修飾。
3.5 線程對立:
兩個線程的操作互斥,放在多線程并發環境會出現死鎖。列舉死鎖的例子:比如Thread.suspend()和Thread.resume()。
4. FAQ
4.1 parkNanos
parkNanos的邏輯是,等待nano時間就會被釋放,釋放的概念也就是unpark(線程),也就是繼續執行線程。
parkNanos在ReentrantLock(lock).Trylock(long timeout, TimeUnit unit)里面使用:
timeout是指定的最大等待響應時間,包括了爭取時間和park時間,deadline時間如果還沒獲取到資源就返回false退出隊列啥也不干了。
parkNanos的用處:
最多parkNanos剩的時間那么久,時間到了就unpark這個線程,即這個節點繼續死循環,再做最后一次爭取鎖,如果再失敗就會發現剩的時間小于0,然后就執行退出隊列操作。
4.2 Interrupt()
Thread有三個public的Interrupt方法:
public void interrupt() : 非靜態方法,被某個Thread實例所調用,作用是把該線程interrupt,但是只是在當前線程中打了一個中止標志,并不是真的停止線程。
public boolean isInterrupted() : 非靜態方法,被某個Thread實例所調用,作用是返回該線程是否有中止標志。
public static boolean interrupted(): 靜態方法,被Thread類或者某個Thread類或者某個Thread實例調用(誰調用都沒關系),
作用是返回當前線程(即正在執行這個方法的線程)是否有中止標志,并清除中止標志。
Interrupt的常見用法:
如果線程正在執行Object.wait、t2.join、Thread.sleep、Lock.lockInterruptibly時會檢查該線程是否已經中止標志,
比如,線程先執行interrupt,再Thread.sleep,這時Thread.sleep會產生InterruptedException異常,會被catch到。拋出InterruptedException異常后,線程即恢復到未interrupt狀態。
4.3 面試題:如何停止一個正在運行的線程?
1. 利用try&catch&sleep方法+Interrupt中止標志拋異常
方法1:本線程在Sleep期間,別人的線程把我t.interrupt()了,則我會拋出異常,所以Sleep所在的try塊被中止執行了,如果所有邏輯寫在tryt塊中,可實現終止線程的作用。
方法2:本線程在Sleep之前,本線程就已經t.interrupt()了,則我只要Sleep就會拋出異常,所以Sleep所在的try塊被中止執行了,如果所有邏輯寫在tryt塊中,可實現終止線程的作用。
ps. 雖然Sleep所在的try塊被中止執行了,但是try catch整體之后的邏輯還是照常執行。如果try catch整體之后還有邏輯,則不會實現終止線程的作用。
ps. 意外發現小知識,主線程中執行t.interrupt()之后,t線程的確生成了中止標志,但是如果這是主線程sleep一會兒,t線程的中止標志就會被清除,而且再次t.interrupt()也不會再生成中止標志。
2. 利用Interrupt中止標志+檢查標志位+return
方法1:本線程的邏輯途中寫幾處檢查點Thread.interrupted(),如果外面把我t.interrupt()了則此處檢查點將返回true。如果為true的話則執行return,或者,如果為false的話則執行return。
3. 暴力中止
方法1:本線程內執行currentThread().stop();
方法2:別的線程內執行t.stop();
ps.該方法本廢棄不建議使用。



浙公網安備 33010602011771號