天天看點

聊聊并發(一)——初識JUC

  JDK 5.0 提供了java.util.concurrent包,在此包中增加了并發程式設計中很常用的使用工具類,用于定義類似于線程的自定義子系統,包括線程池、異步IO和輕量級任務架構。提供可調的、靈活的線程池。還提供了設計用于多線程上下文的Collection實作等。

  記憶體可見性是指當某個線程正在使用對象狀态而另一線程在同時修改該狀态,需要確定當一個線程修改了對象狀态後,其他線程能夠看到發生的狀态變化。

  可見性錯誤是指當讀操作與寫操作在不同的線程中執行時,我們無法確定執行讀操作的線程能實時的看到其他線程寫入之後的值,有時甚至是根本不可能的事情。

  我們可以通過同步來保證對象被安全的釋出。除此之外我們也可以使用一種更加輕量級的volatile變量。

  記憶體可見性問題:當多個線程同時操作共享資料時,對共享資料的操作彼此是不可見的。

  代碼示例:記憶體可見性問題

  問題:結果1不難了解,當線程執行完畢後,主線程才開始執行while。為什麼結果2是死循環呢?

  原因:JVM為每一個執行任務的線程,它都會配置設定一個獨立的工作記憶體用于提高效率。每次都會從主存中讀取變量的副本到各自的工作記憶體中,修改後,再寫回主存中。

  那麼,不難了解結果2:主線程從主存讀取flag = false,因為用的while循環,while屬于底層的東西,執行速度非常快,沒有再讀主存的機會,一直讀取的是自己的工作記憶體(flag = false)。而當線程1讀到flag并修改為true,回寫到主存時,主線程并不知道,是以死循環。

聊聊并發(一)——初識JUC

  解決:知道問題原因了,如何解決呢?

  代碼示例:方式一、加鎖

  分析:synchronize加鎖可以解決。加了鎖,就可以讓while循環每次都從主存中去讀取資料,這樣就能讀取到true了。但是加鎖效率極低。每次隻能有一個線程通路,當一個線程持有鎖時,其他線程就會阻塞,效率就非常低了。不想加鎖,又要解決記憶體可見性問題,那麼就可以使用volatile關鍵字。

  代碼示例:方式二、用volatile修飾

  Java提供了一種稍弱的同步機制——volatile關鍵字,當多個線程通路共享資料時,可以保證記憶體可見性,即記憶體中的資料可見。用這個關鍵字修飾共享資料,就會及時的把線程工作記憶體中的資料重新整理到主存中去,也可以了解為,就是直接操作主存中的資料。

  可以将volatile看做一個輕量級的鎖,相較于synchronized是一種輕量級的同步政策。與鎖(synchronize)的差別:

  volatile不具備互斥性。即一個線程通路共享資料,另一個線程依然可以通路。所有的通路都在主存中完成,保證記憶體可見性。

  synchronized具備互斥性。即一個線程搶到鎖,另一個線程進不來,必須等待。

  volatile不能保證變量的原子性。

  所謂原子性就是一組操作不可再細分。要麼全都做,要麼全都不做。前面提到volatile不能保證變量的原子性,具體表現如下:

  代碼示例:原子性問題

  問題:期望結果應該每個線程對 i 自增一次,最終 i 的值為10。實際結果如上(有重複資料)。

  原因:i++操作不是一個原子性操作,實際分為讀改寫三步,如下:

  int temp = i; // 從主存中讀   i = i + 1; // cpu 對 i 進行+1運算   i = temp; // 寫回主存

  而volatile不能保證變量的原子性。volatile,隻是相當于所有線程都是在主存中操作資料而已,并不具備互斥性。比如兩個線程同時讀取主存中的0,然後又同時自增,同時寫入主存,結果還是會出現重複資料。volatile的不具備互斥性也導緻了它不具備原子性。

  代碼示例:方式二、原子變量

  JDK 1.5 以後java.util.concurrent.atomic包下提供了常用的原子變量。這些原子變量具備以下特點:volatile的記憶體可見性;CAS算法保證資料的原子性。

  類的小工具包,支援在單個變量上解除鎖的線程安全程式設計。事實上,此包中的類可将volatile值、字段和數組元素的概念擴充到那些也提供原子條件更新操作的類。

  類AtomicBoolean、AtomicInteger、AtomicLong和AtomicReference的執行個體各自提供對相應類型單個變量的通路和更新。每個類也為該類型提供适當的實用工具方法。

  AtomicIntegerArray、AtomicLongArray和AtomicReferenceArray類進一步擴充了原子操作,對這些類型的數組提供了支援。這些類在為其數組元素提供volatile通路語義方面也引人注目,這對于普通數組來說是不受支援的。

  核心方法:boolean compareAndSet(int expectedValue, int updateValue)

  java.util.concurrent.atomic包下提供了一些原子操作的常用類:

  AtomicBoolean、AtomicInteger、AtomicLong、AtomicReference<V>   AtomicIntegerArray、AtomicLongArray   AtomicMarkableReference<V>   AtomicReferenceArray<E>   AtomicStampedReference<V>

  CAS(Compare and Swap)是一種硬體對并發的支援,針對多處理器操作而設計的處理器中的一種特殊指令,用于管理對共享資料的并發通路,是硬體對于并發操作共享資料的支援。

  CAS是一種無鎖的非阻塞算法的實作。不存在上下文切換的問題。

  CAS包含了3個操作數:記憶體值V,比較值A,更新值B。當且僅當V == A時,V = B,否則不執行任何操作。

  CAS算法:當多個線程并發的對主存中的資料進行修改的時候。有且隻有一個線程會成功,其他的都會失敗。(同時操作,隻是會失敗而已,并不會被鎖之類的)。

  CAS比普通同步鎖效率高,原因:CAS算法當這一次不成功的時候,它下一次不會阻塞,也就是它不會放棄CPU的執行權,它可以立即再次嘗試,再去更新。

  代碼示例:模拟CAS算法

  JDK 1.5之後,在java.util.concurrent包中提供了多種并發容器類來改進同步容器類的性能。其中最主要的就是ConcurrentHashMap,采用"鎖分段"機制。

  HashMap是線程不安全的;Hashtable 加了鎖,是線程安全的,是以它效率低。Hashtable 加鎖就是将整個hash表鎖起來,當有多個線程通路時,同一時間隻能有一個線程通路,并行變成串行,是以效率低。

  ConcurrentHashMap是一個線程安全的hash表。對于多線程的操作,介于 HashMap 與 Hashtable 之間。内部采用"鎖分段"機制替代 Hashtable 的獨占鎖,進而提高性能。

