《LINUX實(shí)操:《Java并發(fā)編程的藝術(shù)》讀書筆記》要點(diǎn):
本文介紹了LINUX實(shí)操:《Java并發(fā)編程的藝術(shù)》讀書筆記,希望對您有用。如果有疑問,可以聯(lián)系我們。
最近在進(jìn)一步學(xué)習(xí)Java并發(fā)編程,不問可知,這部分內(nèi)容是很重要的.現(xiàn)在就以《并發(fā)編程的藝術(shù)》一書為主導(dǎo)線,開始新一輪的學(xué)習(xí).
Java 并發(fā)編程的藝術(shù)PDF清楚完整版+源碼?
進(jìn)程是一個(gè)應(yīng)用程序在處理機(jī)上的一次執(zhí)行過程,線程是進(jìn)程的最小基本單位(個(gè)人理解).一個(gè)進(jìn)程可以包括多個(gè)線程.
我們都知道,即使是單核處理器也支持多線程,CPU通過時(shí)間片分配算法來給每個(gè)線程分配時(shí)間讓線程得以執(zhí)行,因?yàn)闀r(shí)間片非常短,所以在用戶角度來講,會感覺多個(gè)線程是在同時(shí)執(zhí)行.那什么是上下文切換呢?舉個(gè)例子,當(dāng)線程A執(zhí)行到某一步時(shí),此時(shí)CPU將時(shí)間讓給了線程B進(jìn)行執(zhí)行,那么在進(jìn)行切換的時(shí)候,系統(tǒng)一定要保留此時(shí)此刻線程A所執(zhí)行任務(wù)的狀態(tài),比如執(zhí)行到哪里、運(yùn)行時(shí)的參數(shù)等,那么當(dāng)下一次CPU將時(shí)間讓給線程A進(jìn)行執(zhí)行時(shí),才能正確的切換到A,并繼續(xù)執(zhí)行下去.所以任務(wù)從保留到再加載的過程就是一次上下文切換.
雖然上下文切換可以讓我們覺得可以“同時(shí)”做很多事,但是上下文切換也是需要系統(tǒng)開銷的.在《Java并發(fā)編程的藝術(shù)》中,作者舉例演示了串行和并發(fā)執(zhí)行累加操作,在結(jié)果中可以看得出,累加操作不同的次數(shù)會對不同的結(jié)果,所消耗的時(shí)間也有差別的.如果累加操作的次數(shù)沒有超過百萬次,那么串行執(zhí)行結(jié)果消耗的時(shí)間會比并行執(zhí)行的時(shí)間要少.所以在有些情況下我們需要盡可能的減少上下文切換的次數(shù),使用的辦法有:無鎖并發(fā)編程,CAS算法,使用最少線程和使用協(xié)程.(這里筆者也只知道有這幾種辦法,至于具體如何使用以及在何種場景下使用還未深入研究).
volatile是輕量級的synchronized,它保證了在多處理器開發(fā)中,共享變量的可見性,而且volatile不會引起上下文切換和調(diào)度.可見性的意思是當(dāng)一個(gè)線程修改了某個(gè)變量的值,另外一個(gè)線程可以讀到這個(gè)變量修改后的值,如果一個(gè)變量被volatile修飾,那么Java內(nèi)存模型確保所有線程看到這個(gè)變量的值是一致的.
Java中每一個(gè)對象都可以作為鎖,具體表示為:
當(dāng)一個(gè)線程拜訪同步代碼塊時(shí),必須要先得到鎖,退出或拋出異常時(shí),必須釋放鎖.對于上述三種情況,表現(xiàn)形式為:
1 /** 2 * 普通同步辦法,鎖是當(dāng)前實(shí)例對象 3 */ 4 public synchronized void test1(){ 5 //TODO something 6 } 7 8 /** 9 * 靜態(tài)同步辦法,鎖是當(dāng)前類的Class對象 10 */ 11 public static synchronized void test2(){ 12 //TODO something 13 } 14 15 /** 16 * 同步辦法塊,鎖是synchronized括號中的對象,這里是a 17 */ 18 public void test3(Integer a){ 19 synchronized (a){ 20 //TODO something 21 } 22 }
Java中所有實(shí)例域、靜態(tài)域和數(shù)組元素都存儲在堆內(nèi)存中,堆內(nèi)存在線程之間共享.
Java線程之間的通信由Java內(nèi)存模型(JMM)控制.JMM定義了線程和主內(nèi)存的關(guān)系:線程之前的共享變量存儲在主內(nèi)存中,每個(gè)線程都有一個(gè)私有的本地內(nèi)存(也叫工作內(nèi)存),本地內(nèi)存中存儲了該線程讀寫共享變量的副本.本地內(nèi)存是JMM的抽象概念,不真實(shí)存在,原諒了緩存,寫緩沖區(qū),寄存器以及其他硬件和編譯器優(yōu)化.Java內(nèi)存模型結(jié)構(gòu)圖:?
從上圖可以看出,線程A要與線程B進(jìn)行通信的話,必需要經(jīng)過兩個(gè)步驟:
如下圖:
重排序是指編譯器和處理器為了優(yōu)化法式性能而對指令序列進(jìn)行重新排序的一種手段.
定義:如果兩個(gè)操作同時(shí)拜訪一個(gè)變量,且這兩個(gè)操作中有一個(gè)為寫操作.此時(shí)這兩個(gè)操作之間就存在數(shù)據(jù)依賴性.
編譯器和處置器在重排序時(shí),會遵守?cái)?shù)據(jù)依賴性,編譯器和處置器不會改變存在數(shù)據(jù)依賴關(guān)系的兩個(gè)操作的執(zhí)行順序.
語義:不管怎么重排序,單線程程序的執(zhí)行結(jié)果不能被改變.編譯器,runtime和處理器都必需遵守as-if-serial語義.
為了遵守as-if-serial語義,編譯器和處置器不會對存在數(shù)據(jù)依賴關(guān)系的操作進(jìn)行重排序,但是如果操作之間不存在數(shù)據(jù)依賴關(guān)系,那么就有可能被進(jìn)行重排序.例如:
上面代碼中,C依賴A,C依賴B,所以編譯器不會重排序?qū)排在A,B之前.但是A,B之間沒有依賴,所以可能被進(jìn)行重排序,最終的執(zhí)行次序有兩種:
A->B->C;
B->A->C;
這兩種執(zhí)行順序?qū)ψ铋]幕果不會造成影響.
因?yàn)榇嬖谥嘏判?所以單線程程序不一定依照程序的順序來執(zhí)行.
該文主要講述了一些偏概念的器械,先有一些印象,后續(xù)會以代碼示例的形式進(jìn)行全面的復(fù)習(xí).
更多詳情見請繼續(xù)閱讀下一頁的出色內(nèi)容:
_baidu_page_break_tag_我們打仗了一些有關(guān)Java多線程的基本概念.這篇博客開始,我們就正式的進(jìn)入了Java多線程的實(shí)戰(zhàn)演練了.實(shí)戰(zhàn)演練不僅僅是貼代碼,也會涉及到相關(guān)概念和術(shù)語的講解.
程的狀態(tài)分為:新生,可運(yùn)行,運(yùn)行,壅閉,死亡5個(gè)狀態(tài).如下圖:
狀態(tài)闡明:
其他狀態(tài)比較簡單,阻塞狀態(tài)是其中比較有意思的.造成線程阻塞的原因有:
下面是一個(gè)關(guān)于使用Object類中wait()和notify()方法的例子:
1 /** 2 * @author zhouxuanyu 3 * @date 2017/05/17 4 */ 5 public class ThreadStatus { 6 7 private String flag[] = { "true" }; 8 9 public static void main(String[] args) { 10 System.out.println("main thread start..."); 11 ThreadStatus threadStatus = new ThreadStatus(); 12 WaitThread waitThread = threadStatus.new WaitThread(); 13 NotifyThread notifyThread = threadStatus.new NotifyThread(); 14 waitThread.start(); 15 notifyThread.start(); 16 } 17 18 class NotifyThread extends Thread { 19 20 @Override 21 public void run() { 22 synchronized (flag) { 23 for (int i = 0; i < 5; i++) { 24 try { 25 sleep(1000); 26 } catch (InterruptedException e) { 27 e.printStackTrace(); 28 } 29 System.out.println("NotifyThread.run()---" + i); 30 } 31 flag[0] = "false"; 32 flag.notify(); 33 } 34 } 35 } 36 37 class WaitThread extends Thread { 38 39 @Override 40 public void run() { 41 //使用flag使得線程獲得鎖 42 synchronized (flag) { 43 while (flag[0] != "false") { 44 System.out.println("WaitThread.run....."); 45 try { 46 flag.wait(); 47 } catch (InterruptedException e) { 48 e.printStackTrace(); 49 } 50 } 51 System.out.println("wait() end..."); 52 } 53 } 54 } 55 }
上面的代碼演示了如何使用wait()和notify()辦法,代碼釋義:主線程執(zhí)行,打印出main thread start.....語句當(dāng)在main()中調(diào)用waitThread.start()之后,線程啟動,執(zhí)行run辦法并獲得flag的鎖,并開始執(zhí)行同步代碼塊中的代碼,接下來調(diào)用wait()辦法后,waitThead阻塞并讓出flag鎖,此時(shí)notifyThread獲得flag鎖,開始執(zhí)行,每隔1s打印出對應(yīng)的語句,循環(huán)結(jié)束后,將flag中的標(biāo)志置為false并使用notify()喚醒waitThread線程,使得waitThread線程繼續(xù)執(zhí)行,打印出wait() end...此時(shí)程序結(jié)束.控制臺打印結(jié)果如下:
每個(gè)線程都有一個(gè)優(yōu)先級,在線程大量被阻塞時(shí),法式會優(yōu)先選擇優(yōu)先級較高的線程執(zhí)行.但是不代表優(yōu)先級低的線程不被執(zhí)行,只是機(jī)會相對較小罷.getPriority()獲取線程的優(yōu)先級,setPriority()設(shè)置線程的優(yōu)先級.線程默認(rèn)的優(yōu)先級為5.
都知道hashmap是線程不平安的.為什么不平安?先看下hashmap的數(shù)據(jù)結(jié)構(gòu):
在JDK1.8之前,hashmap采用數(shù)組+鏈表的數(shù)據(jù)布局,如上圖;而在JDK1.8時(shí),其數(shù)據(jù)布局變?yōu)榱藬?shù)據(jù)+鏈表+紅黑樹.??鏈表長度超過8時(shí),會自動轉(zhuǎn)換為紅黑樹(抽時(shí)間復(fù)習(xí)紅黑樹,快忘記了!),提高了查找效率.當(dāng)向hashmap中添加元素時(shí),hashmap內(nèi)部實(shí)現(xiàn)會根據(jù)key值計(jì)算其hashcode,如果hashcode值沒有重復(fù),則直接添加到下一個(gè)節(jié)點(diǎn).如果hashcode重復(fù)了,則會在重復(fù)的位置,以鏈表的方式存儲該元素.JDK1.8源碼分析:
1 /** 2 * Associates the specified value with the specified key in thismap. 3 * If the map previously contained a mapping for the key, the old 4 * value is replaced. 5 * 6 */ 7 public V put(K key, V value) { 8 return putVal(hash(key), key, value, false, true); 9 } 10 static final int hash(Object key) { 11 int h; 12 //key的值為null時(shí),hash值返回0,對應(yīng)的table數(shù)組中的位置是0 13 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); 14 } 15 16 /** 17 * Implements Map.put and related methods 18 * 19 * @param hash hash for key 20 * @param key the key 21 * @param value the value to put 22 * @param onlyIfAbsent if true, don't change existing value 23 * @param evict if false, the table is in creation mode. 24 * @return previous value, or null if none 25 */ 26 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, 27 boolean evict) { 28 Node<K,V>[] tab; Node<K,V> p; int n, i; 29 //先將table賦給tab,判斷table是否為null或大小為0,若為真,就調(diào)用resize()初始化 30 if ((tab = table) == null || (n = tab.length) == 0) 31 n = (tab = resize()).length; 32 //通過i = (n - 1) & hash得到table中的index值,若為null,則直接添加一個(gè)newNode 33 if ((p = tab[i = (n - 1) & hash]) == null) 34 tab[i] = newNode(hash, key, value, null); 35 else { 36 //執(zhí)行到這里,說明發(fā)生碰撞,即tab[i]不為空,需要組成單鏈表或紅黑樹 37 Node<K,V> e; K k; 38 if (p.hash == hash && 39 ((k = p.key) == key || (key != null && key.equals(k)))) 40 //此時(shí)p指的是table[i]中存儲的那個(gè)Node,如果待插入的節(jié)點(diǎn)中hash值和key值在p中已經(jīng)存在,則將p賦給e 41 e = p; 42 //如果table數(shù)組中node類的hash、key的值與將要插入的Node的hash、key不吻合,就需要在這個(gè)node節(jié)點(diǎn)鏈表或者樹節(jié)點(diǎn)中查找. 43 else if (p instanceof TreeNode) 44 //當(dāng)p屬于紅黑樹結(jié)構(gòu)時(shí),則按照紅黑樹方式插入 45 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); 46 else { 47 //到這里說明碰撞的節(jié)點(diǎn)以單鏈表形式存儲,for循環(huán)用來使單鏈表依次向后查找 48 for (int binCount = 0; ; ++binCount) { 49 //將p的下一個(gè)節(jié)點(diǎn)賦給e,如果為null,創(chuàng)建一個(gè)新節(jié)點(diǎn)賦給p的下一個(gè)節(jié)點(diǎn) 50 if ((e = p.next) == null) { 51 p.next = newNode(hash, key, value, null); 52 //如果沖突節(jié)點(diǎn)達(dá)到8個(gè),調(diào)用treeifyBin(tab, hash),這個(gè)treeifyBin首先回去判斷當(dāng)前hash表的長度,如果不足64的話,實(shí)際上就只進(jìn)行resize,擴(kuò)容table,如果已經(jīng)達(dá)到64,那么才會將沖突項(xiàng)存儲結(jié)構(gòu)改為紅黑樹. 53 54 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st 55 treeifyBin(tab, hash); 56 break; 57 } 58 //如果有相同的hash和key,則退出循環(huán) 59 if (e.hash == hash && 60 ((k = e.key) == key || (key != null && key.equals(k)))) 61 break; 62 p = e;//將p調(diào)整為下一個(gè)節(jié)點(diǎn) 63 } 64 } 65 //若e不為null,表示已經(jīng)存在與待插入節(jié)點(diǎn)hash、key相同的節(jié)點(diǎn),hashmap后插入的key值對應(yīng)的value會覆蓋以前相同key值對應(yīng)的value值,就是下面這塊代碼實(shí)現(xiàn)的 66 if (e != null) { // existing mapping for key 67 V oldValue = e.value; 68 //判斷是否修改已插入節(jié)點(diǎn)的value 69 if (!onlyIfAbsent || oldValue == null) 70 e.value = value; 71 afterNodeAccess(e); 72 return oldValue; 73 } 74 } 75 ++modCount;//插入新節(jié)點(diǎn)后,hashmap的結(jié)構(gòu)調(diào)整次數(shù)+1 76 if (++size > threshold) 77 resize();//HashMap中節(jié)點(diǎn)數(shù)+1,如果大于threshold,那么要進(jìn)行一次擴(kuò)容 78 afterNodeInsertion(evict); 79 return null; 80 }
hashmap不平安的原因:上面分析了JDK源碼,知道了put方法不是同步的,如果多個(gè)線程,在某一時(shí)刻同時(shí)操作HashMap并執(zhí)行put操作,而有大于兩個(gè)key的hash值相同,這個(gè)時(shí)候需要解決碰撞沖突,而解決沖突的辦法采用拉鏈法解決碰撞沖突,對于鏈表的結(jié)構(gòu)在這里不再贅述,暫且不討論是從鏈表頭部插入還是從尾部插入,這個(gè)時(shí)候兩個(gè)線程如果恰好都取到了對應(yīng)位置的頭結(jié)點(diǎn)e1,而最終的結(jié)果可想而知,這兩個(gè)數(shù)據(jù)中勢必會有一個(gè)會丟失.同理,hashmap擴(kuò)容的方法也如此,當(dāng)多個(gè)線程同時(shí)檢測到總數(shù)量超過門限值的時(shí)候就會同時(shí)調(diào)用resize操作,各自生成新的數(shù)組并rehash后賦給該map底層的數(shù)組table,結(jié)果最終只有最后一個(gè)線程生成的新數(shù)組被賦給table變量,其他線程的均會丟失.而且當(dāng)某些線程已經(jīng)完成賦值而其他線程剛開始的時(shí)候,就會用已經(jīng)被賦值的table作為原始數(shù)組,這樣也會有問題.
HashTable容器使用synchronized來保證線程平安,但在線程競爭激烈的情況下HashTable的效率非常低下.因?yàn)楫?dāng)一個(gè)線程訪問HashTable的同步方法,其他線程也訪問HashTable的同步方法時(shí),會進(jìn)入阻塞或輪詢狀態(tài).如線程1使用put進(jìn)行元素添加,線程2不但不能使用put方法添加元素,也不能使用get方法來獲取元素,所以競爭越激烈效率越低.
ConcurrentHashMap鎖分段技術(shù):HashTable容器在競爭激烈的并發(fā)環(huán)境下表現(xiàn)出效率低下的原因是所有拜訪HashTable的線程都必須競爭同一把鎖,假如容器里有多把鎖,每一把鎖用于鎖容器其中一部分?jǐn)?shù)據(jù),那么當(dāng)多線程拜訪容器里不同數(shù)據(jù)段的數(shù)據(jù)時(shí),線程間就不會存在鎖競爭,從而可以有效提高并發(fā)拜訪效率,這就是ConcurrentHashMap所使用的鎖分段技術(shù).首先將數(shù)據(jù)分成一段一段地存儲,然后給每一段數(shù)據(jù)配一把鎖,當(dāng)一個(gè)線程占用鎖拜訪其中一個(gè)段數(shù)據(jù)的時(shí)候,其他段的數(shù)據(jù)也能被其他線程拜訪.以下是ConcurrentHashMap的數(shù)據(jù)結(jié)構(gòu):
ConcurrentHashMap是由Segment數(shù)組結(jié)構(gòu)和HashEntry數(shù)組結(jié)構(gòu)組成.Segment是一種可重入鎖(ReentrantLock),在ConcurrentHashMap里飾演鎖的角色;HashEntry則用于存儲鍵值對數(shù)據(jù).一個(gè)ConcurrentHashMap里包含一個(gè)Segment數(shù)組.Segment的結(jié)構(gòu)和HashMap類似,是一種數(shù)組和鏈表結(jié)構(gòu).一個(gè)Segment里包含一個(gè)HashEntry數(shù)組,每個(gè)HashEntry是一個(gè)鏈表結(jié)構(gòu)的元素每Segment守護(hù)著一個(gè)HashEntry數(shù)組里的元素,當(dāng)對HashEntry數(shù)組的數(shù)據(jù)進(jìn)行修改時(shí),必須首先獲得與它對應(yīng)的Segment鎖.
Fork/Join框架是Java 7提供的一個(gè)用于并行執(zhí)行任務(wù)的框架,是**一個(gè)把大任務(wù)分割成若干個(gè)小任務(wù),最終匯總每個(gè)小任務(wù)結(jié)果后得到大任務(wù)結(jié)果的框架.**Fork就是把一個(gè)大任務(wù)切分為若干子任務(wù)并行的執(zhí)行,Join就是合并這些子任務(wù)的執(zhí)行結(jié)果,最后得到這個(gè)大任務(wù)的結(jié)果.好比計(jì)算1+2+…+10000,可以分割成10個(gè)子任務(wù),每個(gè)子任務(wù)分別對1000個(gè)數(shù)進(jìn)行求和,最終匯總這10個(gè)子任務(wù)的結(jié)果.如圖:
工作竊取(work-stealing)算法是指某個(gè)線程從其他隊(duì)列里竊取任務(wù)來執(zhí)行.假如我們需要做一個(gè)比較大的任務(wù),可以把這個(gè)任務(wù)分割為若干互不依賴的子任務(wù),為了減少線程間的競爭,把這些子任務(wù)分別放到不同的隊(duì)列里,并為每個(gè)隊(duì)列創(chuàng)建一個(gè)單獨(dú)的線程來執(zhí)行隊(duì)列里的任務(wù),線程和隊(duì)列一一對應(yīng).比如A線程負(fù)責(zé)處理A隊(duì)列里的任務(wù).但是,有的線程會先把自己隊(duì)列里的任務(wù)干完,而其他線程對應(yīng)的隊(duì)列里還有任務(wù)等待處理.干完活的線程與其等著,不如去幫其他線程干活,于是它就去其他線程的隊(duì)列里竊取一個(gè)任務(wù)來執(zhí)行.而在這時(shí)它們會拜訪同一個(gè)隊(duì)列,所以為了減少竊取任務(wù)線程和被竊取任務(wù)線程之間的競爭,通常會使用雙端隊(duì)列,被竊取任務(wù)線程永遠(yuǎn)從雙端隊(duì)列的頭部拿任務(wù)執(zhí)行,而竊取任務(wù)的線程永遠(yuǎn)從雙端隊(duì)列的尾部拿任務(wù)執(zhí)行.如下圖:
該篇文章主要記錄一些關(guān)于線程中鎖機(jī)制的基礎(chǔ),以及簡單闡發(fā)了一下HashMap,HashTable以及ConcurrentHashMap的相關(guān)原理,文章最后簡單的涉及了一下Fork-Join框架.
本文永遠(yuǎn)更新鏈接地址:
更多LINUX教程,盡在維易PHP學(xué)院專欄。歡迎交流《LINUX實(shí)操:《Java并發(fā)編程的藝術(shù)》讀書筆記》!
轉(zhuǎn)載請注明本頁網(wǎng)址:
http://www.fzlkiss.com/jiaocheng/12115.html