Java中的多線程一定就快嗎?
并發(fā)編程與多線程編程
要了解并發(fā)編程,首先要懂得與并行這個概念進行區(qū)分。并行是指兩個事件同時進行,并發(fā)是CPU切換速度快,看起來像是每個任務同時進行一樣。多線程是實現(xiàn)并發(fā)編程的一種方式,假設一個場景,在廣州地鐵高峰時段,一群人涌進地鐵里,在不同的閘機口刷卡進去。在這個場景里,進地鐵就是任務,每個人可以看出是并發(fā)的,而多個刷卡閘機口就是多線程。
并發(fā)編程的本質目的是為了充分利用CPU,讓程序運行得更快。然而,并不是啟動更多的線程就能讓程序最大限度地并發(fā)執(zhí)行。在進行并發(fā)編程時,如果希望通過多線程執(zhí)行任務讓程序運行得更快,會面臨非常多的挑戰(zhàn)。比如上下文切換的問題、死鎖的問題,以及受限于硬件和軟件的資源限制問題,下面就來嘮嗑嘮嗑這些因素。
上下文切換
原理分析
正如上面所言,并發(fā)與并行最大的區(qū)別就是,并發(fā)只是看起來像是并行。實際上是,CPU通過給每個線程分配時間來執(zhí)行這個線程的程序,只是這個時間非常短,通常是幾十毫秒,我們根本無法觀察到變化,感覺它們都是同時執(zhí)行的一樣。
CPU通過時間片分配算法來循環(huán)執(zhí)行任務,當前任務執(zhí)行一個時間片后會切換到下一個任務。但是,在切換前會保存上一個任務的狀態(tài),以便下次切換回這個任務時,可以再加載這個任務的狀態(tài)。所以任務從保存到再加載的過程就是一次上下文切換。因此,不難得知,上下文切換需要耗費不少時間。
再來假設一個場景,一個人去火車站買票,買票的窗口有十來個那么多。買票的人并不知道哪個窗口可以買到票,只能挨個地問,最后終于在最后一個窗口買到了。這個場景,看似買票的過程很長,其實大部分時間都在切換窗口上,這也就是上下文切換的問題所在。因此,并非線程數(shù)多就一定執(zhí)行得快,要選擇與任務相適應的線程數(shù)才是最佳方案。
測試代碼
package Concurrency; /** * @author RuiMing Lin * @date 2020-03-28 12:19 */ public class Demo1 { public static void main(String[] args) { System.out.println("萬級循環(huán):"); concurrency(10000); serial(10000); System.out.println("--------------------------華麗分隔符--------------------------------"); System.out.println("十萬級循環(huán):"); concurrency(100000); serial(100000); System.out.println("--------------------------華麗分隔符--------------------------------"); System.out.println("百萬級循環(huán):"); concurrency(1000000); serial(1000000); System.out.println("--------------------------華麗分隔符--------------------------------"); System.out.println("千萬級循環(huán):"); concurrency(10000000); serial(10000000); System.out.println("--------------------------華麗分隔符--------------------------------"); System.out.println("億級循環(huán):"); concurrency(100000000); serial(100000000); } private static void concurrency(long count){ // 開啟三個線程執(zhí)行三個循環(huán) long start = System.currentTimeMillis(); new Thread(new Runnable() { @Override public void run() { int a = 0; for (long i = 0; i < count; i++) { a++; } } }).start(); new Thread(new Runnable() { @Override public void run() { int b = 0; for (long i = 0; i < count; i++) { b++; } } }).start(); new Thread(new Runnable() { @Override public void run() { int c = 0; for (long i = 0; i < count; i++) { c++; } } }).start(); long end = System.currentTimeMillis(); long time = end - start; System.out.println("并行執(zhí)行花費時間為:" + time + "ms"); } private static void serial(long count){ // 三個循環(huán)順序執(zhí)行 long start = System.currentTimeMillis(); int a = 0; int b = 0; int c = 0; for (int i = 0; i < count; i++) { a++; } for (int i = 0; i < count; i++) { b++; } for (int i = 0; i < count; i++) { c++; } long end = System.currentTimeMillis(); long time = end - start; System.out.println("串行執(zhí)行花費時間為:" + time + "ms"); } }
結果輸出:
萬級循環(huán): 并行執(zhí)行花費時間為:4ms 串行執(zhí)行花費時間為:1ms
--------------------------華麗分隔符--------------------------------
十萬級循環(huán): 并行執(zhí)行花費時間為:1ms 串行執(zhí)行花費時間為:4ms
--------------------------華麗分隔符--------------------------------
百萬級循環(huán): 并行執(zhí)行花費時間為:1ms 串行執(zhí)行花費時間為:10ms
--------------------------華麗分隔符--------------------------------
千萬級循環(huán): 并行執(zhí)行花費時間為:1ms 串行執(zhí)行花費時間為:36ms
--------------------------華麗分隔符--------------------------------
億級循環(huán): 并行執(zhí)行花費時間為:1ms 串行執(zhí)行花費時間為:357ms
分析結果:
當數(shù)量級在萬級時,串行是比并發(fā)要快的,當數(shù)量級來到十萬以后,串行便顯得力不從心了。所以,可以認為當程序執(zhí)行量不夠大時,是沒必要開啟多線程的。
如何減少上下文切換
減少上下文切換的方法有無鎖并發(fā)編程、CAS算法、使用最少線程和使用協(xié)程。
- 無鎖并發(fā)編程。多線程競爭鎖時,會引起上下文切換,所以多線程處理數(shù)據時,可以用一些辦法來避免使用鎖,如將數(shù)據的ID按照Hash算法取模分段,不同的線程處理不同段的數(shù)據。
- CAS算法。Java的Atomic包使用CAS算法來更新數(shù)據,而不需要加鎖。
- 使用最少線程。避免創(chuàng)建不需要的線程,比如任務很少,但是創(chuàng)建了很多線程來處理,這樣會造成大量線程都處于等待狀態(tài)。
- 協(xié)程:在單線程里實現(xiàn)多任務的調度,并在單線程里維持多個任務間的切換。
死鎖
原理分析
死鎖,是指多個線程在運行過程中因爭奪相同資源而造成的一種僵局,當進程處于這種僵持狀態(tài)時,它們都將無法再向前推進,此時程序就處于癱瘓狀態(tài),無法執(zhí)行。 通常情況下,是多個線程共同競爭同一把鎖對象,而其中一個線程獲得鎖之后發(fā)生異常等未來得及釋放鎖,導致其它線程一直在等待,無法運行。
測試代碼
package Concurrency; /** * @author RuiMing Lin * @date 2020-03-28 13:14 */ public class Demo2 { private static String str1 = "A"; private static String str2 = "B"; public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { synchronized (str1){ System.out.println("第一個線程獲得str1"); try { Thread.currentThread().sleep(2000); }catch (InterruptedException e){ e.printStackTrace(); } synchronized (str2){ System.out.println("第一個線程獲得str2"); } } } }).start(); new Thread(new Runnable() { @Override public void run() { synchronized (str2){ System.out.println("第二個線程獲得str2"); try { Thread.currentThread().sleep(2000); }catch (InterruptedException e){ e.printStackTrace(); } synchronized (str1){ System.out.println("第二個線程獲得str1"); } } } }).start(); } }
結果輸出:
如何解決死鎖
- 避免一個線程同時獲取多個鎖。
- 避免一個線程在鎖內同時占用多個資源,盡量保證每個鎖只占用一個資源。
- 嘗試使用定時鎖,使用lock.tryLock(timeout)來替代使用內部鎖機制。
- 對于數(shù)據庫鎖,加鎖和解鎖必須在一個數(shù)據庫連接里,否則會出現(xiàn)解鎖失敗的情況。
總結
并發(fā)程序并不是簡單的程序,編寫的時候應該嚴謹一些。復雜的代碼容易引起死鎖,因此,建議多使用JDK并發(fā)包提供的并發(fā)容器和工具類來解決并發(fā)問題。同時,也要注重新能上的問題,既要考慮到程序執(zhí)行任務量,也要考慮CPU性能等等,不要一昧地增加線程數(shù)。
以上就是Java中的多線程一定就快嗎?的詳細內容,更多關于Java 多線程的資料請關注腳本之家其它相關文章!
相關文章
Java語言實現(xiàn)簡單FTP軟件 輔助功能模塊FTP站點管理實現(xiàn)(12)
這篇文章主要為大家詳細介紹了Java語言實現(xiàn)簡單FTP軟,輔助功能模塊FTP站點管理的實現(xiàn)方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-04-04Spring中數(shù)據訪問對象Data Access Object的介紹
今天小編就為大家分享一篇關于Spring中數(shù)據訪問對象Data Access Object的介紹,小編覺得內容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2019-01-01mybatis-plus enum實現(xiàn)枚舉類型自動轉換
本文主要介紹了mybatis-plus enum實現(xiàn)枚舉類型自動轉換,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2024-07-07