ConcurrentHashMap详解

1 并发安全的哈希

为什么需要ConcurrentHashMap

HashMap在多线程环境下非线程安全,而HashTable通多对方法使用Synchronized加以修饰从而达到线程安全,但是HashTable实现同步的方法比较暴力,相当于所有读写线程均去读取一把锁,效率比较低下。另外一种同步Map的方法是使用Collections工具类,例如:

Map<Integer,Integer>map=Collections.synchronizedMap(newHashMap<>());

该种方法与HashTable实现方式类似,也是通过锁住整表来实现同步的。而ConcurrentHashMap则避免了上述两种Map同步方式锁住全表的问题。在JDK1.7中,通过引入分段锁Segment(实现了ReentrantLock),保证了一定粒度的并行;在JDK1.8中则抛弃Segment的设计理念,将粒度完全降低到桶数量,基于CAS与Synchronized。

ConcurrentHashMap的基本设计思想

ConcurrentHashMap和HashMap实现上类似,最主要的差别是ConcurrentHashMap 采用了分段锁(Segment),每个分段锁维护着几个桶(HashEntry),多个线程可以同时访问不同分段锁上的桶,从而使其并发度更高(并发度就是Segment的个数)。其中Segment继承自ReentrantLock。默认的并发级别为16,也就是说默认创建16个Segment。JDK1.7使用分段锁机制来实现并发更新操作,核心类为Segment,它继承自重入锁ReentrantLock,并发度与Segment数量相等。JDK1.8使用了CAS操作来支持更高的并发度,在CAS操作失败时使用内置锁synchronized。并且 JDK 1.8 的实现也在链表过长时会转换为红黑树。

1.2 JDK7的ConcurrentHashMap

基本结构


Segment

Segment是JDK1.7中ConcurrentHashMap的核心设计,通过引入分段达成提高并行处理度的效果。易于看出,Segment继承了ReentrantLock并实现了序列化接口,说明Segment的锁是可重入的。

staticfinalclassSegment<K,V>extendsReentrantLockimplementsSerializable{

transientvolatileintcount;// Segment中元素的数量,由volatile修饰,支持内存可见性

transientintmodCount;// 对table的大小造成影响的操作的数量(比如put或者remove操作)

transientintthreshold;// 扩容阈值

transientvolatileHashEntry<K,V>[]table;// 链表数组,数组中的每一个元素代表了一个链表的头部

finalfloatloadFactor;// 负载因子

}

HashEntry

Segment中的元素是以HashEntry的形式存放在链表数组中的,其结构与普通HashMap的HashEntry基本一致,不同的是Segment的HashEntry,其value由volatile修饰,以支持内存可见性,即写操作对其他读线程即时可见。

staticfinalclassHashEntry<K,V>{

finalKkey;

finalinthash;

volatileVvalue;

finalHashEntry<K,V>next;

}

值得注意的是,key,hash值与next域都是由final修饰的,无法修改其引用。

初始化过程

下面以一个ConcurrentHashMap构造函数为例进行分析:

publicConcurrentHashMap(intinitialCapacity,

floatloadFactor,intconcurrencyLevel) {

if(!(loadFactor>0)||initialCapacity<0||concurrencyLevel<=0)

thrownewIllegalArgumentException();

if(concurrencyLevel>MAX_SEGMENTS)

concurrencyLevel=MAX_SEGMENTS;

// 根据传入的concurrencyLevel更新sshift与ssize

// 如concurrencyLevel=5 则天花板方向上离2^3=8最近,则sshift=3,ssize=8

intsshift=0;

intssize=1;

while(ssize<concurrencyLevel) {

++sshift;

ssize<<=1;

   }

// segmentShift和segmentMask元素定位Segment下标

segmentShift=32-sshift;// 计算Segment下标时首先令hash值的位数

segmentMask=ssize-1;// 计算Segment下标时随后求余数的操作数

// 按照ssize的大小对segment数组进行初始化

this.segments=Segment.newArray(ssize);

if(initialCapacity>MAXIMUM_CAPACITY)

initialCapacity=MAXIMUM_CAPACITY;

intc=initialCapacity/ssize;

// 若initialCapacity / ssize不整除,则将c=c+1

if(c*ssize<initialCapacity)

++c;

intcap=1;

// cap为每个segment的初始容量,其值为离c天花板方向最近的2^n

// 例:c为5,cap则为2^3=8;c为12,cap则为2^4=16

while(cap<c)

cap<<=1;

// 创建Segment

for(inti=0;i<this.segments.length;++i)

this.segments[i]=newSegment<K,V>(cap,loadFactor);

}

get方法

根据key获取value时,由于1.7中需要两次Hash过程,第一次需要定位到Segment;第二次需要定位到Segment中的桶下标。

publicVget(Objectkey) {

// 首先计算key的hash值

inthash=hash(key.hashCode());

// segmentFor(hash): 定位到key在哪个segment

// 调用segment的get(key, hash)获取到指定key的value

returnsegmentFor(hash).get(key,hash);

}

finalSegment<K,V>segmentFor(inthash) {

returnsegments[(hash>>>segmentShift)&segmentMask];

}

Vget(Objectkey,inthash) {

if(count!=0) {// read-volatile

// 取得链表的头部,就是第2次hash过程

HashEntry<K,V>e=getFirst(hash);

// 链表搜索,直到hash与key均相等时,返回节点的value

while(e!=null) {

if(e.hash==hash&&key.equals(e.key)) {

Vv=e.value;

if(v!=null)

returnv;

returnreadValueUnderLock(e);// recheck

           }

e=e.next;

       }

   }

returnnull;

}

HashEntry<K,V>getFirst(inthash) {

// 获取Segment的数组结构

HashEntry<K,V>[]tab=table;

// 第2次hash过程,确定key位于哪一个HashEntry

returntab[hash&(tab.length-1)];

}

在第二次查找具体元素时,首先对count做了非零判断,由于count是volatile修饰的,put、remove等操作会更新count的值,所以当竞争发生的时候,volatile的语义可以保证写操作在读操作之前,也就保证了写操作对后续的读操作都是可见的,这样后面get的后续操作就可以拿到完整的元素内容。

put方法

put操作也涉及2次hash定位过程,但是比get操作多了是否扩容、rehash等过程,2次哈希在此不做过多赘述。

Vput(Kkey,inthash,Vvalue,booleanonlyIfAbsent) {

// 先加锁

lock();

try{

intc=count;

// 对c进行+1操作,获取新的数组容量

// 如果新的数组容量高于阈值,则先进行扩容操作

if(c++>threshold)// ensure capacity

rehash();

// 获取Segment的数组table

HashEntry<K,V>[]tab=table;

// 2次hash过程确定桶下标

intindex=hash&(tab.length-1);

// 获取index位置的头结点

HashEntry<K,V>first=tab[index];

HashEntry<K,V>e=first;

// 沿链表遍历,直到找到与元素key或者hash值相同的节点

while(e!=null&&(e.hash!=hash||!key.equals(e.key)))

e=e.next;

VoldValue;

// 若key或者hash值相同的节点存在,则进行更新操作

if(e!=null) {

// value也是volatile修饰的,所以内存即时可见

oldValue=e.value;

if(!onlyIfAbsent)

e.value=value;

       }

// 若key或者hash值相同的节点不存在,则新建节点,追加到当前链表的头部(头插法)

else{

oldValue=null;

// 更新modCount的值,记录对table的大小造成影响的操作的数量

++modCount;

tab[index]=newHashEntry<K,V>(key,hash,first,value);

// 更新count的值,内存即时可见

count=c;// write-volatile

       }

// 返回旧值

returnoldValue;

}finally{

// 必须在finally中显示释放

unlock();

   }

}

remove方法

Vremove(Objectkey,inthash,Objectvalue) {

// 加锁,除了读取操作,其他操作均需要加锁

lock();

try{

// 计算新的Segment元素数量

intc=count-1;

// 获取Segment的数组table

HashEntry<K,V>[]tab=table;

// 第2次hash,确定在table的哪个位置

intindex=hash&(tab.length-1);

HashEntry<K,V>first=tab[index];

HashEntry<K,V>e=first;

// 沿链表遍历,直到找到与元素key或者hash值相同的节点

while(e!=null&&(e.hash!=hash||!key.equals(e.key)))

e=e.next;

VoldValue=null;

if(e!=null) {

Vv=e.value;

if(value==null||value.equals(v)) {

oldValue=v;

// 更新modCount值              

++modCount;

HashEntry<K,V>newFirst=e.next;

// 将待删除元素的前面的元素全部复制一遍,然后头插到链表上去

for(HashEntry<K,V>p=first;p!=e;p=p.next)

newFirst=newHashEntry<K,V>(p.key,p.hash,

newFirst,p.value);

tab[index]=newFirst;

// 更新新的Segment元素数量,内存即时可见

count=c;// write-volatile

           }

       }

// 返回旧值

returnoldValue;

}finally{

// 释放锁

unlock();

   }

}

由于,HashEntry中的next是final的,一经赋值以后就不可修改,在定位到待删除元素的位置以后,程序就将待删除元素前面的那一些元素全部复制一遍,然后再一个一个重新接到链表上去。使用final保证其不变性从而易于并发读取,但是当修改时,其成本就显得有些高了。

1.3 JDK8的ConcurrentHashMap

基本结构

http://ww1.sinaimg.cn/large/005L0VzSly1g7nehxnf22j30hy0fyab5.jpg在JDK1.8的ConcurrentHashMap中,完全摒弃1.7中segment的概念,保持与1.8HashMap一致的设计,通过引入CAS与Synchronized来保证最大粒度的并行。

重要全局变量

staticfinalintMOVED=-1;// 表示正在转移

staticfinalintTREEBIN=-2;// 表示已经转换成树

staticfinalintRESERVED=-3;// hash for transient reservations

staticfinalintHASH_BITS=0x7fffffff;// usable bits of normal node hash

transientvolatileNode<K,V>[]table;//默认没初始化的数组,用来保存元素

privatetransientvolatileNode<K,V>[]nextTable;//转移的时候用的数组

/**

* 用来控制表初始化和扩容的,默认值为0,当在初始化的时候指定了大小,这会将这个大小保存在sizeCtl中,大小为数组的0.75

* 当为负的时候,说明表正在初始化或扩张,

*     -1 表示初始化  

*     -(1+n) n:表示活动的扩张线程

*/

privatetransientvolatileintsizeCtl;

Node内部类

staticclassNode<K,V>implementsMap.Entry<K,V>{

finalinthash;//key的hash值

finalKkey;//key

volatileVval;//value

volatileNode<K,V>next;//表示链表中的下一个节点

Node(inthash,Kkey,Vval,Node<K,V>next) {

this.hash=hash;

this.key=key;

this.val=val;

this.next=next;

   }

publicfinalKgetKey()       {returnkey; }

publicfinalVgetValue()     {returnval; }

publicfinalinthashCode()   {returnkey.hashCode()^val.hashCode(); }

}

TreeNode内部类

staticfinalclassTreeNode<K,V>extendsNode<K,V>{

TreeNode<K,V>parent;// red-black tree links

TreeNode<K,V>left;

TreeNode<K,V>right;

TreeNode<K,V>prev;// needed to unlink next upon deletion

booleanred;

TreeNode(inthash,Kkey,Vval,Node<K,V>next,

TreeNode<K,V>parent) {

super(hash,key,val,next);

this.parent=parent;

   }

}

TreeBin内部类

staticfinalclassTreeBin<K,V>extendsNode<K,V>{

TreeNode<K,V>root;

volatileTreeNode<K,V>first;

volatileThreadwaiter;

volatileintlockState;

// values for lockState

staticfinalintWRITER=1;// set while holding write lock

staticfinalintWAITER=2;// set when waiting for write lock

staticfinalintREADER=4;// increment value for setting read lock

}

初始化方法

初始化方法均采用延迟初始化的形式。

publicConcurrentHashMapDebug(intinitialCapacity) {

if(initialCapacity<0)

thrownewIllegalArgumentException();

intcap=((initialCapacity>=(MAXIMUM_CAPACITY>>>1))?

MAXIMUM_CAPACITY:

tableSizeFor(initialCapacity+(initialCapacity>>>1)+1));

this.sizeCtl=cap;

}

put方法

先判断key与value是否为空。与HashMap不同,ConcurrentHashMap不允许null作为key或value,为什么这样设计? 因为ConcurrentHashmap是支持并发,这样会有一个问题,当你通过get(k)获取对应的value时,如果获取到的是null时,你无法判断,它是put(k,v)的时候value为null,还是这个key从来没有做过映射。HashMap是非并发的,可以通过contains(key)来做这个判断。而支持并发的Map在调用m.contains(key)和m.get(key),可能已经不同了;

计算hash值来确定放在数组的哪个位置

判断当前table是否为空,空的话初始化table

根据重哈希算出的值通过与运算得到桶索引,利用Unsafe类直接获取内存内存中对应位置上的节点,若没有碰撞即桶中无结点CAS直接添加

如果取出来的节点的hash值是MOVED(-1)的话,则表示当前正在对这个数组进行扩容,复制到新的数组,则当前线程也去帮助复制

最后一种情况就是,如果这个节点,不为空,也不在扩容,则通过synchronized来加锁,进行添加操作

其他部分同HashMap中的操作

finalVputVal(Kkey,Vvalue,booleanonlyIfAbsent) {

if(key==null||value==null)thrownewNullPointerException();//K,V都不能为空,否则的话跑出异常

inthash=spread(key.hashCode());//取得key的hash值

intbinCount=0;//用来计算在这个节点总共有多少个元素,用来控制扩容或者转移为树

for(Node<K,V>[]tab=table;;) {//

Node<K,V>f;intn,i,fh;

if(tab==null||(n=tab.length)==0)

tab=initTable();//第一次put的时候table没有初始化,则初始化table

elseif((f=tabAt(tab,i=(n-1)&hash))==null) {//通过哈希计算出一个表中的位置因为n是数组的长度,所以(n-1)&hash肯定不会出现数组越界

if(casTabAt(tab,i,null,//如果这个位置没有元素的话,则通过cas的方式尝试添加,注意这个时候是没有加锁的

newNode<K,V>(hash,key,value,null)))//创建一个Node添加到数组中区,null表示的是下一个节点为空

break;// no lock when adding to empty bin

       }

/*

* 如果检测到某个节点的hash值是MOVED,则表示正在进行数组扩张的数据复制阶段,

* 则当前线程也会参与去复制,通过允许多线程复制的功能,一次来减少数组的复制所带来的性能损失

*/

elseif((fh=f.hash)==MOVED)

tab=helpTransfer(tab,f);

else{

/*

* 如果在这个位置有元素的话,就采用synchronized的方式加锁,

*     如果是链表的话(hash大于0),就对这个链表的所有元素进行遍历,

*         如果找到了key和key的hash值都一样的节点,则把它的值替换到

*         如果没找到的话,则添加在链表的最后面

*  否则,是树的话,则调用putTreeVal方法添加到树中去

*  

*  在添加完之后,会对该节点上关联的的数目进行判断,

*  如果在8个以上的话,则会调用treeifyBin方法,来尝试转化为树,或者是扩容

*/

VoldVal=null;

synchronized(f) {

if(tabAt(tab,i)==f) {//再次取出要存储的位置的元素,跟前面取出来的比较

if(fh>=0) {//取出来的元素的hash值大于0,当转换为树之后,hash值为-2

binCount=1;

for(Node<K,V>e=f;;++binCount) {//遍历这个链表

Kek;

if(e.hash==hash&&//要存的元素的hash,key跟要存储的位置的节点的相同的时候,替换掉该节点的value即可

((ek=e.key)==key||

(ek!=null&&key.equals(ek)))) {

oldVal=e.val;

if(!onlyIfAbsent)//当使用putIfAbsent的时候,只有在这个key没有设置值得时候才设置

e.val=value;

break;

                           }

Node<K,V>pred=e;

if((e=e.next)==null) {//如果不是同样的hash,同样的key的时候,则判断该节点的下一个节点是否为空,

pred.next=newNode<K,V>(hash,key,//为空的话把这个要加入的节点设置为当前节点的下一个节点

value,null);

break;

                           }

                       }

                   }

elseif(finstanceofTreeBin) {//表示已经转化成红黑树类型了

Node<K,V>p;

binCount=2;

if((p=((TreeBin<K,V>)f).putTreeVal(hash,key,//调用putTreeVal方法,将该元素添加到树中去

value))!=null) {

oldVal=p.val;

if(!onlyIfAbsent)

p.val=value;

                       }

                   }

               }

           }

if(binCount!=0) {

if(binCount>=TREEIFY_THRESHOLD)//当在同一个节点的数目达到8个的时候,则扩张数组或将给节点的数据转为tree

treeifyBin(tab,i);

if(oldVal!=null)

returnoldVal;

break;

           }

       }

   }

addCount(1L,binCount);//计数

returnnull;

}

关于put中对CAS和synchronized的使用:

CAS用于当桶为空时,使用cas尝试加入新的桶头结点

synchronized用于桶不为空时,向链表或树中put结点的情形

get方法

当key为null的时候回抛出NullPointerException的异常

get操作通过首先计算key的hash值来确定该元素放在数组的哪个位置

判断table是否为空且table长度大于0且下标不为空

然后遍历该位置的所有节点

如果均无法定位到key则返回null

publicVget(Objectkey) {

Node<K,V>[]tab;Node<K,V>e,p;intn,eh;Kek;

inth=spread(key.hashCode());

if((tab=table)!=null&&(n=tab.length)>0&&

(e=tabAt(tab, (n-1)&h))!=null) {

if((eh=e.hash)==h) {

if((ek=e.key)==key||(ek!=null&&key.equals(ek)))

returne.val;

     }

elseif(eh<0)

return(p=e.find(h,key))!=null?p.val:null;

while((e=e.next)!=null) {

if(e.hash==h&&

((ek=e.key)==key||(ek!=null&&key.equals(ek))))

returne.val;

     }

  }

returnnull;

}

resize方法

当需要扩容的时候,调用的时候tryPresize方法。在tryPresize方法中,并没有加锁,允许多个线程进入,如果数组正在扩张,则当前线程也去帮助扩容。值得注意的是,复制之后的新链表不是旧链表的绝对倒序;在扩容的时候每个线程都有处理的步长,最少为16,在这个步长范围内的数组节点只有自己一个线程来处理。整个操作是在持有段锁的情况下执行。

privatefinalvoidtryPresize(intsize) {

intc=(size>=(MAXIMUM_CAPACITY>>>1))?MAXIMUM_CAPACITY:

tableSizeFor(size+(size>>>1)+1);

intsc;

while((sc=sizeCtl)>=0) {

Node<K,V>[]tab=table;intn;

/*

* 如果数组table还没有被初始化,则初始化一个大小为sizeCtrl和刚刚算出来的c中较大的一个大小的数组

* 初始化的时候,设置sizeCtrl为-1,初始化完成之后把sizeCtrl设置为数组长度的3/4

* 为什么要在扩张的地方来初始化数组呢?这是因为如果第一次put的时候不是put单个元素,

* 而是调用putAll方法直接put一个map的话,在putALl方法中没有调用initTable方法去初始化table,

* 而是直接调用了tryPresize方法,所以这里需要做一个是不是需要初始化table的判断

*/

if(tab==null||(n=tab.length)==0) {

n=(sc>c)?sc:c;

if(U.compareAndSwapInt(this,SIZECTL,sc,-1)) {//初始化tab的时候,把sizeCtl设为-1

try{

if(table==tab) {

@SuppressWarnings("unchecked")

Node<K,V>[]nt=(Node<K,V>[])newNode<?,?>[n];

table=nt;

sc=n-(n>>>2);

                   }

}finally{

sizeCtl=sc;

               }

           }

       }

/*

* 一直扩容到的c小于等于sizeCtl或者数组长度大于最大长度的时候,则退出

* 所以在一次扩容之后,不是原来长度的两倍,而是2的n次方倍

*/

elseif(c<=sc||n>=MAXIMUM_CAPACITY) {

break;//退出扩张

       }

elseif(tab==table) {

intrs=resizeStamp(n);

/*

* 如果正在扩容Table的话,则帮助扩容

* 否则的话,开始新的扩容

* 在transfer操作,将第一个参数的table中的元素,移动到第二个元素的table中去,

* 虽然此时第二个参数设置的是null,但是,在transfer方法中,当第二个参数为null的时候,

* 会创建一个两倍大小的table

*/

if(sc<0) {

Node<K,V>[]nt;

if((sc>>>RESIZE_STAMP_SHIFT)!=rs||sc==rs+1||

sc==rs+MAX_RESIZERS||(nt=nextTable)==null||

transferIndex<=0)

break;

/*

* transfer的线程数加一,该线程将进行transfer的帮忙

* 在transfer的时候,sc表示在transfer工作的线程数

*/

if(U.compareAndSwapInt(this,SIZECTL,sc,sc+1))

transfer(tab,nt);

           }

/*

* 没有在初始化或扩容,则开始扩容

*/

elseif(U.compareAndSwapInt(this,SIZECTL,sc,

(rs<<RESIZE_STAMP_SHIFT)+2)) {

transfer(tab,null);

           }

       }

   }

}

复制之后的新链表不是旧链表的绝对倒序

在扩容的时候每个线程都有处理的步长,最少为16,在这个步长范围内的数组节点只有自己一个线程来处理

remove方法

remove操作首先要确定需要删除的元素的位置,不过删除元素不是简单地把待删除元素的前面的一个元素的next指向后面一个域这么简单。由于HashEntry中的next是final的,一经赋值以后就不可修改引用。因此,在定位到待删除元素的位置以后,程序就将待删除元素前面的那一些元素全部复制一遍,然后再一个一个重新接到链表上去。这其实是由entry的不变性来决定的,除了value,其他所有属性都是用final来修饰的,这意味着在第一次设置了next域之后便不能再改变它,取而代之的是将它之前的节点全都克隆一次。至于entry为什么要设置为不变性,这跟不变性的访问不需要同步从而节省时间有关。

Vremove(Objectkey,inthash,Objectvalue) {

lock();

try{

intc=count-1;

HashEntry<K,V>[]tab=table;

intindex=hash&(tab.length-1);

HashEntry<K,V>first=tab[index];

HashEntry<K,V>e=first;

while(e!=null&&(e.hash!=hash||!key.equals(e.key)))

e=e.next;

VoldValue=null;

if(e!=null) {

Vv=e.value;

if(value==null||value.equals(v)) {

oldValue=v;

// All entries following removed node can stay

// in list, but all preceding ones need to be

// cloned.

++modCount;

HashEntry<K,V>newFirst=e.next;

for(HashEntry<K,V>p=first;p!=e;p=p.next)

newFirst=newHashEntry<K,V>(p.key,p.hash,

newFirst,p.value);

tab[index]=newFirst;

count=c;// write-volatile

           }

       }

returnoldValue;

}finally{

unlock();

   }

}

size方法

计算ConcurrentHashMap的元素大小是一个有趣的问题,因为是并发操作的,就是在你计算size的时候,他还在并发的插入数据,可能会导致你计算出来的size和你实际的size有相差(在你return size的时候,插入了多个数据),要解决这个问题,JDK1.7版本用两种方案:

第一种方案使用不加锁的模式去尝试多次计算ConcurrentHashMap的size,最多三次,比较前后两次计算的结果,结果一致就认为当前没有元素加入,计算的结果是准确的

第二种方案是如果第一种方案不符合,就会给每个Segment加上锁,然后计算ConcurrentHashMap的size返回

1.4 其他

ConcurrentHashMap的工作原理以及版本差异

ConcurrentHashMap为了提高本身的并发能力,在内部采用了一个叫做Segment的结构,一个Segment其实就是一个类HashTable的结构,Segment内部维护了一个链表数组。ConcurrentHashMap定位一个元素的过程需要进行两次Hash操作,第一次 Hash 定位到 Segment,第二次Hash定位到元素所在的链表的头部,因此,这一种结构的带来的副作用是Hash的过程要比普通的HashMap要长,但是带来的好处是写操作的时候可以只对元素所在的Segment进行操作即可,不会影响到其他的Segment,这样,在最理想的情况下,ConcurrentHashMap 可以最高同时支持 Segment 数量大小的写操作(刚好这些写操作都非常平均地分布在所有的 Segment上),所以,通过这一种结构,ConcurrentHashMap的并发能力可以大大的提高。JAVA7之前ConcurrentHashMap主要采用锁机制,在对某个Segment进行操作时,将该Segment锁定,不允许对其进行非查询操作,而在JAVA8之后采用CAS无锁算法,这种乐观操作在完成前进行判断,如果符合预期结果才给予执行,对并发操作提供良好的优化。而1.8中做了进一步的优化:

JDK1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点)

JDK1.8版本的数据结构变得更加简单,使得操作也更加清晰流畅,因为已经使用synchronized来进行同步,所以不需要分段锁的概念,也就不需要Segment这种数据结构了,由于粒度的降低,实现的复杂度也增加了

JDK1.8使用红黑树来优化链表,基于长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是很快的,代替一定阈值的链表,这样形成一个最佳拍档

JDK1.8为什么使用内置锁synchronized来代替重入锁ReentrantLock,我觉得有以下几点:因为粒度降低了,在相对而言的低粒度加锁方式,synchronized并不比ReentrantLock差,在粗粒度加锁中ReentrantLock可能通过Condition来控制各个低粒度的边界,更加的灵活,而在低粒度中,Condition的优势就没有了;在大量的数据操作下,对于JVM的内存压力,基于API的ReentrantLock会产生更多的内存开销。

ConcurrentHashMap中变量使用final和volatile修饰的作用

final可实现不变模式(immutable),是多线程安全里最简单的一种保障方式。不变模式主要通过final关键字来限定的。在JMM中final关键字还有特殊的语义。Final域使得确保初始化安全性(initialization safety)成为可能,初始化安全性让不可变形对象不需要同步就能自由地被访问和共享

使用volatile来保证某个变量内存的改变对其他线程即时可见,在配合CAS可以实现不加锁对并发操作的支持remove执行的开始就将table赋给一个局部变量tab,将tab依次复制出来,最后直到该删除位置,将指针指向下一个变量

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,128评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,316评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,737评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,283评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,384评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,458评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,467评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,251评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,688评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,980评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,155评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,818评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,492评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,142评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,382评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,020评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,044评论 2 352

推荐阅读更多精彩内容