詳解併發編程的優缺點

一直以來併發編程對於剛入行的小白來講老是以爲高深莫測,因而乎,就誕生了想寫點東西記錄下,以提高理解和堆併發編程的認知。爲何須要用的併發?凡事總有好壞兩面,之間的trade-off是什麼,也就是說併發編程具備哪些缺點?以及在進行併發編程時應該瞭解和掌握的概念是什麼?這篇文章主要以這三個問題來談一談。java

一.爲何要用到併發


一直以來,硬件的發展極其迅速,也有一個很著名的"摩爾定律",可能會奇怪明明討論的是併發編程爲何會扯到了硬件的發展,這其中的關係應該是多核CPU的發展爲併發編程提供的硬件基礎。摩爾定律並非一種天然法則或者是物理定律,它只是基於認爲觀測數據後,對將來的一種預測。按照所預測的速度,咱們的計算能力會按照指數級別的速度增加,不久之後會擁有超強的計算能力,正是在暢想將來的時候,2004年,Intel宣佈4GHz芯片的計劃推遲到2005年,而後在2004年秋季,Intel宣佈完全取消4GHz的計劃,也就是說摩爾定律的有效性超過了半個世紀戛然而止。可是,聰明的硬件工程師並無中止研發的腳步,他們爲了進一步提高計算速度,而不是再追求單獨的計算單元,而是將多個計算單元整合到了一塊兒,也就是造成了多核CPU。短短十幾年的時間,家用型CPU,好比Intel i7就能夠達到4核心甚至8核心。而專業服務器則一般能夠達到幾個獨立的CPU,每個CPU甚至擁有多達8個以上的內核。所以,摩爾定律彷佛在CPU核心擴展上繼續獲得體驗。所以,多核的CPU的背景下,催生了併發編程的趨勢,經過併發編程的形式能夠將多核CPU的計算能力發揮到極致,性能獲得提高程序員

頂級計算機科學家Donald Ervin Knuth如此評價這種狀況:在我看來,這種現象(併發)或多或少是因爲硬件設計者機關用盡了致使的,他們將摩爾定律的責任推給了軟件開發者。面試

另外,在特殊的業務場景下先天的就適合於併發編程。好比在圖像處理領域,一張1024X768像素的圖片,包含達到78萬6千多個像素。即時將全部的像素遍歷一邊都須要很長的時間,面對如此複雜的計算量就須要充分利用多核的計算的能力。又好比當咱們在網上購物時,爲了提高響應速度,須要拆分,減庫存,生成訂單等等這些操做,就能夠進行拆分利用多線程的技術完成。面對複雜業務模型,並行程序會比串行程序更適應業務需求,而併發編程更能吻合這種業務拆分 。正是由於這些優勢,使得多線程技術可以獲得重視,也是一名CS學習者應該掌握的:算法

  • 充分利用多核CPU的計算能力;
  • 方便進行業務拆分,提高應用性能

二. 併發編程有哪些缺點


多線程技術有這麼多的好處,難道就沒有一點缺點麼,就在任何場景下就必定適用麼?很顯然不是。sql

2.1 頻繁的上下文切換

時間片是CPU分配給各個線程的時間,由於時間很是短,因此CPU不斷經過切換線程,讓咱們以爲多個線程是同時執行的,時間片通常是幾十毫秒。而每次切換時,須要保存當前的狀態起來,以便可以進行恢復先前狀態,而這個切換時很是損耗性能,過於頻繁反而沒法發揮出多線程編程的優點。一般減小上下文切換能夠採用無鎖併發編程,CAS算法,使用最少的線程和使用協程。數據庫

  • 無鎖併發編程:能夠參照concurrentHashMap鎖分段的思想,不一樣的線程處理不一樣段的數據,這樣在多線程競爭的條件下,能夠減小上下文切換的時間。編程

  • CAS算法:利用Atomic下使用CAS算法來更新數據,使用了樂觀鎖,能夠有效的減小一部分沒必要要的鎖競爭帶來的上下文切換安全

  • 使用最少線程:避免建立不須要的線程,好比任務不多,可是建立了不少的線程,這樣會形成大量的線程都處於等待狀態bash

  • 協程:在單線程裏實現多任務的調度,並在單線程裏維持多個任務間的切換服務器

因爲上下文切換也是個相對比較耗時的操做,因此在"java併發編程的藝術"一書中有過一個實驗,併發累加未必會比串行累加速度要快。 可使用Lmbench3測量上下文切換的時長 vmstat測量上下文切換次數

2.2 線程安全

多線程編程中最難以把握的就是臨界區線程安全問題,稍微不注意就會出現死鎖的狀況,一旦產生死鎖就會形成系統功能不可用。

