什么java并發(fā)編程

      網(wǎng)友投稿 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)題的思路。

      二、死鎖

      什么是java并發(fā)編程

      鎖是一個(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)容。

      上一篇:Word不顯示網(wǎng)格線怎么設(shè)置(Word怎么設(shè)置網(wǎng)格線)
      下一篇:excel雙縱坐標(biāo)怎么做?(excel怎么畫雙縱坐標(biāo))
      相關(guān)文章
      亚洲AV无码久久精品色欲| 亚洲综合中文字幕无线码| 亚洲妇女无套内射精| 亚洲AV综合永久无码精品天堂| 亚洲av永久无码精品网站| 亚洲国产精品一区二区成人片国内 | 久久精品国产亚洲AV未满十八 | va天堂va亚洲va影视中文字幕| 久久精品国产亚洲夜色AV网站| 亚洲综合日韩久久成人AV| 亚洲国模精品一区| 国产啪亚洲国产精品无码| 亚洲男人第一无码aⅴ网站| 亚洲精品国产精品乱码不卞 | 国产亚洲婷婷香蕉久久精品| 国产亚洲精久久久久久无码| 亚洲国产精品VA在线观看麻豆| 亚洲精品无码MV在线观看| 一本色道久久综合亚洲精品高清| 亚洲真人无码永久在线 | 国产成人亚洲合集青青草原精品 | 亚洲午夜精品一级在线播放放| 国产亚洲美女精品久久久| 国产午夜亚洲不卡| 国产偷国产偷亚洲清高动态图| 亚洲精品国产精品乱码不卞| 亚洲av高清在线观看一区二区| 亚洲日韩中文字幕日韩在线| 亚洲一区爱区精品无码| 亚洲成av人影院| 亚洲视频一区在线观看| 亚洲91精品麻豆国产系列在线| 99久久国产亚洲综合精品| 含羞草国产亚洲精品岁国产精品| 一级毛片直播亚洲| 亚洲中文字幕久久精品无码APP | 亚洲色成人网站WWW永久四虎| 大桥未久亚洲无av码在线| 亚洲精品A在线观看| 亚洲国产成人片在线观看无码| 亚洲色大成网站www永久|