怎么并線(三條電線怎么并線)
633
2025-04-02
簡(jiǎn)介:
并發(fā)編程的目的是為了讓程序運(yùn)行的更快,但是,并不是啟動(dòng)更多的線程就能讓程序最大限度的并發(fā)執(zhí)行。在進(jìn)行并發(fā)編程時(shí),如果希望通過(guò)多線程執(zhí)行任務(wù)讓程序運(yùn)行的更快,會(huì)面臨非常多的挑戰(zhàn),比如上下文切換的問(wèn)題、死鎖問(wèn)題,以及受限于硬件和軟件的資源限制問(wèn)題,本篇文章介紹幾種并發(fā)編程的挑戰(zhàn)及解決方案,文章總結(jié)至《Java并發(fā)編程的藝術(shù)》
一、上下文切換
即使是單核處理器也支持多線程執(zhí)行代碼,CPU通過(guò)給每個(gè)線程分配CPU時(shí)間片來(lái)實(shí)現(xiàn)這個(gè)機(jī)制。時(shí)間片是CPU分配給各個(gè)線程執(zhí)行的時(shí)間,因?yàn)闀r(shí)間片非常短,所有CPU通過(guò)不停的切換線程執(zhí)行,讓我們感覺(jué)多個(gè)線程是同時(shí)執(zhí)行的,時(shí)間片一般是幾十毫秒(ms)。
CPU通過(guò)時(shí)間片分配算法來(lái)循環(huán)執(zhí)行任務(wù),當(dāng)前任務(wù)執(zhí)行一個(gè)時(shí)間片后會(huì)切換到下一個(gè)任務(wù)。但是,在切換前會(huì)保存上一次任務(wù)的狀態(tài),以便于下次切換回這個(gè)任務(wù)時(shí),可以再加載這個(gè)任務(wù)的狀態(tài)。所以任務(wù)從保存到再加載的過(guò)程就是一次上下文切換。
這就像我們同事讀兩本書,當(dāng)我們?cè)谧x一本英文的技術(shù)書時(shí),發(fā)現(xiàn)某個(gè)單詞不認(rèn)識(shí),于是便打開中英文字典,但是在放下英文技術(shù)書之前,大腦必須先記住這本書讀到了多少頁(yè)的第多少行,等查完單詞之后,能夠繼續(xù)讀這本英文技術(shù)書。這樣的切換時(shí)會(huì)影響讀書效率的,同樣的道理上下文的切換也會(huì)影響多線程的執(zhí)行速度。
1.1 多線程一定快嗎
下面的代碼演示串行和并發(fā)執(zhí)行并累加操作的時(shí)間,分析并發(fā)執(zhí)行一定比串行執(zhí)行快么?
package com.lizba.p1; /** *
* 測(cè)試并發(fā)執(zhí)行和串行的速度 *
* * @Author: Liziba * @Date: 2021/6/2 23:40 */ public class ConcurrencyTest { /** 執(zhí)行次數(shù) */ private static final long count = 10000; public static void main(String[] args) throws InterruptedException { concurrency(); serial(); } /** * 并發(fā)執(zhí)行 * @throws InterruptedException */ private static void concurrency() throws InterruptedException { long start = System.currentTimeMillis(); Thread thread = new Thread(new Runnable() { public void run() { 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--; } thread.join(); long time = System.currentTimeMillis() - start; System.out.println("concurrency :" + time + "ms, b=" + b); } /** * 串行執(zhí)行 */ 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("serial :" + time + "ms, b=" + b); } }時(shí)間統(tǒng)計(jì)
循環(huán)次數(shù)
串行執(zhí)行耗時(shí)/ms
并發(fā)執(zhí)行耗時(shí)/ms
并發(fā)比串行快多少
1萬(wàn)
0
5
慢
10萬(wàn)
2
3
慢
100萬(wàn)
3
4
差不多
1000萬(wàn)
8
7
差不多
1億
54
54
差不多
10億
514
508
差不多
從上表可以看出,當(dāng)并發(fā)執(zhí)行累計(jì)操作低于百萬(wàn)次時(shí),速度會(huì)比串行執(zhí)行累加操作要慢。為什么在這種情況下并發(fā)執(zhí)行比串行執(zhí)行要慢呢?這是因?yàn)閯?chuàng)建線程和上下文切換的時(shí)間開銷要遠(yuǎn)遠(yuǎn)大于簡(jiǎn)單計(jì)算的時(shí)間開銷。
1.2 測(cè)試上下文切換次數(shù)和時(shí)長(zhǎng)
測(cè)試工具:
使用Lmbench3可以測(cè)量上下文切換的時(shí)長(zhǎng)
使用vmstat可以測(cè)量上下文切換的次數(shù)
vmstat參數(shù)的含義:
參數(shù)名
含義
r
表示運(yùn)行隊(duì)列(就是說(shuō)多少個(gè)進(jìn)程真的分配到CPU)
b
表示阻塞的進(jìn)程
swpd
虛擬內(nèi)存已使用的大小,如果大于0,表示你的機(jī)器物理內(nèi)存不足了,如果不是程序內(nèi)存泄露的原因,那么你該升級(jí)內(nèi)存了或者把耗內(nèi)存的任務(wù)遷移到其他機(jī)器。
free
空閑的物理內(nèi)存的大小
buff
Linux/Unix系統(tǒng)用來(lái)存儲(chǔ),目錄里面有什么內(nèi)容,權(quán)限等的緩存
cache
用來(lái)記憶我們打開的文件,給文件做緩沖
si
每秒從磁盤讀入虛擬內(nèi)存的大小,如果這個(gè)值大于0,表示物理內(nèi)存不夠用或者內(nèi)存泄露了,要查找耗內(nèi)存進(jìn)程解決掉
so
每秒虛擬內(nèi)存寫入磁盤的大小,如果這個(gè)值大于0,同上
bi
塊設(shè)備每秒接收的塊數(shù)量,這里的塊設(shè)備是指系統(tǒng)上所有的磁盤和其他塊設(shè)備,默認(rèn)塊大小是1024byte
bo
塊設(shè)備每秒發(fā)送的塊數(shù)量,例如我們讀取文件,bo就要大于0。bi和bo一般都要接近0,不然就是IO過(guò)于頻繁,需要調(diào)整
in
每秒CPU的中斷次數(shù),包括時(shí)間中斷
cs
每秒上下文切換次數(shù)
us
用戶CPU時(shí)間
sy
系統(tǒng)CPU時(shí)間,如果太高,表示系統(tǒng)調(diào)用時(shí)間長(zhǎng),例如IO操作頻繁
wt
等待IO CPU時(shí)間
# 每隔一秒采集數(shù)據(jù),一直采集,直到程序終止 vmstat 1
CS(Content Switch)表示上下文切換的次數(shù),從上面的可以看出上下文每秒鐘切換1000多次。
1.3 如何減少上下文切換
減少上下文切換的方法有無(wú)鎖并發(fā)編程、CAS算法、使用最少線程和使用協(xié)程。
無(wú)鎖并發(fā)編程。多線程競(jìng)爭(zhēng)鎖時(shí),會(huì)引起上下文切換,所以多線程處理數(shù)據(jù)時(shí),可以用一些辦法來(lái)避免使用鎖,如將數(shù)據(jù)的id按照Hash算法取模分段,不同的線程處理不同段的數(shù)據(jù)。
CAS算法。Java的Atomic包使用CAS算法來(lái)更新數(shù)據(jù),而不需要加鎖。
使用最少線程。避免創(chuàng)建不需要的線程,比如任務(wù)很少,但是創(chuàng)建了很多線程來(lái)處理,這樣會(huì)造成大量線程處于等待狀態(tài)。
協(xié)程。在單線程里實(shí)現(xiàn)多任務(wù)調(diào)度,并在單線程里維持多個(gè)任務(wù)見(jiàn)的切換。
1.4 減少上下文切換實(shí)戰(zhàn)
這個(gè)例子簡(jiǎn)單說(shuō)明如何來(lái)減少線程池中大量WAITING線程,來(lái)減少上下文切換次數(shù)。(本文在Windows環(huán)境dump測(cè)試)
寫一個(gè)模擬出現(xiàn)WAITING狀態(tài)的代碼:
package com.lizba.p1; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** *
* 線程池Dump測(cè)試 -- 代碼只是示例 *
* * @Author: Liziba * @Date: 2021/6/4 23:26 */ public class ThreadPoolDumpTest { public static void main(String[] args) { // 創(chuàng)建固定大小的線程池 ExecutorService fixedThreadPool = Executors.newFixedThreadPool(300); // 初始化線程池中的線程 for (int i = 0; i < 300; i++) { fixedThreadPool.execute(getThread(i)); } while (true) { try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("測(cè)試!"); } } /** * 創(chuàng)建線程 * @param i * @return */ private static Runnable getThread(final int i) { return new Runnable() { public void run() { try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(i); } }; } }用jstack命令dump線程信息,可以看當(dāng)前運(yùn)行的Java程序的pid,查看當(dāng)前進(jìn)程號(hào)里的線程在做什么。
# 查看Java進(jìn)程 jps
結(jié)果:
1216
12176 RemoteMavenServer36
18052 ThreadPoolDumpTest
18084 Launcher
15800 Jps
統(tǒng)計(jì)所有線程分別處于什么狀態(tài),找出處于(onobjectmonitor)阻塞狀態(tài)的線程。
# dump下快照 jstack -l 18052 > d:\dump.txt
打開dump文件查看處于(onobjectmonitor)阻塞的線程在做什么。
發(fā)現(xiàn)有300個(gè)線程處于WAITING狀態(tài)
"pool-1-thread-300" #311 prio=5 os_prio=0 tid=0x000000002fe46800 nid=0x4880 waiting on condition [0x0000000033cfe000] java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for <0x000000077b098178> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039) at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442) at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748) Locked ownable synchronizers: - None
此時(shí)如果發(fā)現(xiàn)是我們?cè)诔绦蛑卸x的線程池中的線程,則我們應(yīng)該適當(dāng)考慮降低線程池的maxThreads的值。
此處示例中我們修改線程池的固定大小為10:
// 創(chuàng)建固定大小的線程池 ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10);
修改maxThread值之后我們可以重啟項(xiàng)目。再次dump線程信息,然后重新統(tǒng)計(jì)(onobjectmonitor)阻塞的線程數(shù)。
再次dump快照分析線程運(yùn)行情況,發(fā)現(xiàn)只有10個(gè)線程處于WAITING狀態(tài)了:
"pool-1-thread-10" #21 prio=5 os_prio=0 tid=0x000000001ecde000 nid=0x312c waiting on condition [0x00000000212ef000] java.lang.Thread.State: WAITING (parking)
在上面的簡(jiǎn)單案例中WAITING線程減少了,系統(tǒng)上下文切換的次數(shù)就會(huì)減少,因?yàn)槊恳淮螐腤AITING到RUNNABLE都會(huì)進(jìn)行一次上下文的切換。在實(shí)際開發(fā)中,我們并不會(huì)做這么看似低級(jí)的操作,但是樣例卻能給我們代理線程池優(yōu)化和程序線程優(yōu)化各方面的解決問(wèn)題的思路。
二、死鎖
鎖是一個(gè)非常有用的工具,運(yùn)用的場(chǎng)景非常多,因?yàn)樗褂闷饋?lái)非常簡(jiǎn)單,而且易于理解。但同時(shí)它會(huì)帶來(lái)一些困擾,那就是可能會(huì)引起死鎖,一旦產(chǎn)生死鎖,就會(huì)造成系統(tǒng)功能不可用。
2.1 死鎖示例
下面演示一段引起死鎖的代碼,使得線程t1和線程t2互相等待對(duì)方釋放鎖。
package com.lizba.p1; /** *
* 死鎖示例代碼 *
* * @Author: Liziba * @Date: 2021/6/5 0:37 */ public class DeadLockDemo { private static final String A = "A"; private static final String B = "B"; /** * t1\t2互相持有鎖 */ private void deadLock() { Thread t1 = new Thread(new Runnable() { public void run() { // 持有鎖A synchronized (A) { try { Thread.currentThread().sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } // 持有鎖B synchronized (B) { System.out.println("hold Lock B"); } } } }); Thread t2 = new Thread(new Runnable() { public void run() { // 持有鎖B synchronized (B) { try { Thread.currentThread().sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } // 持有鎖A synchronized (A) { System.out.println("hold Lock A"); } } } }); t1.start(); t2.start(); } public static void main(String[] args) { new DeadLockDemo().deadLock(); } }這段代碼演示的是簡(jiǎn)單的死鎖場(chǎng)景,在現(xiàn)實(shí)中大家都不會(huì)寫出這樣的代碼。但是,在一些更為復(fù)雜的場(chǎng)景中,你可能會(huì)遇到這樣的問(wèn)題,比如t1拿到鎖之后,因?yàn)橐恍┊惓G闆r并沒(méi)有釋放鎖(比如死循環(huán))。又或者t1拿到一個(gè)數(shù)據(jù)庫(kù)鎖,釋放鎖的時(shí)候拋出了異常,沒(méi)有釋放掉。
現(xiàn)實(shí)中,一旦出現(xiàn)了死鎖,業(yè)務(wù)是可感知的,因?yàn)椴荒芾^續(xù)提供服務(wù)了,那么只能通過(guò)dump線程查看到底是哪個(gè)線程出現(xiàn)了問(wèn)題,我們分析如下Dump出的線程信息:
"Thread-1" #13 prio=5 os_prio=0 tid=0x000000001e011000 nid=0x5318 waiting for monitor entry [0x000000001fcef000] java.lang.Thread.State: BLOCKED (on object monitor) at com.lizba.p1.DeadLockDemo$2.run(DeadLockDemo.java:50) - waiting to lock <0x000000076b042000> (a java.lang.String) - locked <0x000000076b042030> (a java.lang.String) at java.lang.Thread.run(Thread.java:748) Locked ownable synchronizers: - None "Thread-0" #12 prio=5 os_prio=0 tid=0x000000001e00f800 nid=0x4b38 waiting for monitor entry [0x000000001fbef000] java.lang.Thread.State: BLOCKED (on object monitor) at com.lizba.p1.DeadLockDemo$1.run(DeadLockDemo.java:33) - waiting to lock <0x000000076b042030> (a java.lang.String) - locked <0x000000076b042000> (a java.lang.String) at java.lang.Thread.run(Thread.java:748) Locked ownable synchronizers: - None
從上可以看出第33行和第50行引發(fā)了死鎖。
2.2 避免產(chǎn)生死鎖
避免一個(gè)線程同時(shí)獲取多個(gè)鎖。
避免一個(gè)線程在鎖內(nèi)同時(shí)占用多個(gè)資源,盡量保證每個(gè)鎖只占用一個(gè)資源。
嘗試使用定時(shí)鎖,使用lock.tryLock(timeout)來(lái)替代使用內(nèi)部鎖機(jī)制。
對(duì)于數(shù)據(jù)庫(kù)鎖,加鎖和解鎖必須在一個(gè)數(shù)據(jù)庫(kù)連接里,否則會(huì)出現(xiàn)解鎖失敗的情況。
三、資源限制
3.1 什么是資源限制
資源限制是指在進(jìn)行并發(fā)編程時(shí),程序的執(zhí)行速度受限于計(jì)算機(jī)硬件資源或者軟件資源。例如,服務(wù)器的帶寬只有2MB/s,某個(gè)資源的下載速度是1MB/s,系統(tǒng)啟動(dòng)10個(gè)線程下載資源,下載速度不會(huì)變成10MB/s,所以在并發(fā)編程時(shí),要考慮這些資源的限制。
硬件資源限制有帶寬的上傳/下載速度、硬盤讀寫速度和CPU處理速度。
軟件資源的限制有數(shù)據(jù)庫(kù)的連接和socket連接數(shù)等。
3.2 資源限制引發(fā)的問(wèn)題
在并發(fā)編程中,將代碼執(zhí)行速度加快的原則是將代碼中串行執(zhí)行的部分變成并發(fā)執(zhí)行,但是如果將某段串行的代碼并發(fā)執(zhí)行,因?yàn)槭芟抻谫Y源,仍然在串行執(zhí)行,這樣程序不僅不會(huì)加快,反而會(huì)更慢,因?yàn)樵黾由舷挛那袚Q和資源調(diào)度的時(shí)間。
3.3 如何解決資源限制的問(wèn)題
對(duì)于硬件資源的限制,可以考慮使用集群并行執(zhí)行程序
對(duì)應(yīng)軟件資源的限制,可以考慮使用資源池將資源復(fù)用
3.4 在資源限制情況下并發(fā)編程
如何在資源限制的情況下,讓程序執(zhí)行的更加快呢?方法就是,根據(jù)不同的資源限制調(diào)整程序的并發(fā)度,比如下載文件程序依賴于兩個(gè)資源-寬帶和硬盤的讀寫速度。有數(shù)據(jù)庫(kù)操作時(shí),涉及數(shù)據(jù)庫(kù)連接,如果SQL執(zhí)行非常快,而線程的數(shù)量比數(shù)據(jù)量連接數(shù)大很多,則某些線程會(huì)被阻塞,等待數(shù)據(jù)庫(kù)連接。
本文總結(jié)至 -- 《Java并發(fā)編程的藝術(shù)》/《The Art of Java Concurrency Programming》
Java
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實(shí)的內(nèi)容,請(qǐng)聯(lián)系我們jiasou666@gmail.com 處理,核實(shí)后本網(wǎng)站將在24小時(shí)內(nèi)刪除侵權(quán)內(nèi)容。
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實(shí)的內(nèi)容,請(qǐng)聯(lián)系我們jiasou666@gmail.com 處理,核實(shí)后本網(wǎng)站將在24小時(shí)內(nèi)刪除侵權(quán)內(nèi)容。