標籤:this info als java實現 ret 操作 有一個 規則 使用
1.ConcurrentHashMap1.1為什麼要使用ConcurrentHashMap在並發編程中使用HashMap可能導致程式死迴圈。而使用安全執行緒的HashTable效率又非常低下,基於以上兩個原因,便有了ConcurrentHashMap的登場機會。(1)線程不安全的HashMap在多線程環境下,使用HashMap進行put操作會引起死迴圈,導致CPU利用率接近100%。原因是表擴容的時候,容易形成環鏈(2)效率低下的HashTableHashTable容器使用synchronized來保證安全執行緒,但線上程競爭激烈的情況下HashTable的效率非常低下。因為當一個線程訪問HashTable的同步方法,其他線程也訪問HashTable的同步方法時,會進入阻塞或輪詢狀態。如線程1使用put進行元素添加,線程2不但不能使用put方法添加元素,也不能使用get方法來擷取元素,所以競爭越激烈效率越低。(3)ConcurrentHashMap的鎖分段技術可有效提升並發訪問率HashTable容器在競爭激烈的並發環境下表現出效率低下的原因是所有訪問HashTable的線程都必須競爭同一把鎖,假如容器裡有多把鎖,每一把鎖用於鎖容器其中一部分資料,那麼當多線程訪問容器裡不同資料區段的資料時,線程間就不會存在鎖競爭,從而可以有效提高並發訪問效率,這就是ConcurrentHashMap所使用的鎖分段技術。首先將資料分成一段一段地儲存,然後給每一段資料配一把鎖,當一個線程佔用鎖訪問其中一個段資料的時候,其他段的資料也能被其他線程訪問。1.2 ConcurrentHashMap的結構ConcurrentHashMap是由Segment數組結構和HashEntry數組結構組成。Segment是一種可重新進入鎖(ReentrantLock),在ConcurrentHashMap裡扮演鎖的角色;HashEntry則用於儲存索引值對資料。一個ConcurrentHashMap裡包含一個Segment數組。Segment的結構和HashMap類似,是一種數組和鏈表結構。一個Segment裡包含一個HashEntry數組,每個HashEntry是一個鏈表結構的元素,每個Segment守護著一個HashEntry數組裡的元素,當對HashEntry數組的資料進行修改時,必須首先獲得與它對應的Segment鎖,1-2所示。
1.3.get操作get操作的高效之處在於整個get過程不需要加鎖,除非讀到的值是空才會加鎖重讀。我們知道HashTable容器的get方法是需要加鎖的,那麼ConcurrentHashMap的get操作是如何做到不加鎖的呢?原因是它的get方法裡將要使用的共用變數都定義成volatile類型,如用於統計當前Segement大小的count欄位和用於儲存的HashEntry的value。定義成volatile的變數,能夠線上程之間保持可見度,能夠被多線程同時讀,並且保證不會讀到到期的值,但是只能被單線程寫(有一種情況可以被多線程寫,就是寫入的值不依賴於原值),在get操作裡只需要讀不需要寫共用變數count和value,所以可以不用加鎖。之所以不會讀到到期的值,是因為根據Java記憶體模型的happen before原則,對volatile欄位的寫入操作先於讀操作,即使兩個線程同時修改和擷取volatile變數,get操作也能拿到最新的值,這是volatile替換鎖的經典應用情境。1.4put操作由於put方法裡需要對共用變數進行寫入操作,所以為了安全執行緒,在操作共用變數時必須加鎖。put方法首先定位到Segment,然後在Segment裡進行插入操作。插入操作需要經曆兩個步驟,第一步判斷是否需要對Segment裡的HashEntry數組進行擴容,第二步定位添加元素的位置,然後將其放在HashEntry數組裡。(1)如何擴容。在擴容的時候,首先會建立一個容量是原來容量兩倍的數組,然後將原數組裡的元素進行再散列後插入到新的數組裡。為了高效,ConcurrentHashMap不會對整個容器進行擴容,而只對某個segment進行擴容。1.51.5.size操作如果要統計整個ConcurrentHashMap裡元素的大小,就必須統計所有Segment裡元素的大小後求和。Segment裡的全域變數count是一個volatile變數,那麼在多線程情境下,是不是直接把所有Segment的count相加就可以得到整個ConcurrentHashMap大小了呢?不是的,雖然相加時可以擷取每個Segment的count的最新值,但是可能累加前使用的count發生了變化,那麼統計結果就不準了。所以,最安全的做法是在統計size的時候把所有Segment的put、remove和clean方法全部鎖住,但是這種做法顯然非常低效。因為在累加count操作過程中,之前累加過的count發生變化的幾率非常小,所以ConcurrentHashMap的做法是先嘗試2次通過不鎖住Segment的方式來統計各個Segment大小,如果統計的過程中,容器的count發生了變化,則再採用加鎖的方式來統計所有Segment的大小。那麼ConcurrentHashMap是如何判斷在統計的時候容器是否發生了變化呢?使用modCount變數,在put、remove和clean方法裡操作元素前都會將變數modCount進行加1,那麼在統計size前後比較modCount是否發生變化,從而得知容器的大小是否發生變化。2 ConcurrentLinkedQueue一種使用非阻塞演算法的基於連結節點的無界安全執行緒隊列,它採用先進先出的規則對節點進行排序,當我們添加一個元素的時候,它會添加到隊列的尾部;當我們擷取一個元素時,它會返回隊列頭部的元素。它採用了“wait-free”演算法(即CAS演算法)來實現
入隊和出隊,都不會立即更新head和tair指標:發現入隊主要做兩件事情:第一是將入隊節點設定成當前隊列尾節點的下一個節點;第二是更新tail節點,如果tail節點的next節點不為空白,則將入隊節點設定成tail節點,如果tail節點的next節點為空白,則將入隊節點設定成tail的next節點,所以tail節點不總是尾節 3什麼是阻塞隊列阻塞隊列(BlockingQueue)是一個支援兩個附加操作的隊列。這兩個附加的操作支援阻塞的插入和移除方法。1)支援阻塞的插入方法:意思是當隊列滿時,隊列會阻塞插入元素的線程,直到隊列不滿。2)支援阻塞的移除方法:意思是在隊列為空白時,擷取元素的線程會等待隊列變為非空。
·拋出異常:當隊列滿時,如果再往隊列裡插入元素,會拋出IllegalStateException("Queuefull")異常。當隊列空時,從隊列裡擷取元素會拋NoSuchElementException異常。·返回特殊值:當往隊列插入元素時,會返回元素是否插入成功,成功返回true。如果是移除方法,則是從隊列裡取出一個元素,如果沒有則返回null。·一直阻塞:當阻塞隊列滿時,如果生產者線程往隊列裡put元素,隊列會一直阻塞生產者線程,直到隊列可用或者響應中斷退出。當隊列空時,如果消費者線程從隊列裡take元素,隊列會阻塞住消費者線程,直到隊列不為空白。·逾時退出:當阻塞隊列滿時,如果生產者線程往隊列裡插入元素,隊列會阻塞生產者線程一段時間,如果超過了指定的時間,生產者線程就會退出。JDK 7提供了7個阻塞隊列,如下。·ArrayBlockingQueue:一個由數組結構組成的有界阻塞隊列。·LinkedBlockingQueue:一個由鏈表結構組成的有界阻塞隊列。·PriorityBlockingQueue:一個支援優先順序排序的無界阻塞隊列。·DelayQueue:一個使用優先順序隊列實現的無界阻塞隊列。·SynchronousQueue:一個不儲存元素的阻塞隊列。·LinkedTransferQueue:一個由鏈表結構組成的無界阻塞隊列。·LinkedBlockingDeque:一個由鏈表結構組成的雙向阻塞隊列。4.Fork/Join架構Fork/Join架構是Java 7提供的一個用於並存執行任務的架構,是一個把大任務分割成若干個小任務,最終匯總每個小任務結果後得到大任務結果的架構。1.工作竊取(work-stealing)是指某個線程從其他隊列裡竊取任務來執行。幹完活的線程與其等著,不如去幫其他線程幹活,於是它就去其他線程的隊列裡竊取一個任務來執行。而在這時它們會訪問同一個隊列,所以為了減少竊取任務線程和被竊取任務線程間的競爭,通常會使用雙端隊列,被竊取任務線程永遠從雙端隊列的頭部拿任務執行,而竊取任務的線程永遠從雙端隊列的尾部拿任務執行。優點:充分利用線程進行並行計算,減少了線程間的競爭。缺點:在某些情況下還是存在競爭,比如雙端隊列裡只有一個任務時。並 且該演算法會消耗了更多的系統資源,比如建立多個線程和多個雙端隊列。2.java實現Fork/Join使用兩個類來完成以上兩件事情。①ForkJoinTask:我們要使用ForkJoin架構,必須首先建立一個ForkJoin任務。它提供在任務中執行fork()和join()操作的機制。通常情況下,我們不需要直接繼承ForkJoinTask類,只需要繼承它的子類,Fork/Join架構提供了以下兩個子類。·RecursiveAction:用於沒有返回結果的任務。·RecursiveTask:用於有返回結果的任務。②ForkJoinPool:ForkJoinTask需要通過ForkJoinPool來執行。
public class CountTask extends RecursiveTask<Integer> { private static final int THRESHOLD = 2;// 閾值 private int start; private int end; public CountTask(int start, int end) { this.start = start; this.end = end; } protected Integer compute() { int sum = 0;// 如果任務足夠小就計算任務 boolean canCompute = (end - start) <= THRESHOLD; if (canCompute) { for (int i = start; i <= end; i++) { sum += i; } } else {// 如果任務大於閾值,就分裂成兩個子任務計算 int middle = (start + end) / 2; CountTask leftTask = new CountTask(start, middle); CountTask rightTask = new CountTask(middle + 1, end);// 執行子任務 leftTask.fork(); rightTask.fork();// 等待子任務執行完,並得到其結果 int leftResult=leftTask.join(); int rightResult=rightTask.join();// 合并子任務 sum = leftResult + rightResult; } return sum; } static public void test(){ ForkJoinPool forkJoinPool = new ForkJoinPool();// 產生一個計算任務,負責計算1+2+3+4 CountTask task = new CountTask(1, 40000);// 執行一個任務 Future<Integer> result = forkJoinPool.submit(task); try { System.out.println(result.get()); } catch (InterruptedException e) { } catch (ExecutionException e) { } }}
java並發容器和架構