java併發編程的藝術,讀書筆記第六章 concurrentHashMap以及併發容器的介紹

ConcurrentHashMap的原理

將數據一段一段的存儲然後給每一段數據分配一把鎖,當線程訪問數據的一段時,爲每段分配一把鎖,同時其他段的數據可以被其他線程數據訪問

2)concurrentHashMap 的結構

concurrentHashMap 由segament數組和hashentry數組結構組成,segament是一種可靠的重入鎖,在裏面扮演鎖的角色,hashentry用於存儲鍵值對數據,一個segament裏面包含一個hashentry數組,每個hashentry是一個鏈表結構的元素,每個segament守護着一個hashentry數組裏的元素,當hashentry數組的數據進行修改時,必須首先獲得與它對應的鎖

初始化segament數組

if(concurrencyLevel>max_segament){

concurrencyLevel=max_segament;

}

int sshift=0;

int sszie=1;

while(ssize<concurrentcyLevel){

++sshift;

ssize<<=1;

}

segamentshift=32-sshift;

segamentMask=ssize-1;

this.segaments=Segament.newArray(ssize);

ssize 是2的n次方最接近concurrencylevel的數字。

segamentshift爲段偏移量

segamentmark爲段掩碼

2定位segament:在插入或者獲取元素的時候,必須通過散列算法對hashcode進行二次散列,之所以二次散列目的是減少散列衝突,使元素能夠均勻的分佈在不同的segament上,最壞情況,所有的值都散列在一個segament上

計算segament位置的僞代碼如下:

final  Segament<K,V> segamentfor(int hash){

return segaments[hash>>>segamentshift&segamentMask]

}

ConcurrentHashMap 的操作:get put size

get僞代碼如下:

public v get(Object hashcode)){

//此函數的作用是對hashcode進行二次散列,因爲對效率有要求,故採用位移算法的方式

int hash = hash(hashcode);

return segamentfor(hash).get(key,hash);

}

get操作的高效之處在於在讀取時不用加鎖,除非是讀到空值,重新加鎖,那麼如何做到不加鎖?主要的方法在於使用volatitle變量,使之在線程間可見。能夠被多線程同時讀,並且保證不會讀到過期的值,但是隻能被單線程寫,根據happen-before原則對volatitle的寫優先於讀

定位hashentry和segament的算法雖然一樣,但是值不一樣

兩個方法如下

hash>>>segamentshift&segamentMarsk 定位segament所使用的hash算法

int index=hash&(tab.length-1) //定位hashentry的算法

目的是在segament和hashentry中同時散列開

put操作:

put需要對共享變量進行寫入,因此爲了線程安全需要加鎖

步驟1)判斷是否需要擴容

注意:hashmap線插入後擴容,有可能產生無效擴容

concurrenthashmap 先擴容在插入。

擴容方式:將hashentry數組擴容至原來兩倍,重新散列後插入,同時只會對segament進行擴容,不會全部擴容。

size操作

統計size的安全方式:在put clean remove方法時鎖住變量,但這種效率非常低下

concurrenthashmap採用的方式是實用modcount變量 每進行一次 put  clean remove操作時 將變量 +1 然後在統計size前後進行比較時否相等,從而得知容器時否發生變化

concurrentlinkedqueue非阻塞隊列

隊列有兩種實現方式阻塞隊列和非阻塞隊列,阻塞隊列是採用阻塞算法,即使用鎖來實現,非阻塞隊列是使用cas循環來實現

concurrentlinkedqueue是一種***隊列採用了先進先出的方式進行了排序 ,採用了 wait -free算法

最簡單的算法

offer(入隊列)算法:

public boolean offer(E e){

Node node = new Node(e);

while(true){

Node tail=gettail();

if(tail.getnext().casNext(null,node)&&node.casTail(tail,node)){

return true;

}

}

}

//出隊列 個人簡化的僞代碼,可能不準確歡迎指正

public e poll(){

while(true){

Node head = getHead();

currentNode = head.getNode();

//將head節點替換爲下一個節點

if(head.cas(currentNode,currentNode.getNext())){

//斷絕引用

currentNode.setNext()=null;

return current;

}

}

注意next 節點可以使用atomicReference 實現。

這樣就可以實現cas操作

阻塞隊列

阻塞隊列是一個支持兩個附加操作的隊列,這兩個操作支持阻塞的插入和移除

1)阻塞插入:當隊列滿時,隊列阻塞所有的插入操作,直到隊列不滿

2)阻塞移除:當隊列爲空時,隊列阻塞所有的移除操作,直到隊列不空

阻塞隊列常用語生產者和消費者場景,生產者是向隊列裏添加元素的線程,消費者是從隊列裏取元素的線程,阻塞隊列是存放元素的容器

隊列阻塞的四種處理方式

1)拋出異常

2)返回特殊值

3)一直阻塞

4)超時退出

java中常見的阻塞隊列

1)ArrayBlockingQueue:是用數組實現的有界阻塞隊列,此隊列按照先進先出的原理進行排序

2)linkedBlockingQueue:是用鏈表實現的有界阻塞隊列此隊列的最大長度爲Integer.MAX_VALUE 值

