多線程編程是為了讓程序運(yùn)行得更快,但是不是說(shuō),線程創(chuàng)建地越多越好,線程切換的時(shí)候上下文切換,以及受限于硬件和軟件資源的限制問(wèn)題
上下文切換
單核CPU同樣支持多線程編程,CPU通過(guò)給每個(gè)線程分配CPU時(shí)間片來(lái)實(shí)現(xiàn)這個(gè)機(jī)制,時(shí)間片是CPU分配給各個(gè)線程的時(shí)間,這個(gè)時(shí)間片非常短,所以就不得不通過(guò)切換線程來(lái)執(zhí)行(時(shí)間片一般是幾十毫秒)
當(dāng)前任務(wù)執(zhí)行一個(gè)時(shí)間片后,會(huì)切換到下一個(gè)任務(wù),但是,在切換前會(huì)保存上一個(gè)任務(wù)的狀態(tài),這樣的話下次這條線程獲取到時(shí)間片之后就可以恢復(fù)這個(gè)任務(wù)的狀態(tài)
協(xié)程
協(xié)程說(shuō)通俗一點(diǎn)就是由線程調(diào)度的線程,操作系統(tǒng)創(chuàng)建一個(gè)進(jìn)程,進(jìn)程再創(chuàng)建若干個(gè)線程并行,線程的切換由操作系統(tǒng)負(fù)責(zé)調(diào)度,Java語(yǔ)言等線程其實(shí)與操作系統(tǒng)線程是1:1的關(guān)系,每個(gè)線程都有自己的Stack,Java在64位操作系統(tǒng)默默人stack大小為1024kb,所以一個(gè)進(jìn)程也是不能夠開(kāi)啟上萬(wàn)個(gè)線程的
基于J2EE項(xiàng)目都是基于每個(gè)請(qǐng)求占用一個(gè)線程去完成完整的業(yè)務(wù)邏輯,(包括事務(wù))。所以系統(tǒng)的吞吐能力取決于每個(gè)線程的操作耗時(shí)。如果遇到很耗時(shí)的I/O行為,則整個(gè)系統(tǒng)的吞吐立刻下降,比如JDBC是同步阻塞的,這也是為什么很多人都說(shuō)數(shù)據(jù)庫(kù)是瓶頸的原因。這里的耗時(shí)其實(shí)是讓CPU一直在等待I/O返回,說(shuō)白了線程根本沒(méi)有利用CPU去做運(yùn)算,而是處于空轉(zhuǎn)狀態(tài)。
Java的JDK里有封裝很好的ThreadPool,可以用來(lái)管理大量的線程生命周期,但是本質(zhì)上還是不能很好的解決線程數(shù)量的問(wèn)題,以及線程空轉(zhuǎn)占用CPU資源的問(wèn)題。
先階段行業(yè)里的比較流行的解決方案之一就是單線程加上異步回調(diào)。其代表派是node.js以及Java里的新秀Vert.x。他們的核心思想是一樣的,遇到需要進(jìn)行I/O操作的地方,就直接讓出CPU資源,然后注冊(cè)一個(gè)回調(diào)函數(shù),其他邏輯則繼續(xù)往下走,I/O結(jié)束后帶著結(jié)果向事件隊(duì)列里插入執(zhí)行結(jié)果,然后由事件調(diào)度器調(diào)度回調(diào)函數(shù),傳入結(jié)果。
協(xié)程的本質(zhì)上其實(shí)還是和上面的方法一樣,只不過(guò)他的核心點(diǎn)在于由他進(jìn)行調(diào)度,遇到阻塞操作,立刻yield掉,并且記錄當(dāng)前棧上的數(shù)據(jù),阻塞完后立刻再找一個(gè)線程恢復(fù)棧并把阻塞的結(jié)果放到這個(gè)線程上去跑,這樣看上去好像跟寫同步代碼沒(méi)有任何差別,這整個(gè)流程可以稱為coroutine,而跑在由coroutine負(fù)責(zé)調(diào)度的線程稱為Fiber。比如Golang里的 go關(guān)鍵字其實(shí)就是負(fù)責(zé)開(kāi)啟一個(gè)Fiber,讓func邏輯跑在上面。而這一切都是發(fā)生的用戶態(tài)上,沒(méi)有發(fā)生在內(nèi)核態(tài)上,也就是說(shuō)沒(méi)有切換上下文的開(kāi)銷。
Java線程調(diào)度
JVM必須維護(hù)一個(gè)有優(yōu)先權(quán),基于優(yōu)先級(jí)的調(diào)度模式,優(yōu)先級(jí)的值很重要,因?yàn)镴ava虛擬機(jī)和下層的操作系統(tǒng)之間的約定是操作系統(tǒng)必須選擇有最高優(yōu)先權(quán)的Java線程運(yùn)行,所以我們說(shuō)Java實(shí)現(xiàn)了一個(gè)基于優(yōu)先權(quán)的調(diào)度程序該調(diào)度程序使用一種有優(yōu)先權(quán)的方式實(shí)現(xiàn),這意味著當(dāng)一個(gè)有更高優(yōu)先權(quán)的線程到來(lái)時(shí),無(wú)論低優(yōu)先級(jí)的線程是否在運(yùn)行,都會(huì)中斷(搶占)它(JVM會(huì)這么做)。這個(gè)約定對(duì)于操作系統(tǒng)來(lái)說(shuō)并不總是這樣,這意味著操作系統(tǒng)有時(shí)可能會(huì)選擇運(yùn)行一個(gè)更低優(yōu)先級(jí)的線程。
yield()方法
理論上,yield意味著放手,放棄,投降。一個(gè)調(diào)用yield()方法的線程告訴虛擬機(jī)它樂(lè)意讓其他線程占用自己的位置。這表明該線程沒(méi)有在做一些緊急的事情。注意,這僅是一個(gè)暗示,并不能保證不會(huì)產(chǎn)生任何影響。
/**
* A hint to the scheduler that the current thread is willing to yield its current use of a processor. The scheduler is free to ignore
* this hint. Yield is a heuristic attempt to improve relative progression between threads that would otherwise over-utilize a CPU.
* Its use should be combined with detailed profiling and benchmarking to ensure that it actually has the desired effect.
*/
public static native void yield();
- Yield是一個(gè)靜態(tài)的本地(native)方法
- Yield告訴當(dāng)前正在執(zhí)行的線程把運(yùn)行機(jī)會(huì)交給線程池中擁有相同優(yōu)先級(jí)的線程。
- Yield不能保證使得當(dāng)前正在運(yùn)行的線程迅速轉(zhuǎn)換到可運(yùn)行的狀態(tài)
- 它僅能使一個(gè)線程從運(yùn)行狀態(tài)轉(zhuǎn)到可運(yùn)行狀態(tài),而不是等待或阻塞狀態(tài)
join()方法
如果一個(gè)線程A執(zhí)行了thread.join()方法目的是,當(dāng)前線程A等待thread線程終止之后才從thread.join()返回,
/**
* Waits for this thread to die.
*
* <p> An invocation of this method behaves in exactly the same
* way as the invocation
*
* <blockquote>
* {@linkplain #join(long) join}{@code (0)}
* </blockquote>
*
* @throws InterruptedException
* if any thread has interrupted the current thread. The
* <i>interrupted status</i> of the current thread is
* cleared when this exception is thrown.
*/
public final void join() throws InterruptedException {
join(0);
}
/**
* Waits at most {@code millis} milliseconds for this thread to
* die. A timeout of {@code 0} means to wait forever.
*
* <p> This implementation uses a loop of {@code this.wait} calls
* conditioned on {@code this.isAlive}. As a thread terminates the
* {@code this.notifyAll} method is invoked. It is recommended that
* applications not use {@code wait}, {@code notify}, or
* {@code notifyAll} on {@code Thread} instances.
*
* @param millis
* the time to wait in milliseconds
*
* @throws IllegalArgumentException
* if the value of {@code millis} is negative
*
* @throws InterruptedException
* if any thread has interrupted the current thread. The
* <i>interrupted status</i> of the current thread is
* cleared when this exception is thrown.
*/
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;
}
}
}
如何減少上下文切換
- 無(wú)鎖并發(fā)編程,多線程競(jìng)爭(zhēng)鎖的時(shí)候,會(huì)引起上下文切換,如將數(shù)據(jù)的ID按照Hash算法取模分段,不同線程處理不同段的數(shù)據(jù)
- CAS算法,Java的Atomic包下使用的同步類都是使用CAS和Volatile實(shí)現(xiàn)的無(wú)鎖
- 線程數(shù)量的控制
- 協(xié)程:?jiǎn)尉€程內(nèi)實(shí)現(xiàn)多任務(wù)的調(diào)度,在單線程中維持多個(gè)任務(wù)間的切換
如何避免死鎖
- 避免一個(gè)線程同時(shí)獲得多個(gè)鎖
- 避免一個(gè)線程在鎖內(nèi)同時(shí)占用多個(gè)資源,盡量保證每個(gè)鎖只占用一個(gè)資源
- 嘗試使用定時(shí)鎖,tryLock(time)代替內(nèi)部鎖的機(jī)制
- 對(duì)于數(shù)據(jù)庫(kù)來(lái)說(shuō),加鎖和解鎖必須在同一條連接中,否則將會(huì)出現(xiàn)問(wèn)題