前言- CPU競爭策略
操作系統中,CPU競爭有很多種策略。Unix系統使用的是時間片算法,而Windows則屬于搶占式的。
在時間片算法中,所有的進程排成一個隊列。操作系統按照他們的順序,給每個進程分配一段時間,即該進程允許運行的時間。如果在 時間片結束時進程還在運行,則CPU將被剝奪并分配給另一個進程。如果進程在時間片結束前阻塞或結束,則CPU當即進行切換。調度程 序所要做的就是維護一張就緒進程列表,當進程用完它的時間片后,它被移到隊列的末尾。
搶占式操作系統,就是說如果一個進程得到了 CPU 時間,除非它自己放棄使用 CPU ,否則將完全霸占 CPU 。因此可以看出,在搶 占式操作系統中,操作系統假設所有的進程都是“人品很好”的,會主動退出 CPU 。
在搶占式操作系統中,假設有若干進程,操作系統會根據他們的優先級、饑餓時間(已經多長時間沒有使用過 CPU 了),給他們算出一 個總的優先級來。操作系統就會把 CPU 交給總優先級最高的這個進程。當進程執行完畢或者自己主動掛起后,操作系統就會重新計算一 次所有進程的總優先級,然后再挑一個優先級最高的把 CPU 控制權交給他。
1-線程的優先級
在java線程中,可以在構造線程時通過setPriority()方法設定線程的優先級,優先級為從1-10的整數(默認為5),優先級越高系統分配的時間就越多;這里有一個設置優先級的一個常用經驗知識:對于頻繁阻塞的線程(經常休眠,IO操作等)需要設置較高的優先級,因為這些經常阻塞的線程即使設置為較高的優先級,但是在大部分時間里,處于阻塞狀態,會讓出CPU;而對于偏重計算的(將會長時間獨占CPU)線程設置為較低的優先級,防止其他線程的不會長時間得不到執行。
Thread thread = new Thread(job);
thread.setPriority(10);
thread.start();
注意:但是在很多系統下面對線程優先級的設置可能無效(如類unix的分時系統)
2-線程的狀態
給定一個時刻,線程只能處于6種狀態其中的一種狀態
- NEW:初始狀態,線程被構建,但是還沒有調用start()方法。
- RUNNABLE:運行狀態,java線程將操作系統中的就緒狀態和運行兩種狀態籠統的稱作運行中。
- BLOCKED:阻塞狀態,特指線程阻塞于鎖(synchronized關鍵字修飾的方法或者方法塊),并將該線程加入同步隊列中。
- WAITING:等待狀態,表示線程進入等待狀態,進入該狀態表示當前線程需要等待其他線程做出一些特定的動作(比如通知或者中斷),并將該線程加入等待隊列中。需要注意的是調用LockSupport.park()方法和Thread.join()會使得線程進入這個狀態,而不是阻塞狀態。
- TIME_WAITING:超時等待狀態,該狀態是WAITING狀態的超時版本,它可以在指定的時間自行返回。一般由帶有超時設置的方法調用引起。
- TERMINATED:終止狀態,表示當前線程已經執行完畢。
注意上圖 Object.join()有誤,應改成Thread.join()
線程start()和run()方法的區別
- thread.start()方法
調用此方法將會由操作系統任務調度器在新創建的線程中執行run()方法,可能不會立刻執行,由任務調度器調度,但是一定是在新創建的線程中執行。重復調用start方法將拋出異常IllegalThreadStateException
- thread.run()方法
Thread實現了Runnable接口,默認實現是調用target的run方法,調用此方法并不會再新創建的線程去執行run方法,只會在調用Thread.run()方法的線程本地執行,和調用一個普通對象的一個方法效果一樣,可以被重復調用。
線程方法
- Thread.sleep(long n)靜態方法
- 當n = 0 時,thread 線程主動放棄自己CPU控制權,進入就緒狀態。這種情況下只能調度優先級相等或者更高的線程,低優先級的線程很有能永遠得不到執行,當沒有符合條件的線程時,當前會一直占用CPU,造成CPU滿載。
- 當n > 0 時,Thread線程將會被強制放棄CPU控制權,并睡眠n毫秒,進入阻塞狀態。這種情況下所有其他任意優先級就緒的線程都有機會競爭CPU控制權。無論有沒有符合的線程,都會放棄CPU控制權-,因此CPU占用率較低。
- 上述1、2是從線程調度的角度分析的,無論1、2,都不會釋放對象的鎖,也就是說如果有synchronized方法塊,其他線程仍然不能訪問共享數據,該方法拋出中斷異常。
- thread.join()
使得調用thread.join()語句的線程等待thread線程的執行完畢,才從這個語句返回,并繼續這個線程,該方法也需要捕獲中斷異常。這個方法含有超時重載版本
- Thread.yield()靜態方法
將thread線程放入就緒隊列中,而不是同步隊列,由操作系統去調度。如果沒有找到其他就緒的線程,則當前線程繼續運行,比thread.sleep(0)速度快,只能讓相同優先級的線程得以運行。
重點分析join方法的實現
如何實現join方法的語義?
- ** 方法內部調用Object.wait()方法進行等待。**
- 當線程終止時,會調用線程自身的notifyAll()方法,通知所有等待在該線程對象監視器上的線程。
屬于經典的等待/通知模式
例子
解析:假設有兩個線程A、B,在B中調用方法A.join,由于join是同步方法,線程B排他獲取方法所屬的對象監視器鎖,即線程對象A的監視器鎖;線程B獲取線程A的對象監視器鎖成功后,在join方法內部,調用的是this.wait()方法,即在線程B在線程A對象上等待并釋放線程A上的對象監視器鎖。
方法內部有兩個循環判斷:
- join(0):Object.wait(0),在第一個while循環里始終對線程A是否終止進行判斷,如果還在運行,則使線程B等待,直到被通知或者中斷,當被喚醒時還得去判斷線程A是否終止,如果終止則在獲取監視器鎖后從join方法返回繼續代碼,否則繼續等待。
- join(millis > 0) : Object.wait(millis)分析方法和上面基本一樣,只不過加了超時返回,即從wait方法返回時判斷是否超時,如果超時則在獲取對象鎖后跳出循環,從join方法返回繼續執行。
對象方法
object.wait(),object.notify(),object.notifyAll()
- 這3個方法在使用之前都要獲取object對象的鎖,即在
synchronized(object){ object.wait();}
- 調用wait()方法后,線程狀態將由running變為waiting,并將當前線程放置到等待隊列中,并釋放object上的鎖。
- notify() 和notifyAll()方法調用后,等待線程依舊不會從wait方法返回,而是將等待隊列的一個或全部的線程移動到同步隊列中,被移動的線程狀態變為blocked,然后通知線程從同步方法塊返回,并釋放object上鎖,只有下一次鎖的競爭中,等待線程成功獲取到object上的鎖,才從wait方法返回。
3-線程的創建
提供了三個方法來創建Thread
- 繼承Thread類來創建線程類,重寫run()方法作為線程執行體。
缺點:
線程類繼承了Thread類,無法在繼承其他父類。
因為每條線程都是一個Thread子類的實例,因此多個線程之間共享數據比較麻煩。
- 用實現了Runnable接口的對象作為target來創建線程對象。
推薦,用來將沒有返回值和不會拋出異常的方法體run()傳遞給線程去執行
- 用實現了Callable接口的對象作為task提交給線程池ExecutorService 通過submit方法來提交執行
推薦,用來將有返回值和會拋出異常的方法體run()傳遞給線程去執行
4-線程中斷
中斷是一種線程之間通信機制,但是中斷不像其名字一樣會讓線程中斷,而是線程通過循環判斷查看中斷標志位,來及時的查看中斷狀態并采取下一步的操作。
- 其他線程通過該線程的interrupt()方法對其進行中斷操作。
- 線程通過調用自身的isInterrupted()來進行判斷是否被中斷,也可以調用靜態方法Thread.interrupted()將清除當前線程的中斷標志并返回之前線程的中斷狀態;如果該線程已經處于終結狀態,無論是否中斷,則調用該對象的isInterrupted()都將返回false。
- 拋出InterruptedException異常的方法,比如Thread.sleep(),這些方法在拋出異常之前,Java虛擬機會先將該線程的中斷標志位清除,然后再拋出InterruptedException異常,這時在調用isInterrupted()方法進行判斷將返回false。
5-等待/通知的經典線程間通信范式
- 等待方遵循如下原則
- 獲取對象的鎖。
- 如果條件不滿足,那么調用對象的wait()方法,被通知后還要檢查條件。
- 條件滿足則執行對應的邏輯 。
synchronized(object對象){
while(條件不滿足){
object.wait();
}
對應的處理邏輯;
}
- 通知方遵循如下原則
- 獲取對象的鎖。
- 改變條件
- 通知所有等待在對象上的線程
synchronized(object對象){
改變條件
object.notifyAll();
}
6-ThreadLocal
線程本地變量,是一個以TreadLocal變量為鍵,任意對象為值的存儲結構,將變量與線程綁定在一起,為每一個線程維護一個獨立的變量副本,ThreadLocal將變量的范圍限制在一個線程的上下文當中,使得變量的作用域為線程級別。
- ThreadLocal僅僅是個變量訪問的入口;
- 每一個Thread對象都有一個ThreadLocalMap對象,這個ThreadLocalMap持有所有已經初始化的ThreadLocal值對象的引用;
- 只有在線程中調用ThreadLocal的set(),或者get()方法時都會在當前線程中綁定這個變量,否則不會綁定。第一次get()方法調用將會進行初始化(如果set方法沒有調用過),而且初始化每個線程值進行一次。
- 初始化方法
允許對默認初始化方法進行重寫
// 默認初始化方法
protected T initialValue(){
return null;
}
ThreadLocal源碼分析
- set()
// ThreadLocal.java
public void set(T value) {
//1.首先獲取當前線程對象
Thread t = Thread.currentThread();
//2.獲取該線程對象的ThreadLocalMap
ThreadLocalMap map = getMap(t);
//如果map不為空,執行set操作,以當前threadLocal對象為key
//實際存儲對象為value進行set操作
if (map != null)
map.set(this, value);
//如果map為空,則為該線程創建ThreadLocalMap
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
//線程對象持有ThreadLocalMap的引用
return t.threadLocals;
}
// Thread.java
ThreadLocal.ThreadLocalMap threadLocals = null;
- get()
public T get() {
//1.首先獲取當前線程
Thread t = Thread.currentThread();
//2.獲取線程的map對象
ThreadLocalMap map = getMap(t);
//3.如果map不為空,以threadlocal實例為key獲取到對應Entry,然后從Entry中取出對象即可。
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
//如果map為空,也就是第一次沒有調用set直接get
//(或者調用過set,又調用了remove)時,為其設定初始值
return setInitialValue();
}
private T setInitialValue() {
T value = initialValue();//獲取初始值
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
場景一:為每一個線程分配一個遞增無重復的ID
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadLocalDemo {
public static void main(String []args){
for(int i=0;i<5;i++){
final Thread t = new Thread(){
@Override
public void run(){
System.out.println("當前線程:"+Thread.currentThread().getName()
+",已分配ID:"+ThreadId.get());
}
};
t.start();
}
}
static class ThreadId{
//一個遞增的序列,使用AtomicInger原子變量保證線程安全
private static final AtomicInteger nextId = new AtomicInteger(0);
//線程本地變量,為每個線程關聯一個唯一的序號
private static final ThreadLocal<Integer> threadId =
new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
//相當于nextId++,由于nextId++這種操作是個復合操作而非原子操作,
//會有線程安全問題(可能在初始化時就獲取到相同的ID,所以使用原子變量
return nextId.getAndIncrement();
}
};
//返回當前線程的唯一的序列,如果第一次get,會先調用initialValue,后面看源碼就了解了
public static int get() {
return threadId.get();
}
}
}
說明:ThreadID是線程共享的,所以需要原子類來保證線程訪問的安全性,而ThreadID的成員變量threadId是線程封閉的,只是線程本地變量初始化時需要訪問原子類(多個線程同時訪問引起 )
場景二:web開發中,為每一個連接創建一個ThreadLocal保存session信息,如果web服務器使用線程池技術(比如Tomcat)進行線程復用,則每一次連接都要重新的set,以保證session為本次連接的信息。當session結束,調用remove方法,將線程本地變量從線程的ThreadLocalMap中移除。
7-等待超時
主要學習的是剩余時間的計算
等待超時模式的經典模式
// 同步方法
public synchronized Object get(long millss) throws InterruptedException {
// 獲取將來時間點
long future = System.currentTimeMillis)() + millis;
// 初始化剩余時間為millis,從這可以看出超時等待時間并不是十分的嚴格
long remaining = millis;
// 超時等待判斷,當返回值的結果不滿足并且剩余時間小于0時,從循環退出
while((result == null) && remaining > 0){
wait(remaining);
remaining = future - System.currentTimeMillis();
}
return result;
}