java高併發基礎概念

第一章

1.2基礎概念

1.2.1 同步和異步

1. 同步:同步方法調用一旦開始,調用者必須等到方法調用返回後,才能繼續後續的行爲。
2. 異步:異步方法調用更像一個消息傳遞,一旦開始,方法調用就會立即返回,調用者就可以繼續後續的操作,被調用的方法則會在另一個線程中執行。

###1.2.2 併發和並行###
1. 併發:偏重於多個任務交替執行,而多個任務之間可能是串行的。
2. 並行:從嚴格意義上來說,並行的多個任務是真的同時執行,而對於併發來說,這個過程是交替的,一會兒執行任務A,一會兒執行任務B,系統會不停地在兩者間切換s。

1.2.3 臨界區

1. 臨界區:用來表示一種公共資源或者說共享數據,可以被多個線程使用,但是每次只能被一個線程使用,一旦臨界區資源被佔用,其他線程想要使用這個資源就必須等待。在並行程序中,臨界區資源是保護對象.

1.2.4 阻塞(Blocking)和非阻塞(Non-Blocking)

1. 阻塞和非阻塞通常用來形容多線程間的相互影響。比如已有一個線程佔用了臨界區資源,那個其他需要這個資源的線程們都必須在這個臨界區中等待,等待會導致線程掛起,這種情況就是阻塞。非阻塞的意思與之相反,他強調沒有一個線程可以妨礙其他線程執行,所有線程都會嘗試不斷前向執行。