聊聊并發(一)——初識JUC

  每個段都是一個獨立的鎖。JDK 1.8 以後concurrentHashMap的鎖分段被取消了。采用的是CAS算法。

  此包還提供了設計用于多線程上下文中的 Collection 實作:

  ConcurrentHashMap   ConcurrentSkipListMap   ConcurrentSkipListSet   CopyOnWriteArrayList   CopyOnWriteArraySet

  當期望多線程通路一個給定 collection 時,ConcurrentHashMap 通常優于同步的 HashMap,ConcurrentSkipListMap 通常優于同步的 TreeMap。當期望的讀數和周遊遠遠大于清單的更新數時,CopyOnWriteArrayList 優于同步的 ArrayList。

  代碼示例:CopyOnWriteArrayList

  如果用CopyOnWriteArrayList,則不會有異常。

  CopyOnWriteArrayList:寫入并複制,添加操作多時,效率低,因為每次添加時都會進行複制,開銷非常的大。并發疊代操作多時可以選擇。

  java.util.concurrent包中提供了多種并發容器類來改進同步容器的性能。CountDownLatch是一個同步輔助類,在完成某些運算時,隻有其他所有線程的運算全部完成,目前運算才繼續執行,這就叫閉鎖。

  代碼示例:計算10個線程列印偶數的時間

  Callable和Runable的差別是,Callable帶泛型,其call方法有傳回值。使用的時候,需要用FutureTask來接收傳回值。而且它也要等到線程執行完調用get方法才會執行,也可以用于閉鎖操作。

  代碼示例:

  在JDK1.5之前,解決多線程安全問題用sychronized隐式鎖:同步代碼塊;同步方法。

  在JDK1.5之後,出現了更加靈活的方式Lock顯式鎖:同步鎖。

  Lock需要通過lock()方法上鎖,通過unlock()方法釋放鎖。為了保證鎖能釋放,所有unlock方法一般放在finally中去執行。

  代碼示例:賣票問題

  多個線程并發讀資料,是不會出現問題。但是,多個線程并發寫資料,到底是寫入哪個線程的資料呢?是以,寫寫/讀寫需要互斥,讀讀不需要互斥。這個時候可以用讀寫鎖來提高效率。

  ReadWriteLock 維護了一對相關的鎖,一個用于隻讀操作,另一個用于寫入操作。隻要沒有 writer,讀取鎖可以由多個 reader 線程同時保持。

  讀鎖,可以多個線程并發的持有。

  寫鎖,是獨占的。

  源碼示例:讀寫鎖

作者:Craftsman-L