大家好泉哈,我是易安!
我們知道破讨,在并發(fā)程序中丛晦,并不是啟動(dòng)更多的線程就能讓程序最大限度地并發(fā)執(zhí)行。線程數(shù)量設(shè)置太小提陶,會(huì)導(dǎo)致程序不能充分地利用系統(tǒng)資源烫沙;線程數(shù)量設(shè)置太大,又可能帶來資源的過度競(jìng)爭(zhēng)隙笆,導(dǎo)致上下文切換帶來額外的系統(tǒng)開銷锌蓄,今天我們就來談下線程的上線文切換。
什么是上下文切換
在單個(gè)處理器的時(shí)期撑柔,操作系統(tǒng)就能處理多線程并發(fā)任務(wù)瘸爽。處理器給每個(gè)線程分配 CPU 時(shí)間片(Time Slice),線程在分配獲得的時(shí)間片內(nèi)執(zhí)行任務(wù)铅忿。
CPU 時(shí)間片是 CPU 分配給每個(gè)線程執(zhí)行的時(shí)間段蝶糯,一般為幾十毫秒。在這么短的時(shí)間內(nèi)線程互相切換辆沦,我們根本感覺不到昼捍,所以看上去就好像是同時(shí)進(jìn)行的一樣。
時(shí)間片決定了一個(gè)線程可以連續(xù)占用處理器運(yùn)行的時(shí)長(zhǎng)肢扯。當(dāng)一個(gè)線程的時(shí)間片用完了妒茬,或者因自身原因被迫暫停運(yùn)行了,這個(gè)時(shí)候蔚晨,另外一個(gè)線程(可以是同一個(gè)線程或者其它進(jìn)程的線程)就會(huì)被操作系統(tǒng)選中乍钻,來占用處理器。這種一個(gè)線程被暫停剝奪使用權(quán)铭腕,另外一個(gè)線程被選中開始或者繼續(xù)運(yùn)行的過程就叫做上下文切換(Context Switch)银择。
具體來說,一個(gè)線程被剝奪處理器的使用權(quán)而被暫停運(yùn)行累舷,就是“切出”浩考;一個(gè)線程被選中占用處理器開始或者繼續(xù)運(yùn)行,就是“切入”被盈。在這種切出切入的過程中析孽,操作系統(tǒng)需要保存和恢復(fù)相應(yīng)的進(jìn)度信息搭伤,這個(gè)進(jìn)度信息就是“上下文”了。
那上下文都包括哪些內(nèi)容呢袜瞬?具體來說怜俐,它包括了寄存器的存儲(chǔ)內(nèi)容以及程序計(jì)數(shù)器存儲(chǔ)的指令內(nèi)容。CPU 寄存器負(fù)責(zé)存儲(chǔ)已經(jīng)邓尤、正在和將要執(zhí)行的任務(wù)拍鲤,程序計(jì)數(shù)器負(fù)責(zé)存儲(chǔ)CPU 正在執(zhí)行的指令位置以及即將執(zhí)行的下一條指令的位置。
在當(dāng)前 CPU 數(shù)量遠(yuǎn)遠(yuǎn)不止一個(gè)的情況下汞扎,操作系統(tǒng)將 CPU 輪流分配給線程任務(wù)殿漠,此時(shí)的上下文切換就變得更加頻繁了,并且存在跨 CPU 上下文切換,比起單核上下文切換,跨核切換更加昂貴疾掰。
上下文切換的誘因
在操作系統(tǒng)中,上下文切換的類型還可以分為進(jìn)程間的上下文切換和線程間的上下文切換莲蜘。而在多線程編程中,我們主要面對(duì)的就是線程間的上下文切換導(dǎo)致的性能問題帘营,下面我們就重點(diǎn)看看究竟是什么原因?qū)е铝硕嗑€程的上下文切換票渠。開始之前,先看下系統(tǒng)線程的生命周期狀態(tài)芬迄。
結(jié)合圖示可知问顷,線程主要有“新建”(NEW)、“就緒”(RUNNABLE)禀梳、“運(yùn)行”(RUNNING)杜窄、“阻塞”(BLOCKED)、“死亡”(DEAD)五種狀態(tài)算途。到了Java層面它們都被映射為了NEW塞耕、RUNABLE、BLOCKED嘴瓤、WAITING扫外、TIMED_WAITING、TERMINADTED等6種狀態(tài)廓脆。
在這個(gè)運(yùn)行過程中筛谚,線程由RUNNABLE轉(zhuǎn)為非RUNNABLE的過程就是線程上下文切換。
一個(gè)線程的狀態(tài)由 RUNNING 轉(zhuǎn)為 BLOCKED 停忿,再由 BLOCKED 轉(zhuǎn)為 RUNNABLE 驾讲,然后再被調(diào)度器選中執(zhí)行,這就是一個(gè)上下文切換的過程。
當(dāng)一個(gè)線程從 RUNNING 狀態(tài)轉(zhuǎn)為 BLOCKED 狀態(tài)時(shí)蝎毡,我們稱為一個(gè)線程的暫停厚柳,線程暫停被切出之后氧枣,操作系統(tǒng)會(huì)保存相應(yīng)的上下文沐兵,以便這個(gè)線程稍后再次進(jìn)入 RUNNABLE 狀態(tài)時(shí)能夠在之前執(zhí)行進(jìn)度的基礎(chǔ)上繼續(xù)執(zhí)行。
當(dāng)一個(gè)線程從 BLOCKED 狀態(tài)進(jìn)入到 RUNNABLE 狀態(tài)時(shí)便监,我們稱為一個(gè)線程的喚醒扎谎,此時(shí)線程將獲取上次保存的上下文繼續(xù)完成執(zhí)行。
通過線程的運(yùn)行狀態(tài)以及狀態(tài)間的相互切換烧董,我們可以了解到毁靶,多線程的上下文切換實(shí)際上就是由多線程兩個(gè)運(yùn)行狀態(tài)的互相切換導(dǎo)致的。
那么在線程運(yùn)行時(shí)逊移,線程狀態(tài)由 RUNNING 轉(zhuǎn)為 BLOCKED 或者由 BLOCKED 轉(zhuǎn)為 RUNNABLE预吆,這又是什么誘發(fā)的呢?
我們可以分兩種情況來分析胳泉,一種是程序本身觸發(fā)的切換拐叉,這種我們稱為自發(fā)性上下文切換,另一種是由系統(tǒng)或者虛擬機(jī)誘發(fā)的非自發(fā)性上下文切換扇商。
自發(fā)性上下文切換指線程由 Java 程序調(diào)用導(dǎo)致切出凤瘦,在多線程編程中,執(zhí)行調(diào)用以下方法或關(guān)鍵字案铺,常常就會(huì)引發(fā)自發(fā)性上下文切換蔬芥。
- sleep()
- wait()
- yield()
- join()
- park()
- synchronized
- lock
非自發(fā)性上下文切換指線程由于調(diào)度器的原因被迫切出。常見的有:線程被分配的時(shí)間片用完控汉,虛擬機(jī)垃圾回收導(dǎo)致或者執(zhí)行優(yōu)先級(jí)的問題導(dǎo)致笔诵。
這里重點(diǎn)說下“虛擬機(jī)垃圾回收為什么會(huì)導(dǎo)致上下文切換”。在 Java 虛擬機(jī)中姑子,對(duì)象的內(nèi)存都是由虛擬機(jī)中的堆分配的嗤放,在程序運(yùn)行過程中,新的對(duì)象將不斷被創(chuàng)建壁酬,如果舊的對(duì)象使用后不進(jìn)行回收次酌,堆內(nèi)存將很快被耗盡。Java 虛擬機(jī)提供了一種回收機(jī)制舆乔,對(duì)創(chuàng)建后不再使用的對(duì)象進(jìn)行回收岳服,從而保證堆內(nèi)存的可持續(xù)性分配。而這種垃圾回收機(jī)制的使用有可能會(huì)導(dǎo)致 stop-the-world 事件的發(fā)生希俩,這其實(shí)就是一種線程暫停行為吊宋。
發(fā)現(xiàn)上下文切換
我們總說上下文切換會(huì)帶來系統(tǒng)開銷,那它帶來的性能問題是不是真有這么糟糕呢颜武?我們又該怎么去監(jiān)測(cè)到上下文切換璃搜?上下文切換到底開銷在哪些環(huán)節(jié)拖吼?接下來我將給出一段代碼,來對(duì)比串聯(lián)執(zhí)行和并發(fā)執(zhí)行的速度这吻,然后一一解答這些問題吊档。
public class DemoApplication {
public static void main(String[] args) {
//運(yùn)行多線程
MultiThreadTester test1 = new MultiThreadTester();
test1.Start();
//運(yùn)行單線程
SerialTester test2 = new SerialTester();
test2.Start();
}
static class MultiThreadTester extends ThreadContextSwitchTester {
@Override
public void Start() {
long start = System.currentTimeMillis();
MyRunnable myRunnable1 = new MyRunnable();
Thread[] threads = new Thread[4];
//創(chuàng)建多個(gè)線程
for (int i = 0; i < 4; i++) {
threads[i] = new Thread(myRunnable1);
threads[i].start();
}
for (int i = 0; i < 4; i++) {
try {
//等待一起運(yùn)行完
threads[i].join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
long end = System.currentTimeMillis();
System.out.println("multi thread exce time: " + (end - start) + "s");
System.out.println("counter: " + counter);
}
// 創(chuàng)建一個(gè)實(shí)現(xiàn)Runnable的類
class MyRunnable implements Runnable {
public void run() {
while (counter < 100000000) {
synchronized (this) {
if(counter < 100000000) {
increaseCounter();
}
}
}
}
}
}
//創(chuàng)建一個(gè)單線程
static class SerialTester extends ThreadContextSwitchTester{
@Override
public void Start() {
long start = System.currentTimeMillis();
for (long i = 0; i < count; i++) {
increaseCounter();
}
long end = System.currentTimeMillis();
System.out.println("serial exec time: " + (end - start) + "s");
System.out.println("counter: " + counter);
}
}
//父類
static abstract class ThreadContextSwitchTester {
public static final int count = 100000000;
public volatile int counter = 0;
public int getCount() {
return this.counter;
}
public void increaseCounter() {
this.counter += 1;
}
public abstract void Start();
}
}
執(zhí)行之后,看一下兩者的時(shí)間測(cè)試結(jié)果:
通過數(shù)據(jù)對(duì)比我們可以看到: 串聯(lián)的執(zhí)行速度比并發(fā)的執(zhí)行速度要快唾糯。這就是因?yàn)榫€程的上下文切換導(dǎo)致了額外的開銷怠硼,使用 Synchronized 鎖關(guān)鍵字,導(dǎo)致了資源競(jìng)爭(zhēng)移怯,從而引起了上下文切換香璃,但即使不使用 Synchronized 鎖關(guān)鍵字,并發(fā)的執(zhí)行速度也無法超越串聯(lián)的執(zhí)行速度舟误,這是因?yàn)槎嗑€程同樣存在著上下文切換葡秒。Redis、NodeJS的設(shè)計(jì)就很好地體現(xiàn)了單線程串行的優(yōu)勢(shì)嵌溢。
在 Linux 系統(tǒng)下眯牧,可以使用 Linux 內(nèi)核提供的 vmstat 命令,來監(jiān)視 Java 程序運(yùn)行過程中系統(tǒng)的上下文切換頻率堵腹,cs如下圖所示:
如果是監(jiān)視某個(gè)應(yīng)用的上下文切換炸站,就可以使用 pidstat命令監(jiān)控指定進(jìn)程的 Context Switch 上下文切換。
由于 Windows 沒有像 vmstat 這樣的工具疚顷,在 Windows 下旱易,我們可以使用 Process Explorer,來查看程序執(zhí)行時(shí)腿堤,線程間上下文切換的次數(shù)阀坏。
至于系統(tǒng)開銷具體發(fā)生在切換過程中的哪些具體環(huán)節(jié),總結(jié)如下:
- 操作系統(tǒng)保存和恢復(fù)上下文笆檀;
- 調(diào)度器進(jìn)行線程調(diào)度忌堂;
- 處理器高速緩存重新加載;
- 上下文切換也可能導(dǎo)致整個(gè)高速緩存區(qū)被沖刷酗洒,從而帶來時(shí)間開銷士修。
如果是單個(gè)線程,在 CPU 調(diào)用之后樱衷,那么它基本上是不會(huì)被調(diào)度出去的棋嘲。如果可運(yùn)行的線程數(shù)遠(yuǎn)大于 CPU 數(shù)量,那么操作系統(tǒng)最終會(huì)將某個(gè)正在運(yùn)行的線程調(diào)度出來矩桂,從而使其它線程能夠使用 CPU 沸移,這就會(huì)導(dǎo)致上下文切換。
還有,在多線程中如果使用了競(jìng)爭(zhēng)鎖雹锣,當(dāng)線程由于等待競(jìng)爭(zhēng)鎖而被阻塞時(shí)网沾,JVM 通常會(huì)將這個(gè)線程掛起,并允許它被交換出去蕊爵。如果頻繁地發(fā)生阻塞辉哥,CPU 密集型的程序就會(huì)發(fā)生更多的上下文切換。
那么問題來了在辆,我們知道在某些場(chǎng)景下使用多線程是非常必要的证薇,但多線程編程給系統(tǒng)帶來了上下文切換度苔,從而增加的性能開銷也是實(shí)打?qū)嵈嬖诘拇衣āD敲次覀冊(cè)撊绾蝺?yōu)化多線程上下文切換呢?
競(jìng)爭(zhēng)鎖優(yōu)化
大多數(shù)人在多線程編程中碰到性能問題寇窑,第一反應(yīng)多是想到了鎖鸦概。
多線程對(duì)鎖資源的競(jìng)爭(zhēng)會(huì)引起上下文切換,還有鎖競(jìng)爭(zhēng)導(dǎo)致的線程阻塞越多甩骏,上下文切換就越頻繁窗市,系統(tǒng)的性能開銷也就越大。由此可見饮笛,在多線程編程中咨察,鎖其實(shí)不是性能開銷的根源,競(jìng)爭(zhēng)鎖才是福青。
下面我們談一下鎖優(yōu)化的一些思路:
1.減少鎖的持有時(shí)間
我們知道摄狱,鎖的持有時(shí)間越長(zhǎng),就意味著有越多的線程在等待該競(jìng)爭(zhēng)資源釋放无午。如果是Synchronized同步鎖資源媒役,就不僅是帶來線程間的上下文切換,還有可能會(huì)增加進(jìn)程間的上下文切換宪迟。
可以將一些與鎖無關(guān)的代碼移出同步代碼塊酣衷,尤其是那些開銷較大的操作以及可能被阻塞的操作。
- 優(yōu)化前
public synchronized void mySyncMethod(){
businesscode1();
mutextMethod();
businesscode2();
}
- 優(yōu)化后
public void mySyncMethod(){
businesscode1();
synchronized(this)
{
mutextMethod();
}
businesscode2();
}
2.降低鎖的粒度
同步鎖可以保證對(duì)象的原子性次泽,我們可以考慮將鎖粒度拆分得更小一些穿仪,以此避免所有線程對(duì)一個(gè)鎖資源的競(jìng)爭(zhēng)過于激烈。具體方式有以下兩種:
- 鎖分離
與傳統(tǒng)鎖不同的是意荤,讀寫鎖實(shí)現(xiàn)了鎖分離啊片,也就是說讀寫鎖是由“讀鎖”和“寫鎖”兩個(gè)鎖實(shí)現(xiàn)的,其規(guī)則是可以共享讀袭异,但只有一個(gè)寫钠龙。
這樣做的好處是,在多線程讀的時(shí)候,讀讀是不互斥的碴里,讀寫是互斥的沈矿,寫寫是互斥的。而傳統(tǒng)的獨(dú)占鎖在沒有區(qū)分讀寫鎖的時(shí)候咬腋,讀寫操作一般是:讀讀互斥羹膳、讀寫互斥、寫寫互斥根竿。所以在讀遠(yuǎn)大于寫的多線程場(chǎng)景中陵像,鎖分離避免了在高并發(fā)讀情況下的資源競(jìng)爭(zhēng),從而避免了上下文切換寇壳。
- 鎖分段
我們?cè)谑褂面i來保證集合或者大對(duì)象原子性時(shí)醒颖,可以考慮將鎖對(duì)象進(jìn)一步分解。例如壳炎,Java1.8 之前版本的 ConcurrentHashMap 就使用了鎖分段泞歉。
3.非阻塞樂觀鎖替代競(jìng)爭(zhēng)鎖
volatile關(guān)鍵字的作用是保障可見性及有序性,volatile的讀寫操作不會(huì)導(dǎo)致上下文切換匿辩,因此開銷比較小腰耙。 但是,volatile不能保證操作變量的原子性铲球,因?yàn)闆]有鎖的排他性挺庞。
而 CAS 是一個(gè)原子的 if-then-act 操作,CAS 是一個(gè)無鎖算法實(shí)現(xiàn)稼病,保障了對(duì)一個(gè)共享變量讀寫操作的一致性选侨。CAS 操作中有 3 個(gè)操作數(shù),內(nèi)存值 V溯饵、舊的預(yù)期值 A和要修改的新值 B侵俗,當(dāng)且僅當(dāng) A 和 V 相同時(shí),將 V 修改為 B丰刊,否則什么都不做隘谣,CAS 算法將不會(huì)導(dǎo)致上下文切換。Java 的 Atomic 包就使用了 CAS 算法來更新數(shù)據(jù)啄巧,就不需要額外加鎖寻歧。
上面我們了解了如何從編碼層面去優(yōu)化競(jìng)爭(zhēng)鎖,那么除此之外秩仆,JVM內(nèi)部其實(shí)也對(duì)Synchronized同步鎖做了優(yōu)化码泛。
在JDK1.6中,JVM將Synchronized同步鎖分為了偏向鎖澄耍、輕量級(jí)鎖噪珊、自旋鎖以及重量級(jí)鎖晌缘,優(yōu)化路徑也是按照以上順序進(jìn)行。JIT 編譯器在動(dòng)態(tài)編譯同步塊的時(shí)候痢站,也會(huì)通過鎖消除磷箕、鎖粗化的方式來優(yōu)化該同步鎖。
wait/notify優(yōu)化
在 Java 中阵难,我們可以通過配合調(diào)用 Object 對(duì)象的 wait()方法和 notify()方法或 notifyAll() 方法來實(shí)現(xiàn)線程間的通信岳枷。
在線程中調(diào)用 wait()方法,將阻塞等待其它線程的通知(其它線程調(diào)用notify()方法或notifyAll()方法)呜叫,在線程中調(diào)用 notify()方法或 notifyAll()方法空繁,將通知其它線程從 wait()方法處返回。
下面我們通過wait() / notify()來實(shí)現(xiàn)一個(gè)簡(jiǎn)單的生產(chǎn)者和消費(fèi)者的案例朱庆,代碼如下:
public class WaitNotifyTest {
public static void main(String[] args) {
Vector<Integer> pool=new Vector<Integer>();
Producer producer=new Producer(pool, 10);
Consumer consumer=new Consumer(pool);
new Thread(producer).start();
new Thread(consumer).start();
}
}
/**
* 生產(chǎn)者
* @author admin
*
*/
class Producer implements Runnable{
private Vector<Integer> pool;
private Integer size;
public Producer(Vector<Integer> pool, Integer size) {
this.pool = pool;
this.size = size;
}
public void run() {
for(;;){
try {
System.out.println("生產(chǎn)一個(gè)商品 ");
produce(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
private void produce(int i) throws InterruptedException{
while(pool.size()==size){
synchronized (pool) {
System.out.println("生產(chǎn)者等待消費(fèi)者消費(fèi)商品,當(dāng)前商品數(shù)量為"+pool.size());
pool.wait();//等待消費(fèi)者消費(fèi)
}
}
synchronized (pool) {
pool.add(i);
pool.notifyAll();//生產(chǎn)成功盛泡,通知消費(fèi)者消費(fèi)
}
}
}
/**
* 消費(fèi)者
* @author admin
*
*/
class Consumer implements Runnable{
private Vector<Integer> pool;
public Consumer(Vector<Integer> pool) {
this.pool = pool;
}
public void run() {
for(;;){
try {
System.out.println("消費(fèi)一個(gè)商品");
consume();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
private void consume() throws InterruptedException{
synchronized (pool) {
while(pool.isEmpty()) {
System.out.println("消費(fèi)者等待生產(chǎn)者生產(chǎn)商品,當(dāng)前商品數(shù)量為"+pool.size());
pool.wait();//等待生產(chǎn)者生產(chǎn)商品
}
}
synchronized (pool) {
pool.remove(0);
pool.notifyAll();//通知生產(chǎn)者生產(chǎn)商品
}
}
}
wait/notify的使用導(dǎo)致了較多的上下文切換
結(jié)合以下圖片,我們可以看到椎工,在消費(fèi)者第一次申請(qǐng)到鎖之前饭于,發(fā)現(xiàn)沒有商品消費(fèi)蜀踏,此時(shí)會(huì)執(zhí)行 Object.wait() 方法维蒙,這里會(huì)導(dǎo)致線程掛起,進(jìn)入阻塞狀態(tài)果覆,這里為一次上下文切換颅痊。
當(dāng)生產(chǎn)者獲取到鎖并執(zhí)行notifyAll()之后,會(huì)喚醒處于阻塞狀態(tài)的消費(fèi)者線程局待,此時(shí)這里又發(fā)生了一次上下文切換斑响。
被喚醒的等待線程在繼續(xù)運(yùn)行時(shí),需要再次申請(qǐng)相應(yīng)對(duì)象的內(nèi)部鎖钳榨,此時(shí)等待線程可能需要和其它新來的活躍線程爭(zhēng)用內(nèi)部鎖舰罚,這也可能會(huì)導(dǎo)致上下文切換。
如果有多個(gè)消費(fèi)者線程同時(shí)被阻塞薛耻,用notifyAll()方法营罢,將會(huì)喚醒所有阻塞的線程。而某些商品依然沒有庫(kù)存饼齿,過早地喚醒這些沒有庫(kù)存的商品的消費(fèi)線程饲漾,可能會(huì)導(dǎo)致線程再次進(jìn)入阻塞狀態(tài),從而引起不必要的上下文切換缕溉。
優(yōu)化wait/notify的使用考传,減少上下文切換
首先,我們?cè)诙鄠€(gè)不同消費(fèi)場(chǎng)景中证鸥,可以使用 Object.notify() 替代 Object.notifyAll()僚楞。 因?yàn)镺bject.notify() 只會(huì)喚醒指定線程勤晚,不會(huì)過早地喚醒其它未滿足需求的阻塞線程,所以可以減少相應(yīng)的上下文切換泉褐。
其次运翼,在生產(chǎn)者執(zhí)行完 Object.notify() / notifyAll()喚醒其它線程之后,應(yīng)該盡快地釋放內(nèi)部鎖兴枯,以避免其它線程在喚醒之后長(zhǎng)時(shí)間地持有鎖處理業(yè)務(wù)操作血淌,這樣可以避免被喚醒的線程再次申請(qǐng)相應(yīng)內(nèi)部鎖的時(shí)候等待鎖的釋放。
最后财剖,為了避免長(zhǎng)時(shí)間等待悠夯,我們常會(huì)使用Object.wait (long)設(shè)置等待超時(shí)時(shí)間,但線程無法區(qū)分其返回是由于等待超時(shí)還是被通知線程喚醒躺坟,從而導(dǎo)致線程再次嘗試獲取鎖操作沦补,增加了上下文切換。
這里我建議使用Lock鎖結(jié)合Condition 接口替代Synchronized內(nèi)部鎖中的 wait / notify咪橙,實(shí)現(xiàn)等待/通知夕膀。這樣做不僅可以解決上述的Object.wait(long) 無法區(qū)分的問題,還可以解決線程被過早喚醒的問題美侦。
Condition 接口定義的 await 方法 产舞、signal 方法和 signalAll 方法分別相當(dāng)于 Object.wait()、 Object.notify()和 Object.notifyAll()菠剩。
合理地設(shè)置線程池大小易猫,避免創(chuàng)建過多線程
線程池的線程數(shù)量設(shè)置不宜過大,因?yàn)橐坏┚€程池的工作線程總數(shù)超過系統(tǒng)所擁有的處理器數(shù)量具壮,就會(huì)導(dǎo)致過多的上下文切換准颓。
還有一種情況就是,在有些創(chuàng)建線程池的方法里棺妓,線程數(shù)量設(shè)置不會(huì)直接暴露給我們攘已。比如,用 Executors.newCachedThreadPool() 創(chuàng)建的線程池怜跑,該線程池會(huì)復(fù)用其內(nèi)部空閑的線程來處理新提交的任務(wù)样勃,如果沒有,再創(chuàng)建新的線程(不受 MAX_VALUE 限制)妆艘,這樣的線程池如果碰到大量且耗時(shí)長(zhǎng)的任務(wù)場(chǎng)景彤灶,就會(huì)創(chuàng)建非常多的工作線程,從而導(dǎo)致頻繁的上下文切換批旺。因此幌陕,這類線程池就只適合處理大量且耗時(shí)短的非阻塞任務(wù)。
使用協(xié)程實(shí)現(xiàn)非阻塞等待
相信很多人一聽到協(xié)程(Coroutines)汽煮,馬上想到的就是Go語言搏熄。協(xié)程對(duì)于大部分 Java 程序員來說可能還有點(diǎn)陌生棚唆,但其在 Go 中的使用相對(duì)來說已經(jīng)很成熟了。
協(xié)程是一種比線程更加輕量級(jí)的東西心例,相比于由操作系統(tǒng)內(nèi)核來管理的進(jìn)程和線程宵凌,協(xié)程則完全由程序本身所控制,也就是在用戶態(tài)執(zhí)行止后。協(xié)程避免了像線程切換那樣產(chǎn)生的上下文切換瞎惫,在性能方面得到了很大的提升。
減少Java虛擬機(jī)的垃圾回收
很多 JVM 垃圾回收器(serial收集器译株、ParNew收集器)在回收舊對(duì)象時(shí)瓜喇,會(huì)產(chǎn)生內(nèi)存碎片,從而需要進(jìn)行內(nèi)存整理歉糜,在這個(gè)過程中就需要移動(dòng)存活的對(duì)象乘寒。而移動(dòng)內(nèi)存對(duì)象就意味著這些對(duì)象所在的內(nèi)存地址會(huì)發(fā)生變化,因此在移動(dòng)對(duì)象前需要暫停線程匪补,在移動(dòng)完成后需要再次喚醒該線程伞辛。因此減少 JVM 垃圾回收的頻率可以有效地減少上下文切換。
總結(jié)
上下文切換是多線程編程性能消耗的原因之一夯缺,而競(jìng)爭(zhēng)鎖蚤氏、線程間的通信以及過多地創(chuàng)建線程等多線程編程操作,都會(huì)給系統(tǒng)帶來上下文切換喳逛。除此之外瞧捌,I/O阻塞以及JVM的垃圾回收也會(huì)增加上下文切換。系統(tǒng)和 Java 程序自發(fā)性以及非自發(fā)性的調(diào)用操作润文,就會(huì)導(dǎo)致上下文切換,從而帶來系統(tǒng)開銷殿怜。
線程越多典蝌,系統(tǒng)的運(yùn)行速度不一定越快。那么我們平時(shí)在并發(fā)量比較大的情況下头谜,什么時(shí)候用單線程骏掀,什么時(shí)候用多線程呢?
一般在單個(gè)邏輯比較簡(jiǎn)單柱告,而且速度相對(duì)來非辰赝裕快的情況下,我們可以使用單線程际度。例如 Redis葵袭,從內(nèi)存中快速讀取值,不用考慮 I/O 瓶頸帶來的阻塞問題乖菱。而在邏輯相對(duì)來說很復(fù)雜的場(chǎng)景坡锡,等待時(shí)間相對(duì)較長(zhǎng)又或者是需要大量計(jì)算的場(chǎng)景蓬网,我建議使用多線程來提高系統(tǒng)的整體性能。例如鹉勒,NIO 時(shí)期的文件讀寫操作帆锋、圖像處理以及大數(shù)據(jù)分析等。
本文由mdnice多平臺(tái)發(fā)布