3)PriorityBlockingQueue:通過compartor方法或者compareTo方法進行排序

4)DelayQueue:是一個支持延遲獲取元素的***阻塞隊列,使用pritorityQueue隊列,實現Delay接口

delayqueue非常有用

1)緩存設計:可以使用delayqueue保存緩存元素的有效期,使用一個線程循環查詢delayqueue,當查到元素時,表示該緩存到期了

2)定時任務調度:使用delayqueue保存當天將會執行的任務和執行時間,當從delayqueue查詢到了任務時就開始執行。

如何實現delay接口

1)在創建對象時初始化

2)實現getdelay方法,返回執行還需要的時間

3)實現compareTo方法,將返回時間最長的放在最後面

4)linkedblockingdqeque:由鏈表組成的雙向阻塞隊列;可以用在工作-竊取模式中

阻塞隊列實現原理:

1)使用通知模式實現,主要通過condtion方式實現隊列阻塞

僞代碼如下:

Lock lock = new RetreenLock();

private final Condition notempty =lock.newCondition();//表示可以獲取

private final  Condition notFull = lock.newConditon();//表示可以插入

//插入方法

public void put(E e){

final RetreenLock lock =this.lock;

lock.lockinterrupt();

try{

while(e.length=max_count){

notfull.await();

}

insert(e);

}finally{

lock.unlock();

}

private void insert(E e){

items[putIndex]=e;

putIndex=inc(e);

++count;

notEmpty.signal();//表示通知take線程可以獲取

}

//獲取方法

public E take(){

final Reentrantlock lock = this.lock;

lock.interruptibly();

try{

while(item.length==0){

//表示隊列爲空,線程阻塞

notempty.await();

}

return extract();

}finally{

lock.unlock();

}

}

fork/join框架

fork/join框架是jdk 1.7推出的新特性,用於一個並行執行的任務框架

tips:並行與併發

併發的實質是一個物理CPU(也可以多個物理CPU) 在若干道程序之間多路複用,併發性是對有限物理資源強制行使多用戶共享以提高效率。

並行性指兩個或兩個以上事件或活動在同一時刻發生。在多道程序環境下,並行性使多個程序同一時刻可在不同CPU上同時執行。

原理:fork是把一個大任務拆分成若干個小任務,join就是合併這些小任務得到的結果。最後得到這個大任務的結果.

工作竊取算法(working-stealing)

是某個線程從其他隊列裏獲取任務用來執行,應用場景:如某個大任務A拆分成多個互不依賴的子任務放入不同的隊列中,當某些隊列中的任務執行完畢,另外一些隊列中的任務還未執行完,於是執行完隊列的線程就會去執行其他還有任務的隊列的任務,(比如 線程x 發現隊列a中的任務沒有了,那麼就去執行隊列b中的任務)

這樣做的好處是;充分利用線程間並行計算,減少線程間競爭

這樣做的壞處是:某些情況下存在競爭。如任務隊列中只有一個任務。並且會消耗更多的系統資源,比如創建隊列,創建線程池

注意點:爲了減少競爭,通常採用雙端隊列,竊取方式爲從隊尾獲取。正常線程從頭部獲取

fork/join 主要有兩個工作步驟

1)分割任務

2)執行任務併合並結果

RecursiveAction 用於執行沒有返回結果的任務

RecursiveTask 用於執行有返回結果的任務

forkjointask 需用由forkjoinpool來執行

使用方法:

首先繼承RecursiveTask 通過重寫compute方法來拆分任務並且fork執行,join合併

//main方法使用

public static void main(String[] args){

//創建線程池

ForkJoinPool pool = new ForkJoinPool();

//創建主任務

CountTask task = new CountTask();

//執行任務

Future<Integer> result = pool.submit(task);

system.out.println(result.get());

}

異常處理代碼

//時否是異常處理完成

if(task.isCompletedAbnormally()){

Sysmte.out.println(task.getException());

FORK/JOIN框架的實現原理

1)fork的實現原理

調用puttask放入任務,採用異步調用

public final forkJoinTask<V> fork(){

((ForkJoinWorkThread)Thread.currentThread).putTask(this);

}

2)putTask把當前任務放入任務數組中,然後調用ForkJoinPool的signalwork來喚醒或者創建一個新的線程

僞代碼如下

public final void pushTask(ForkJoinTask<> t){

ForkJoinTask <> [] q;int s,m;

if(q=queue)!=null){

//計算出偏移量

long u =( (s=queueTop)&(m=queueTop.length-1))<<ASHIFT+ABASE;

//根據偏移量直接刷新主內存

unsafe.putOrderObject(q,u,t);

queueTop=s+1;

if(s-=queueBase<=-2){

//喚醒工作線程

signalwork();

}else{

//創建新線程

growQueue();

}

}

}

1)join方法原理,首先查看任務時否執行完成,如果完成,則直接返回完成狀態

2)如果沒有完成,則從數組裏取出任務並行執行,如果正常,則返回normal如果異常則返回exception

如果正常則返回結果


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章