public class DeadLockDemo {
	private static String resource_a = "A";
	private static String resource_b = "B";
	public static void main(String[] args) {
		deadLock();
	}
	public static void deadLock() {
		Thread threadA = new Thread(new Runnable() {
			@Override
			            public void run() {
				synchronized (resource_a) {
					System.out.println("get resource a");
					try {
						Thread.sleep(3000);
						synchronized (resource_b) {
							System.out.println("get resource b");
						}
					}
					catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		}
		);
		Thread threadB = new Thread(new Runnable() {
			@Override
			            public void run() {
				synchronized (resource_b) {
					System.out.println("get resource b");
					synchronized (resource_a) {
						System.out.println("get resource a");
					}
				}
			}
		}
		);
		threadA.start();
		threadB.start();
	}
}
複製代碼

在上面的這個demo中,開啓了兩個線程threadA, threadB,其中threadA佔用了resource_a, 並等待被threadB釋放的resource _b。threadB佔用了resource _b正在等待被threadA釋放的resource _a。所以threadA,threadB出現線程安全的問題,造成死鎖。一樣能夠經過jps,jstack證實這種推論:

"Thread-1":
  waiting to lock monitor 0x000000000b695360 (object 0x00000007d5ff53a8, a java.lang.String),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x000000000b697c10 (object 0x00000007d5ff53d8, a java.lang.String),
  which is held by "Thread-1"
Java stack information for the threads listed above:
===================================================
"Thread-1":
        at learn.DeadLockDemo$2.run(DeadLockDemo.java:34)
        - waiting to lock <0x00000007d5ff53a8(a java.lang.String)
        - locked <0x00000007d5ff53d8(a java.lang.String)
        at java.lang.Thread.run(Thread.java:722)
"Thread-0":
        at learn.DeadLockDemo$1.run(DeadLockDemo.java:20)
        - waiting to lock <0x00000007d5ff53d8(a java.lang.String)
        - locked <0x00000007d5ff53a8(a java.lang.String)
        at java.lang.Thread.run(Thread.java:722)
Found 1 deadlock.
複製代碼

如上所述,徹底能夠看出當前死鎖的狀況。

那麼,一般能夠用以下方式避免死鎖的狀況:

  • 避免一個線程同時得到多個鎖;
  • 避免一個線程在鎖內部佔有多個資源,儘可能保證每一個鎖只佔用一個資源;
  • 嘗試使用定時鎖,使用lock.tryLock(timeOut),當超時等待時當前線程不會阻塞;
  • 對於數據庫鎖,加鎖和解鎖必須在一個數據庫鏈接裏,不然會出現解鎖失敗的狀況

因此,如何正確的使用多線程編程技術有很大的學問,好比如何保證線程安全,如何正確理解因爲JMM內存模型在原子性,有序性,可見性帶來的問題,好比數據髒讀,DCL等這些問題(在後續篇幅會講述)。而在學習多線程編程技術的過程當中也會讓你收穫頗豐。

三. 應該瞭解的概念


3.1 同步VS異步

同步和異步一般用來形容一次方法調用。同步方法調用一開始,調用者必須等待被調用的方法結束後,調用者後面的代碼才能執行。而異步調用,指的是,調用者不用管被調用方法是否完成,都會繼續執行後面的代碼,當被調用的方法完成後會通知調用者。好比,在超時購物,若是一件物品沒了,你得等倉庫人員跟你調貨,直到倉庫人員跟你把貨物送過來,你才能繼續去收銀臺付款,這就相似同步調用。而異步調用了,就像網購,你在網上付款下單後,什麼事就不用管了,該幹嗎就幹嗎去了,當貨物到達後你收到通知去取就好。

3.2 併發與並行

併發和並行是十分容易混淆的概念。併發指的是多個任務交替進行,而並行則是指真正意義上的「同時進行」。實際上,若是系統內只有一個CPU,而使用多線程時,那麼真實系統環境下不能並行,只能經過切換時間片的方式交替進行,而成爲併發執行任務。真正的並行也只能出如今擁有多個CPU的系統中。

3.3 阻塞和非阻塞

阻塞和非阻塞一般用來形容多線程間的相互影響,好比一個線程佔有了臨界區資源,那麼其餘線程須要這個資源就必須進行等待該資源的釋放,會致使等待的線程掛起,這種狀況就是阻塞,而非阻塞就剛好相反,它強調沒有一個線程能夠阻塞其餘線程,全部的線程都會嘗試地往前運行。

3.4 臨界區

臨界區用來表示一種公共資源或者說是共享數據,能夠被多個線程使用。可是每一個線程使用時,一旦臨界區資源被一個線程佔有,那麼其餘線程必須等待。

讀者福利

分享免費學習資料

針對於Java程序員,我這邊準備免費的Java架構學習資料(裏面有高可用、高併發、高性能及分佈式、Jvm性能調優、MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)

爲何某些人會一直比你優秀,是由於他自己就很優秀還一直在持續努力變得更優秀,而你是否是還在知足於現狀心裏在竊喜!但願讀到這的您能點個小贊和關注下我,之後還會更新技術乾貨,謝謝您的支持!

資料領取方式:加入Java技術交流羣963944895點擊加入羣聊,私信管理員便可免費領取

如何成爲一個有逼格的Java架構師

怎麼提升代碼質量?——來自阿里P8架構師的研發經驗總結

阿里P8分享Java架構師的學習路線,第六點尤其重要

每一個Java開發者應該知道的八個工具

想面試Java架構師?這些最基本的東西你都會了嗎?

畫個圖來找你的核心競爭力,變中年危機爲加油站

哪有什麼中年危機,不過是把定目標當成了有計劃

被裁人不是寒冬重點,重點是怎麼破解職業瓶頸

相關文章
相關標籤/搜索