雙十一前的一個多月,全部的電商相關的系統都在進行壓測,不斷的優化系統,咱們的電商ERP系統也進行了一個多月的壓測和優化的過程,在這其中,咱們發現了大量的超時報警,經過工具分析,咱們發現是cs指標很高,而後分析日誌,咱們發現有大量wait()相關的Exception,這個時候咱們懷疑是在多線程併發處理的時候,出現了大量的線程處理不及時致使的這些問題,後來咱們經過減少線程池最大線程數,再進行壓測發現系統的性能有了不小的提高。編程
咱們都知道,在併發編程中,並非線程越多就效率越高,線程數太少可能致使資源不能充分利用,線程數太多可能致使競爭資源激烈,而後上下文切換頻繁形成系統的額外開銷。緩存
咱們都知道,在處理多線程併發任務的時候,處理器會給每一個線程分配CPU時間片,線程在各自分配的時間片內執行任務,每一個時間片的大小通常爲幾十毫秒,因此在一秒鐘就可能發生幾十上百次的線程相互切換,給咱們的感受就是同時進行的。微信
線程只在分配的時間片內佔用處理器,當一個線程分配的時間片用完了,或者自身緣由被迫暫停運行的時候,就會有另一個線程來佔用這個處理器,這種一個線程讓出處理器使用權,另一個線程獲取處理器使用權的過程就叫作上下文切換。多線程
一個線程讓出處理器使用權,就是「切出」;另一個線程獲取處理器使用權。就是「切入」,在這個切入切出的過程當中,操做系統會保存和恢復相關的進度信息,這個進度信息就是咱們常說的「上下文」,上下文中通常包含了寄存器的存儲內容以及程序計數器存儲的指令內容。併發
多線程編程中,咱們知道線程間的上下文切換會致使性能問題,那麼是什麼緣由形成的線程間的上下文切換。咱們先看一下線程的生命週期,從中看一下找找答案。ide
線程的五種狀態咱們都很是清楚:NEW、RUNNABLE、RUNNING、BLOCKED、DEAD,對應的Java中的六種狀態分別爲:NEW、RUNABLE、BLOCKED、WAINTING、TIMED_WAITING、TERMINADTED。
工具
圖中,一個線程從RUNNABLE到RUNNING的過程就是線程的上下文切換,RUNNING狀態到BLOCKED、再到RUNNABLE、再從RUNNABLE到RUNNING的過程就是一個上下文切換的過程。一個線程從RUNNING轉爲BLOCKED狀態時,咱們叫作線程的暫停,線程暫停了,這個處理器就會有別的線程來佔用,操做系統就會保存相應的上下文,爲了這個線程之後再進入RUNNABLE狀態時能夠接着以前的執行進度繼續執行。當線程從BLOCKED狀態進入到RUNNABLE時,也就是線程的喚醒,此時線程將獲取上次保存的上下文信息。性能
咱們看到,多線程的上下文切換實際上就是多線程兩個運行狀態的相互切換致使的。測試
咱們知道兩種狀況能夠致使上下文切換:一種是程序自己觸發的切換,這種咱們通常稱爲自發性上下文切換,另外一種是系統或者虛擬機致使的上下文切換,咱們稱之爲非自發性上下文切換。優化
自發性上下文是線程由Java程序調用致使切出,通常是在編碼的時候,調用一下幾個方法或關鍵字:
sleep()
wait()
yield()
join()
park();
synchronized
lock
非自發的上下文切換常見的有:線程被分配的時間片用完,虛擬機垃圾回收致使,或者執行優先級的問題致使。
咱們經過一個例子來看一下併發執行和串行執行的速度對比;
public class DemoApplication {
public static void main(String[] args) {
//運行多線程
MultiThreadTester test1 = new MultiThreadTester();
test1.Start();
//運行單線程
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];
//建立多個線程
for (int i = 0; i < 4; i++) {
threads[i] = new Thread(myRunnable1);
threads[i].start();
}
for (int i = 0; i < 4; i++) {
try {
//等待一塊兒運行完
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);
}
// 建立一個實現Runnable的類
class MyRunnable implements Runnable {
public void run() {
while (counter < 100000000) {
synchronized (this) {
if(counter < 100000000) {
increaseCounter();
}
}
}
}
}
}
//建立一個單線程
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();
}
}
執行結果;
multi thread exce time: 5149s
counter: 100000000
serial exec time: 956s
counter: 100000000
經過執行的結果對比咱們能夠看到,串行的執行速度比並發執行的速度更快,這其中就是由於多線程的上下文切換致使了系統額外的開銷,使用的synchronized關鍵字,致使了鎖競爭,致使了線程上下文切換,這個地方若是不使用synchronized關鍵字,併發的執行效率也比不上串行執行的速度,由於沒有鎖競爭多線程的上下文切換依然存在。
系統開銷在上下文切換的哪些環節:
上下文就是一個釋放處理器的使用權,另一個線程獲取處理器的使用權,自發和非自發的調用操做,都會致使上下文切換,會致使系統資源開銷。線程越多不必定執行的速度越快,在單個邏輯比較簡單的時候,並且速度相對來講很是快的狀況下,咱們推薦是使用單線程。若是邏輯很是複雜,或者須要進行大量的計算的地方,咱們建議使用多線程來提升系統的性能。
- END -
閒聊:天氣愈來愈冷,你們能夠關注公衆號,便可添加個人我的微信號,拉你進技術交流羣,一塊兒抱團取暖吧~