1.2.5 死鎖(Deadlock)、飢餓(Starvation)和活鎖(Livelock

1. 死鎖、飢餓和活鎖多屬於多線程的活躍性問題。如果發現上述幾種情況,那麼相關線程可能就不再活躍,也就是說他可能很難再繼續往下執行。
2. 死鎖:是最糟糕的一種情況(其他幾種情況也好不到哪裏去),如果大家都不願意釋放自己的資源,那麼這個狀態將會永遠持續下去,誰都不能通過。死鎖是一個很嚴重的並且應該避免和時時小心的問題。
3. 飢餓是指某一個或者多個線程因爲種種原因無法獲得所需要的資源,導致一直無法執行。例如一個線程的優先級太低,而高優先級的線程不斷搶佔他需要的資源,導致低優先級線程無法工作;此外,某一個線程一直佔着關鍵資源不放,導致其他需要這個資源的線程無法正常執行,這也是飢餓的一種情況。
4. 活鎖:發生在兩個線程都主動將資源釋放給他人使用,那麼就會導致資源不斷地在兩個線程間跳動,而沒有一個線程可以同時拿到所有資源正常執行,這種情況就是活鎖。

1.3 併發級別

我們將併發級別分爲:阻塞、無飢餓、無障礙、無鎖、無等待幾種。

1.3.1 阻塞

1. 一個線程是阻塞的,那麼在其它線程釋放資源前,當前線程無法繼續執行。當我們使用synchronized關鍵字或重入鎖時,我們得到的就是阻塞的線程,因爲synchronized關鍵字和重入鎖都試圖在執行後續代碼前,得到臨界區的鎖,如果的不到,線程就會被掛起等待,直到佔有了所需資源位置。

1.3.2 無飢餓(Starvation-Free)

1. 如果鎖是公平的,按照先來後到的規則,那麼飢餓就不會產生,不管新來的線程優先級多高,要想獲得資源,就必須乖乖排隊,這樣所有的線程都有機會執行。

1.3.3 無障礙(Obstruction-Free)

1. 無障礙是一種最弱的非阻塞調度。兩個線程如果無障礙地執行,那麼不會因爲臨界區的問題導致一方被掛起來。換言之,大家都可以大搖大擺地進入臨界區。那麼大家一起修改共享數據,把數據改壞了怎麼辦呢?對於無障礙的線程來說,一旦檢測到這種情況,他就會立即對自己所做的修改進行回滾,確保數據安全。但如果沒有數據競爭發生,那麼線程就可以順利完成自己的工作,做出臨界區。
2. 如果說阻塞的控制方式是悲觀策略,也就是說,系統認爲兩個線程之間很有可能發生不幸的衝突,因此以保護共享數據爲第一優先級,相對來說,非阻塞的調度就是一種樂觀的策略。他認爲多個線程間很有可能不會發生衝突,或者來說這種概率不大。因此大家都應該無障礙地執行,但是一旦檢測到衝突,就應該進行回滾。
3. 一種可行的無障礙實現可以依賴一個“一致性標記”來實現。線程在操作前,先讀取並保存這個標記,在操作完成後,再次讀取,檢查這個標記是否被更改過,如果兩者是一致的,則說明資源訪問沒有衝突。如果不一致,則說明資源可能在操作過程中與其他寫線程衝突,需要重試操作。而任何對資源有修改操作的線程,在修改數據前,都需要更新這個一致性標記,表示數據不在安全。

1.3.4 無鎖(Lock-Free)

1. 無鎖的並行都是無障礙的。在無鎖的情況下,所有的線程都能嘗試對臨界區進行訪問,但不同的是,無鎖的併發保證必然有一個線程能夠在有限步內完成操作離開臨界區。

1.3.5 無等待(Wait-Free)

1. 無鎖只要求有一個線程可以在有限步內完成操作,而無等待則會在無鎖的基礎上更進一步擴展。他要求所有的縣城都必須在有限步內完成,這樣就不會引起飢餓問題。如果限制這個步驟的上限,還可以進一步分解爲有界無等待和線程數無關的無等待等幾種,他們之間的區別是隻是對循環次數的限制不同。
2. 一種典型的無等待結構就是RCU(Read Copy Update).它的基本思想是,對數據的讀可以不加控制。因此,所有的讀線程都是無等待的,它們既不會被鎖定等待也不會引起任何衝突。但在寫數據的時候,先取得原始數據的副本,接着只修改副本數據(這就是爲什麼讀可以不加控制),修改完成後,在合適的時機回寫數據。

1.4 有關並行的兩個重要定律

編寫多線程程序的目的,總的來說分爲兩個:
	1. 爲了獲得更好的性能
	2. 由於業務模型的需要
我們更關注與第一個目的,針對於這一問題有兩個定律進行解答,一個是Amdahl定律,另一個是Gustafson定律.

1.4.1 Amdahl定律

1. 該定律證明了優化的效果取決於cpu的數量,以及系統中的串行化程序的比例。表明即使cpu的數量趨近於無窮也不能無限提升程序的執行效率。

1.4.2 Gustafson定律

1. 百度

1.4.3 是否相互矛盾

1. 兩個定律是從兩個不同角度來看待問題。

1.5 回到java: JMM

1. JMM(Java內存模型):JMM關鍵技術點都是圍繞着多線程的原子性、可見性和有序性來建立的。因此,我們必須瞭解這些概念

1.5.1 原子性(Atomicity)

1. 原子性是指一個操作是不可中斷的。即使是在多個線程一起執行的時候,一個操作一旦開始,就不會被其他線程干擾。

1.5.2 可見性(Visibility)

1. 可見性是指當一個線程修改了某一個共享變量的值時,其它線程是否能夠立即知道這個修改。

1.5.3 有序性(Orderding)

1. 有序性問題的原因是程序在執行時,可能會進行指令重排,重排後的指令與原指令的順序未必一致。

第二章 java並行程序基礎

前一章探討了爲什麼必須面對並行程序這樣複雜的程序設計方法,接下來就要研究如何才能構建一個正確、健壯並且高效的並行程序。本章將詳細介紹有關Java並行程序的設計基礎,以及一些常見的問題。

2.1 有關線程你必須知道的事

1. 在當代面向線程設計的計算機結構中,進程是線程的容器,程序是指令、數據及其組織形式的描述,進程是程序實體,這解釋了進程、線程、程序三者間的關係。
2. 線程是輕量級進程,是程序執行的最小單位。使用多線程而不是多進程去進行併發程序的設計,是因爲線程間的切換和調度的成本遠遠小於進程。
3. 將線程所有狀態可分爲如下:
	1. NEW
	2. RUNNABLE
	3. BLOCKED
	4. WARITING
	5. TIMED_WAITING
	6. TERMINATED
4. NEW表示剛剛創建的線程,這種線程還沒開始執行。等線程的start()方法調用時,才表示線程開始執行。當線程執行時,處於RUNNABLE狀態,表示線程所需的一切資源都已經準備好了。如果線程在執行過程中遇到了synchronized同步塊,就會進行BLOCKED阻塞狀態,這是線程就會暫停執行,直到獲得請求的鎖。WAITING和TIMED_WAITING都表示等待狀態,他們的區別是WAITING會進入一個無時間限制的等待,TIMED_WAITING會進行一個有時限的等待。那麼等待的線程究竟在等什麼?一般來說,WAITING的線程正在等待一些特殊的事件。比如,通過wait()方法等待的線程在等待notify()方法,而通過join()方法等待的線程則會等待線程則會等待目標線程的終止。一旦等到了期望事件,縣城就會再次執行,進入RUNNABLE狀態。當線程執行完畢後,則進入TERMINATED狀態,表示結束。
5. 注意:從NEW狀態出發後,縣城不能再回到NEW狀態,同理,處於TERMINATED狀態的線程也不能再回到RUNNABLE狀態。 

2.2 初始線程: 線程的基本操作

除了瞭解java中衛線程操作所提供的一些API,由於多線程本身的複雜性,導致這些API有着比較隱晦的“坑”,會盡可能將潛在的問題描述清楚。

2.2.1 新建線程

1. 新建線程很簡單。只要使用new關鍵字創建一個線程對象,並且將它start()起來即可。
	Thread t1 = new Thread();
	t1.start();
2. 線程start()後,會幹什麼呢?這纔是問題的關鍵。線程Thread,有一個run()方法,執行start()方法就會新建一個線程並讓這個線程執行run()方法。
3. 注意下面的代碼,下面的代碼通過編譯,也能正常執行。但是,卻不能新建一個線程,而是在當前線程中調用run()方法,只是作爲一個普通方法調用。
	Thread t1 = new Thread();
	t1.run();
4. 這裏是爲了提醒大家,調用start()方法和直接調用run()方法的區別。
5. 注意:不要用run()方法來開啓新線程。他只會在當前線程中串行執行run()方法中的代碼。
6. 在默認情況下,即使使用start()開啓線程,由於run()方法什麼都沒有做,因此,這個線程已啓動就馬上結束了。如果你想讓線程做點什麼,就必須重寫run()方法,把你的“任務放進去”。
	Thread t1 = new Thread(){
		@Override
		public void run(){
			System.out.println("Hello I am t1");
		}
	};
	t1.start();
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章