談?wù)劧嗑€程的上線文切換

大家好泉哈,我是易安!

我們知道破讨,在并發(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ā)布

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末禽额,一起剝皮案震驚了整個(gè)濱河市锯厢,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌脯倒,老刑警劉巖哲鸳,帶你破解...
    沈念sama閱讀 221,576評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異盔憨,居然都是意外死亡徙菠,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,515評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門郁岩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來婿奔,“玉大人,你說我怎么就攤上這事问慎∑继” “怎么了?”我有些...
    開封第一講書人閱讀 168,017評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵如叼,是天一觀的道長(zhǎng)冰木。 經(jīng)常有香客問我,道長(zhǎng)笼恰,這世上最難降的妖魔是什么踊沸? 我笑而不...
    開封第一講書人閱讀 59,626評(píng)論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮社证,結(jié)果婚禮上逼龟,老公的妹妹穿的比我還像新娘。我一直安慰自己追葡,他們只是感情好腺律,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,625評(píng)論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著宜肉,像睡著了一般匀钧。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上谬返,一...
    開封第一講書人閱讀 52,255評(píng)論 1 308
  • 那天之斯,我揣著相機(jī)與錄音,去河邊找鬼朱浴。 笑死吊圾,一個(gè)胖子當(dāng)著我的面吹牛达椰,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播项乒,決...
    沈念sama閱讀 40,825評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼啰劲,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了檀何?” 一聲冷哼從身側(cè)響起蝇裤,我...
    開封第一講書人閱讀 39,729評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎频鉴,沒想到半個(gè)月后栓辜,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,271評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡垛孔,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,363評(píng)論 3 340
  • 正文 我和宋清朗相戀三年狈邑,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了橘蜜。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片子房。...
    茶點(diǎn)故事閱讀 40,498評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡饲宛,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出概作,到底是詐尸還是另有隱情腋妙,我是刑警寧澤,帶...
    沈念sama閱讀 36,183評(píng)論 5 350
  • 正文 年R本政府宣布讯榕,位于F島的核電站骤素,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏愚屁。R本人自食惡果不足惜济竹,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,867評(píng)論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望集绰。 院中可真熱鬧规辱,春花似錦、人聲如沸栽燕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,338評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽碍岔。三九已至,卻和暖如春朵夏,著一層夾襖步出監(jiān)牢的瞬間蔼啦,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,458評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工仰猖, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留捏肢,地道東北人奈籽。 一個(gè)月前我還...
    沈念sama閱讀 48,906評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像鸵赫,于是被迫代替她去往敵國(guó)和親衣屏。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,507評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容