java並發容器和架構

來源:互聯網
上載者:User

標籤: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並發容器和架構

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.