?每日一例 | 并發(fā)真的比串行快嗎?

前言
在找到這個(gè)問題答案之前,我回想了自己所有掌握的知識(shí)點(diǎn),以及我的相關(guān)經(jīng)驗(yàn),我發(fā)現(xiàn)我無法回答這個(gè)問題,當(dāng)然在面試中,也有類似的問題被問到過:是不是線程池越大越好?我通常的回答是,不是,要根據(jù)系統(tǒng)的內(nèi)存情況、系統(tǒng)的并發(fā)情況,綜合來看,但怎么綜合來看,我是不知道的。
直到昨天,隨手翻開《并發(fā)編程的藝術(shù)》,才讓我真正找到了這個(gè)問題的答案,當(dāng)然相關(guān)問題的答案也開始變得明朗了,所以今天我們要探討的問題,就是找到并發(fā)某些情況下慢的原因。
不知道有沒有小伙伴還不知道并發(fā)和串行,這里我們簡(jiǎn)單說下,并發(fā)就是我們經(jīng)常說的多線程,就有由多個(gè)線程共同去完成一個(gè)任務(wù),同步進(jìn)行,目的是為了提高效率;串行也就是單線程,就是一個(gè)線程完成一個(gè)任務(wù),效率相對(duì)比較低。好了,我們開始正文吧!
一個(gè)示例
在找到問題的答案之前,我們要先寫一段測(cè)試代碼:
private static final long count = 10000L;
public static void main(String[] args) throws Exception{
concurrentcy();;
serial();
}
private static void concurrentcy() throws Exception {
long start = System.currentTimeMillis();
Thread thread = new Thread(() -> {
int a = 0;
for (long i = 0; i < count; i++) {
a += 5;
}
});
thread.start();
int b = 0;
for (long i = 0; i < count; i++) {
b--;
}
long time = System.currentTimeMillis() - start;
thread.join();
System.out.println(String.format("concurrecy: %dms, b=%d", time, b));
}
private static void serial() {
long start = System.currentTimeMillis();
int a = 0;
for (long i = 0; i < count; i++) {
a += 5;
}
int b = 0;
for (long i = 0; i < count; i++) {
b --;
}
long time = System.currentTimeMillis() - start;
System.out.println(String.format("serial: %dms, b=%d", time, b));
}
上面的方法分別定義了一個(gè)多線程并發(fā)的方法和一個(gè)串行運(yùn)行的方法,他們的作用都是執(zhí)行兩次for循環(huán),循環(huán)內(nèi)是簡(jiǎn)單的業(yè)務(wù)。這里需要說明的是,thread.join()就是在此處加入線程,也就是第二次執(zhí)行。
運(yùn)行結(jié)果
當(dāng)count為10000的時(shí)候,他們的運(yùn)行結(jié)果如下:

還是讓人挺意外的,串行方式竟然比并行快了100倍,這個(gè)數(shù)據(jù)可能和我的電腦有關(guān)系,二代i3的老爺機(jī),多線程也太差了吧,作者的數(shù)據(jù)是差了1ms,我這也差距太大了。
count到十萬的時(shí)候,差距變小了,差了快20倍:

count到百萬的時(shí)候,差了差不多十五倍:

count到千萬的時(shí)候,差了差不多五倍:

count到一億的時(shí)候,差了不到0.5倍:

好了,這電腦該換了,多線程在各種數(shù)量級(jí)下都沒有串行效率高。直接上作者的結(jié)果吧:
| 循環(huán)次數(shù) | 串行 | 并行 | 并發(fā)比串行快多少 |
|---|---|---|---|
| 1萬 | 0 | 1 | 慢 |
| 10萬 | 4 | 3 | 差不多 |
| 100萬 | 5 | 5 | 差不多 |
| 1000萬 | 18 | 9 | 快一倍 |
| 1億 | 130 | 77 | 快一倍 |
結(jié)論
根據(jù)上面這些結(jié)果來看,在數(shù)據(jù)量較小的情況下,并發(fā)效率不如串行,但是隨著數(shù)據(jù)量不斷增大,并發(fā)的效率就體現(xiàn)出來了。
數(shù)據(jù)量小的時(shí)候,并行慢的原因是上下文切換比較耗時(shí)。按照我們的代碼,所有業(yè)務(wù)執(zhí)行完成需要切換4次:第一次,主線程切換到第一次循環(huán)線程,第一次循環(huán)線程切換到主線程,主線程切換到第二次循環(huán)線程,第二次循環(huán)線程切換到主線程。所以數(shù)據(jù)量小的時(shí)候,大部分時(shí)間都花費(fèi)在線程之間的上下文切換上了,所以比較慢,后面隨著數(shù)據(jù)量增加,這種上下文切換時(shí)長(zhǎng)相比程序執(zhí)行時(shí)長(zhǎng)就可以忽略了,所以這時(shí)候就是它發(fā)揮真正的技術(shù)的時(shí)候了。
并發(fā)慢的原因在于上下文切換,所以在使用多線程的時(shí)候,我們要盡可能減少線程之間的上下文切換,最明顯的一個(gè)點(diǎn)就是,在使用線程池的時(shí)候,不要把線程池設(shè)置過大,過大會(huì)導(dǎo)致上下文切換過于頻繁,從而讓程序效率變低。這里提供幾個(gè)命令(書里面的知識(shí)),可以讓你查看系統(tǒng)的上下文切換數(shù)據(jù):
vmstat # 統(tǒng)計(jì)上下文切換次數(shù)
lmbench3 # 統(tǒng)計(jì)上下文切換時(shí)長(zhǎng)
這兩個(gè)工具都是linux環(huán)境的,可以協(xié)助你排查線程池的問題。
從程序?qū)用?,可以通過如下方式,減少上下文切換:
無鎖并發(fā)編程 CAS算法 使用最少線程 協(xié)程
好了,今天就到這里吧
- END -