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
如果正常則返回結果