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