记录:持续更新
jdk1.7
首先比较一下hashTable和ConcurrentHashMap:
1.hashTable它的每一个方法都是使用了synchronized同步锁机制,将整个入口锁起来,在多线程的情况下,他整个数组结构的入口就只能一条线程执行完成之后其他线程才能进入,无论下标是否相同,是否存在hash碰撞;
2.ConcurrentHashMap它内部使用了Cas+lock锁机制,锁的粒度更细化,在多线程的情况下,它的数组结构入口,每一个segment算成一个锁,并非整体加锁;
jdk1.7的构造方法:
put方法:
1.ConcurrentHashMap的key和value都不能为null
2.通过put进入的key获取到hash值,通过与算法获取到segment数组下标的位置
3.获取segment对应下标位置的对象,判断是否为空,如果为空,走对象的创建逻辑ensureSegment,否则走添加put的逻辑;
4.进入创建逻辑ensureSegment
1.获取到整个segments的数组
2.根据传入的j获取到数组上的下标值
3.判断数组下标位置是否存在Segment数据对象,因为在多线程的情况下,比如AB两个线程同时进入此处,如果A线程已经在这个下标位置创建出来了,那么B线程就没有必要走这段创建逻辑了,直接returen seg即可
4.进入此判断方法,因为在构造方法的时候,Segments数组的第一个位置就已经创建出来了一个segment对象,所以这个地方就直接拿第一个位置里面的信息,提高了效率,不然每一个创建都会去创建,浪费了资源;
5.构建出来一个Entry数组,因为每一个Segment的内部都会存在一个Engtry数组
6.继续判断此下标位置是否为空,这里也考虑到了多线程的情况下,A线程还在执行创建方法的时候,B线程就已经生成出来了
7.创建Segment对象,下方while循环继续判断此处下标位置是否非空,依然是考虑多线程的情况但是就算做再多的check也不一定能够控制并发问题,所以此处引入了CAS机制,若是多线程同时进入此方法,那么只会存在一个能够创建成功,这个地方while+CAS形成自旋锁,实则是让没有成功的线程赋值返回创建出来的segment
5.进入put方法:
1.
1.判断key和value是否非空
2.根据key过去到hash值,对应过去到segment数组的下标
3.判断segment下标位置的对象,是否为空,如果为空进入ensureSegment的创建逻辑,否则进入put的逻辑方法
2.如果Segment对应数组下标位置的对象为空就进入ensureSegment方法:
1.获取到Segment的数组
2.获取到下标值
3.定义一个Segment的对象
4.判断下标位置的Segment对象是否为空,这个位置的目的就是防止多线程的时候,A线程还在走逻辑,其他线程已经创建出来了
5.进入判断逻辑,将构造方法构造出来的Segment第一个位置的Segment对象的属性copey过来,此处体现了构造方法为什么要在segment数组的第一个位置初始化的时候就创建,因为多线程的情况下,无论segment数组任何位置,皆可copy,效率方面得到了提升,不然所有位置都得重新去创建一遍
6.再次判断segment数组对应此下标位置是否存在segmen,目的也是防止多线程的情况下,其他线程提前创建
7.new出一个segment对象
8.此处使用while循环加CAS组成自旋锁,循环中继续判断下标位置是否为空,然后使用cas轻量锁防止并发的情况,如果A线程创建了,B线程进入就不会在创建而是做赋值操作之后就返回
3.如果Segment对应数组下标位置的对象不为空就进入put方法:
1.如果segment数组对应下标位置不为空,则进入put方法
2.加锁
3.try内部:获取下标位置的Entry数组,计算Entry的下标,得到下标位置的Entry对象,也就是first第一个对象
4.遍历数组下的链表,如果下标位置存在Entry对象数据,那么就判断我们put进入的key是否相等,hash是否相等,如果相等则更新value且返回
1.else表示如果segmen数组下的Entry数组中的下标位置为空
2.新建一个Entry对象,count表示我们Entry数组上的节点有多少个
3.如果数组的长度小于 cup核数计算的方式,最大64,那么久进入扩容方法,否则就将新建的entry对象放入entry数组的第一个位置
进入加锁方法:
1.进入加锁方法,获取entry数组上的下标
2.设置重试需要的值retries
3.使用非阻塞锁 tryLock,如果没有获取到锁,就进入此判断
4.进入判断方法后:第一次的时候进入retries=-1,会创建一个entry的对象
5.如果遍历这个链表上存在put进入的key,就将retries设置为0
6.一直遍历这个链表
1.这个时候restries不等于0的时候,因为已经遍历完了,且重试次数已经大于MAX_SCAN_RETRIES的时候,就会调用lock方法,同步阻塞,然后关闭此线程
2.如果retries是偶数且entry数组的下标头节点是否等于刚刚进入此方法获取的头节点,目的就是,因为jdk1.7插入的时候是头插法,所以要看看有没有其他线程在此位置新增了Entry,若是其他线程已经新增,那么就将retries改为初始值-1
进入扩容方法:
1.进入扩容的方法
2.获取Entrys数组,获取老数组的长度,生成新数组,新数组的长度是老数组的两倍
3.遍历老数组的长度,获取老数组上每一个entry
4.判断老数组上每一个节点是否为空,如果不为空则进入此方法
5.进入判断方法后,获取这个数组的下一个节点,获取新数组下标位置
6.判断这个entry数组头节点的下一个节点是否为空,如果为空,直接copy到新数组中去
7.如果不为空,那么就将老数组的entry赋值给lastRun,将新数组对应的下标赋值给lastIdx
8.遍历链表,然后得出每一个链表中entry对应新数组的下标
9.如果每一个链表的下标不等于新数组中的entry数组中头节点下标,则进入判断方法,将老链表中的entry的下标更新成新数组的下标,将老数组的entry对象信息赋值给新数组的entry对象信息
10.将记录的新数组下标信息,赋值给新数组即可
就此,第一个循环结束,目的就是记录一些相同的下标的entry对象
第二个循环开始:
1.将老链表遍历取出之前不相同与新数组下标的,也就是还保留的记录
2.遍历将值获取出来,并在计算得出的新数组下标中创建一个entry对象即可
最后:
1.如果等于空的时候 将我们新增的数据,加载到新数组中去即可
扩容方法中两次for循环的意义图:
jdk1.7总结:
put方法:
1.计算出segment数组的下标,并判断下标是否存在数据
2.如果不存在,则进入ensureSegment创建对应Segment数组下的entry数组 此处通过CAS机制控制并发
3.如果存在,进入put方法,加锁,如果没有获取到锁,就需要去创建entry数组下的entry对象,此处用的是非阻塞锁tryLock()+Lock()+unLock() 使用lock()同步阻塞的目的是防止cpu飙高
4.如果put的key,value已经存在,那么就更新value
5.如果容量不够,就扩容,扩容的话新数组就是旧数组的两倍,然后采用了两个for循环去将老数组链表上的entry对象做隔离,然后复制到新数组
6.释放锁
查询get方法:
jdk1.8
进入put:
final V putVal(K key, V value, boolean onlyIfAbsent) {
//判断key,value是否为空
if (key == null || value == null) throw new NullPointerException();
//计算hash
int hash = spread(key.hashCode());
//统计
int binCount = 0;
//遍历数组
for (ConcurrentHashMap.Node<K,V>[] tab = table;;) {
//f数组上下标位置第一个节点对象
//i下标位置
//fh是f.hash
ConcurrentHashMap.Node<K,V> f; int n, i, fh;
//如果数组为nulL,那么就初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//如果数组头节点为null ,就新建一个node对象
if (casTabAt(tab, i, null,
new ConcurrentHashMap.Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
//进入扩容方法,此判断表示,如果A线程在这个节点发现存在其他线程对此线程正在扩容,那么就进入两条线程一起扩容,提高效率
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
//使用数下标中头节点作为锁对象
synchronized (f) {
if (tabAt(tab, i) == f) {
//此处表示链表
if (fh >= 0) {
binCount = 1;
for (ConcurrentHashMap.Node<K,V> e = f;; ++binCount) {
K ek;
//如果key已存在,赋值value
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
//如果不存在相同key,此处使用尾插法
ConcurrentHashMap.Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new ConcurrentHashMap.Node<K,V>(hash, key,
value, null);
break;
}
}
}
//此处表示红黑树
//jdk1.7第一个数组是用的segmen,没有使用到红黑树
//jdk8的hashmap的红黑树使用的是TreeNode节点,表示数节点
//jdk8此处使用的是红黑树,且数组中的头节点f如果是树的话,那么就只存在一个对象,就是TreeBin,这个对象内含
//数组链表等信息,而对比hashMap的TreeNode,加了一个树组织的外套,为什么?因为如果多线程的情况下,我们jdk8使用的是根节点去做的锁对象,
//又因为红黑树有一个自平衡原理,所以不能让他将根节点自平衡掉,才加了TreeBin这个外套
else if (f instanceof ConcurrentHashMap.TreeBin) {
ConcurrentHashMap.Node<K,V> p;
binCount = 2;
if ((p = ((ConcurrentHashMap.TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
//如果数组下标节点的数据不为0,进入
//如果数据大于了8那么就生成红黑树,这里需要结合上面的判断循环走
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
//相当于size++
addCount(1L, binCount);
return null;
}
初始化方法:
//如果数组上为空进入初始化
private final ConcurrentHashMap.Node<K,V>[] initTable() {
ConcurrentHashMap.Node<K,V>[] tab; int sc;
//如果数组为空进入自旋
while ((tab = table) == null || tab.length == 0) {
//sc默认等于0,CAS操作会减1,然后初始化成功会赋值0.75
if ((sc = sizeCtl) < 0
//释放资源
Thread.yield(); // lost initialization race; just spin
//在多线程的情况下,进入CAS这个方法只会存在一个返回true的,
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
//再次判断,防止其他线程已经初始化
if ((tab = table) == null || tab.length == 0) {
//初始化大小16
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
ConcurrentHashMap.Node<K,V>[] nt = (ConcurrentHashMap.Node<K,V>[])new ConcurrentHashMap.Node<?,?>[n];
table = tab = nt;
//加载因子0.75
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
进入acount方法:
private final void addCount(long x, int check) {
//定义一个数组CounterCell
ConcurrentHashMap.CounterCell[] as; long b, s;
//如果counterCells不为空,或者Cas+1失败
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
ConcurrentHashMap.CounterCell a; long v; int m;
boolean uncontended = true;
//如果数组为空,或者CELLVALUE+1失败
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
//将这个CounterCell数组下标中的value+1 fullAddCount内部循环涉及到CounterCell数组扩容
// uncontended如果进入这个方法,这个变量值默认传true
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
//统计
s = sumCount();
}
if (check >= 0) {
ConcurrentHashMap.Node<K,V>[] tab, nt; int n, sc;
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
if (sc < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}
jdk8持续更新:
总结jdk1.7和1.8的区别:
jdk1.7:
1.数据结构:ReentrantLock+Segment+HashEntry+CAS组成,Segment中内含了着HashEntry数组,每个HashEntry都是一个链表结构
如何保证线程安全:segment数组每一个下标位置就是一把锁,首先使用一系列的校验,然后使用Cas配合循环实现自旋锁+TryLock(非阻塞锁)+Lock(阻塞锁)将来保证线程安全,
2.扩容结构:扩容的时候并非是扩容Segment数组,而是扩容Entry数组,每次扩容都是两倍;
扩容的逻辑:首先new出一个等于两倍旧数组的新数组,将segment数组下的Entry数组中下标位置计算得出,如果非链表,就计算出新数组下标,将旧数组下标位置的头节点数据迁移到新数组即可,如果是链表,就执行两次for血循环,第一次for循环计算并记录位于新数组相同下标位置的Entry对象,然后迁移,第二次for循环将剩余Entry对象计算相对应的新数组的下标位置,迁移数据即可
扩容的 触点就是老数组table的长度小于内置的cpu核数计算方式,最大64,最小1
3.元素查询:两次hash,第一次定位到Segment数组,第二次hash定位到Segment下的Entry数组,通过循环遍历匹配key和hash值得出结果并返回
get方法不用加锁,volatile保证
4.并发的可靠性,tryLock+Lock+CAS
jdk1.8: 加粗属于重点
1.数据结构:synchronized+CAS+Node+链表+treeBin=红黑树,
Node的val和next都用volatile修饰,保证了可见性,查找替换赋值操作都使用了CAS;
2.如何保证线程安全:在put方法的时候使用了Unsafe+CAS+synchronized同步锁机制,保证了多线程情况下的线程安全下性问题
其中Unsafe操作和jdk7的操作类似,主要负责并发安全的修改对象的某个属性或值,synchronized主要负责再需要操作某个下标位置的时候加锁(该位置不为空),比如向某个链表或者某个红黑树进行插入操作的时候;
3.简化put:
1.首先计算key对应的数组下标,如果该位置没有元素,那么通过自旋的方法向该位置赋值
2.如果有元素,那么就synchronized加锁
3.加锁成功后走判断逻辑:
a:如果是链表节点则进行添加节点到链表中
b:如果是红黑树则添加节点到树中
4.添加成功后判断是否需要树化
5.进入addCount方法,此方法的意思是ConcurrentHashMap的元素个数加1,但是这个操作需要并发安全的控制,并且个数+1成功后需要继续判断是否需要扩容,如果需要就会进行扩容
6.再扩容的同时,其他线程进入put的时候就会发现此数组有节点正在扩容,就会帮助去扩容;
4.锁:锁链表的head节点,不影响其他元素的读写,锁粒度更细,效率更高,扩容时,阻塞所有的读写操作,并发扩容
5.读操作无锁:
Node节点的val和next使用了voiatile修饰,读写线程对该变量互相可见,数组用volatile修饰,保证扩容时被线程感知;
总结:
put逻辑:计算hash值,第一个条件判断数组是否存在,不存在就初始化(此处使用CAS机制保证了线程安全性),第二个条件如果存在就查看数组下标是否存在数据,不存在就使用CAS保证线程安全的情况下创建Node对象,前面条件都不满足的情况下,进入第三个条件判断关于扩容的,如果数组下标存在fwd标记的那么就进入扩容方法,帮助扩容(这是多线程情况下),当然这里有一个点要注意的是:当被标记fwd的下标位置,表示正在做扩容,就会阻塞其他线程的读写操作;第四个判断条件加锁,如果是链表就遍历,判断是否已存在,已存在就更新value,不存在,就尾插法插入,内部并行判断是否是红黑树,如果是就TreeBin也是遍历红黑树,然后如果存在就更新value,如果不存在就添加,此处需要注意,如果是树组织,那么数组头节点存入的就是TreeBin,内部做的遍历;第五个判断条件,如果是链表的情况,数量大于了8,就会将链表更改成红黑树,最后在addCount,在此方法中就会去判断是否需要扩容,扩容逻辑如下
扩容的逻辑:首先new出一个大于老数组2倍的新数组,然后计算出老数组中的bound步长范围,锁定索引下标位置,使用自旋锁做一系列循环,比如此时步长为2,那么就是从右开始迁移数据,在开始迁移数据之前就会对这个数组下标加锁并做上fwd标记,使得其他线程无法在此标记中做读写操作,从数组的右开始,一直计算到索引值为0的时候才结束,当然如果在中途的时候,其他线程进入,也可以帮助做扩容,如果线程A从8计算到6,那么此时B线程加入进来,就是从6开始到4的迁移;
持续更新jdk1.8源码: