序言
由于最近项目上遇到了高并发问题,而自己对高并发,多线程这里的知识点相对薄弱,尤其是基础,所以想系统的学习一下,以后可能会出一系列的JUC文章及总结 ,同时也为企业级的高并发项目做好准备。
本文是JUC文章的第三篇,如想看以往关于JUC文章,请点击JUC系列总结
此系列文章的总结思路大致分为三部分:
- 理论(概念);
- 实践(代码证明);
- 总结(心得及适用场景)。
在这里提前说也是为了防止大家看着看着就迷路了。
备注:本文的阅读需要Volatile、CAS、Synchronized以及集合原理部分知识阅读最佳。
集合不安全问题大纲
ArrayList
我们都知道ArrayList是线程不安全,但是具体的不安全体现在哪?或者说你用代码去证明他的不安全性以及他的解决方案。
不安全原因
ArrayList是非线性安全,具体现在多线程环境下,对集合的操作:
- 一方线程在遍历列表,另一方线程在修改列表时,会报ConcurrentModificationException。
- 多线程插入操作,即add方法,由于没有同步操作,容易丢失数据,同时也可能出现索引越界异常(ArrayIndexOfBoundsException)。
public boolean add(E e) {
ensureCapacity(size + 1); // Increments modCount!!
elementData[size++] = e;//使用了size++操作,会产生多线程数据丢失问题。
return true;
}
而对于多线程操作问题,其最本质的问题就是通过锁来解决。大致分为3种解决方案:
- 使用
Vector
(ArrayList
所有方法加synchronized
,锁作用范围为方法,比较重)。 - 使用
Collections.synchronizedList()
转换成线程安全类(锁作用范围为代码块)。 - 使用
java.concurrent.CopyOnWriteArrayList
(分场景使用)。
参考连接:为什么说ArrayList是线程不安全的?
其实就两点:
- 在判断是否需要扩容的时候,多线程环境下会对其造成影响导致误判没有扩容,接而导致索引越界;
- 在进行下一步elementData[s]=e,s=s+1的时候,如果多个线程在elementData[s]=e造成值覆盖,就会造成数据丢失问题,也是为什么集合添加值得时候出现null;
代码证明不安全问题
并发修改异常证明
public class ArrayListNoSafe {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < 8; i++) {
new Thread(() ->{
list.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(Thread.currentThread().getName() + "\t" + list);
},String.valueOf(i)).start();
}
}
}
出现了边遍历,边修改的情况,结果就可能如下(抛出ConcurrentModificationException异常):
1 [3f790bed, b0566629]
7 [3f790bed, b0566629, 74fa619f, 1d4d9f94, d125b6c6, 0c2dee3f, ae9c6ce2, edb884c7]
4 [3f790bed, b0566629, 74fa619f, 1d4d9f94, d125b6c6, 0c2dee3f, ae9c6ce2]
5 [3f790bed, b0566629, 74fa619f, 1d4d9f94, d125b6c6, 0c2dee3f]
2 [3f790bed, b0566629, 74fa619f, 1d4d9f94, d125b6c6]
6 [3f790bed, b0566629, 74fa619f]
3 [3f790bed, b0566629]
Exception in thread "0" java.util.ConcurrentModificationException
...省略
多线程环境下的添加操作,出现数据遗漏(其实最本质的问题就是size++)
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
new Thread(() ->{
list.add(UUID.randomUUID().toString().substring(0,8));
},String.valueOf(i)).start();
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.err.println("=====长度:"+list.size());
}
输出结果:
=====长度:9997
其实不止会出现数据遗漏情况,有时候还会出现索引越界情况 ,原因是因为:
假如当前集合里面有9个元素,线程A,B经过add方法之后会出现11个元素,list在第10个元素会发生扩容。
- 线程A,B同时进入add方法,在判断是否要扩容时候,由于线程A,B是同时进入,导致误判没有扩容(以为只添加一个元素,此时size=9),此时假如线程A被挂起;
- 线程B进入add方法,并完成了整个流程,但由于数组未扩容,但是索引已+1(size=10),线程A被唤醒,执行elementData[s] = e,s代表下表,集合的下表默认减一,但现在已经是10了,所以会抛出索引越界异常;
输出结果:
Exception in thread "4012" java.lang.ArrayIndexOutOfBoundsException: 4164
at java.util.ArrayList.add(ArrayList.java:463)
at com.company.thread.collection.ArrayListNoSafe.lambda$main$0(ArrayListNoSafe.java:31)
at java.lang.Thread.run(Thread.java:748)
=====长度:9992
解决方案1 Vector
Vector之所以可以保证线程安全,是因为它对所有操作都加上了synchronized关键字(注意:它锁定的范围时方法级别的),将整个方法都锁住。但它的本质还是ArrayList,就是简单粗暴的加上了synchronized,这种方式严重影响效率,而且在拓展性方面也不如Collections.SynchronizedList,仅作用于ArrayList。因此,不推荐使用Vector。
Stackoverflow当中有这样的描述:Why is Java Vector class considered obsolete or deprecated?。
但是作为ArrayList不安全的解决方式中,我们还是有必要说一下。代码也很简单,我们在这就不演示了。
解决方案2 Collections.SynchronizedList
与Vector相同的是它也加了synchronized关键字,但是最大不同的是它的synchronized锁定的范围是代码块,锁定的是构造函数传进来的list对象。(不仅限于ArrayList)
我们来翻阅一下源码:
publicstatic <T> List<T> synchronizedList(List<T> list) {
return (list instanceof RandomAccess ?
//ArrayList使用了SynchronizedRandomAccessList类
new SynchronizedRandomAccessList<>(list) :
new SynchronizedList<>(list));
}
通过源码可知,它其实就是通过 synchronizedList
静态方法将一个非线程安全的List(并不仅限ArrayList)包装为线程安全的List。
SynchronizedList
方法如下:
//SynchronizedRandomAccessList继承自SynchronizedList
static class SynchronizedRandomAccessList<E> extends SynchronizedList<E> implements RandomAccess {
}
//SynchronizedList对代码块进行了synchronized修饰来实现线程安全性
static class SynchronizedList<E> extends SynchronizedCollection<E> implements List<E> {
final List<E> list;
SynchronizedList(List<E> list) {
super(list);
this.list = list;
}
SynchronizedList(List<E> list, Object mutex) {
super(list, mutex);
this.list = list;
}
public E get(int index) {
synchronized (mutex) {return list.get(index);}
}
public E set(int index, E element) {
synchronized (mutex) {return list.set(index, element);}
}
public void add(int index, E element) {
synchronized (mutex) {list.add(index, element);}
}
public E remove(int index) {
synchronized (mutex) {return list.remove(index);}
}
//迭代操作并未加锁,所以需要手动同步
public ListIterator<E> listIterator() {
return list.listIterator();
}
}
Collections.synchronizedList
生成了特定同步的SynchronizedCollection
,生成的集合每个同步操作都是持有mutex
这个锁,而这个mutex即构造函数传入的list,所以再进行操作时就是线程安全的集合了。
注意:这里需要注意一个地方:
- 迭代操作必须加锁,可以使用
synchronized
关键字修饰; - synchronized持有的监视器对象必须是
synchronized (list)
,即包装后的list,使用其他对象如synchronized (new Object())
会使add
,remove
等方法与迭代方法使用的锁不一致,无法实现完全的线程安全性。
解决ArrayList不安全的代码:
public static void main(String[] args) {
collectionsSynchronizedListTest();
}
//collection.synchronizedList安全性测试
private static void collectionsSynchronizedListTest() {
List<String> list = Collections.synchronizedList(new ArrayList<String>());
for (int i = 0; i < 80; i++) {
new Thread(() ->{
list.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(Thread.currentThread().getName()+"\t"+list);
},String.valueOf(i)).start();
}
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("list的长度====================="+list.size());
collectionsSynchronizedListIteratorTest(list);
}
//collections.synchronizedList的遍历
private static void collectionsSynchronizedListIteratorTest(List list) {
synchronized (list){
Iterator iterator = list.iterator();
while (iterator.hasNext()){
System.err.print(iterator.next() + ",");
}
}
}
在这里提醒一下,增强for循坏其实底层也是调用的迭代器…
解决方案3 copyOnWriteArrayList
顾名思义,CopyOnWrite~ArrayList,写时复制,读写分离。即在进行写操作(add,remove,set等)时会进行Copy操作,可以推测出在进行写操作时CopyOnWriteArrayList
性能应该不会很高,从而进一步可以推出它是适合在读多写少的情况下使用。
再者,我们可以发现这个类的所在包为 java.util.concurrent
,可想而知,这个类是为并发而设计的.
为了防止看源码看的迷路,在这里先提前总结一下其原理:
通过写时复制来实现读写分离。比如其add()
方法,就是先复制一个新数组,长度为原数组长度+1,然后将新数组最后一个元素设为添加的元素。而读操作(读到的是修改前的数组)并没有对数组修改,不会产生线程安全问题。但同时也会带来一个新的问题,就是数据的一致性。
线程1读取集合里面的数据,然后被挂起,线程2、线程3、线程4四个线程都修改/删除了CopyOnWriteArrayList里面的数据,操作完成后此时线程1被唤醒(由于太快了,volatile还未及时刷新主内存的值),线程1拿到的还是最老的那个Object[] array,此时如果 get(i)获取指定位置时,可能会造成索引越界异常。所以线程1读取的内容未必准确。
所以结论为:在不要求数据实时一致性的情况下,读不加锁,写加锁,使用copyOnWriteArrayList以提高性能。
接下来,我们来看一下它的结构:
public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
//lock锁
final transient ReentrantLock lock = new ReentrantLock();
//volatile保证可见性,一旦有线程修改,即可见。
private transient volatile Object[] array;
final Object[] getArray() {
return array;
}
final void setArray(Object[] a) {
array = a;
}
//构造方法,赋予初始值
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
}
可以看到CopyOnWriteArrayList
底层跟ArrayList一样,实现同为Object[] array
数组。
添加操作
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
//在原先数组基础之上新建长度+1的数组,并将原先数组当中的内容拷贝到新数组当中。
Object[] newElements = Arrays.copyOf(elements, len + 1);
//给最后一个元素设值
newElements[len] = e;
//对新数组进行赋值
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
可以看到每次添加元素时都会进行Arrays.copyOf
操作,代价非常昂贵。
读操作
public E get(int index) {
return (E)(getArray()[index]);
}
读的时候是不需要加锁的,直接获取。删除和增加是需要加锁的。
拓展
Vector与SynchronizedList的区别
- SynchronizedList它是对list集合的包装类,所以在扩容上是List的扩容机制,即1.5倍,而vector扩容为原来的2倍;
- SynchronizedList不止作用于ArrayList,它的作用域在整个List集合类。而vector相当于是线程安全下的ArrayList(因为其底层是object数组);
- SynchronizedList的遍历并没有加锁,所以遍历时需要同步处理;
- SynchronizedList的锁作用域为同步代码块,vector的锁作用域是整个方法。
Vector、SynchronizedList和copyOnWriteArrayList的效率对比
代补充
Vector、SynchronizedList和copyOnWriteArrayList的使用场景
- vector在所有的方法添加了synchronized关键字,并发性能差,不推荐使用;
- CopyOnWriteArrayList写时复制,读时没有锁,会出现数据性一致问题,所以在不要求数据实时一致性的情况下,使用copyOnWriteArrayList以提高性能;
- Collections.synchronizedList在写多的情况下使用,但需要注意迭代操作未加锁(其实还有一种场景,List的其他子类需要线程安全的条件下)。
HashSet
其实熟读过源码的同学都知道,HashSet底层是由HashMap实现 ,值存放于HashMap的key上 ,HashMap的value统一为PRESENT
而对于它不安全问题的原因我们会在HashMap处做详细的解释。
解决不安全问题可以通过Collections.synchronizedSet去解决,在这里,其原理跟上面的差不多,在这里我们就不做详述了。
HashMap
不安全原因
我们先说结论(以防在后面的介绍中迷失自我~~~)
这里需要分以下JDK版本:
JDK1.7版本其中有两点:
- 多线程环境操作下,在resize扩容的过程中,由于采用的是头插法,所以会导致环形链表的产生;
- get方法的不可见性;(即上一秒put完值,下一秒get值,所get到的值不是最新值。)
- put方法值如果index一致,多线程环境下可能会造成数据丢失,导致后一个插入的覆盖前一个的值。
JDK1.8版本:
1. 由于1.8版本扩容机制也发生改变,所以环形链表问题出现的概率大幅度降低,由于有红黑树的引入,提高性能,将头插法改为尾插;
2. 最主要的就体现在get/put方法这里。
接下来我们来详细探讨一下其多线程环境下的操作的操作细节,主要从以下思路讲解:
- 什么是头插法,什么尾插法;
- 为什么会形成环形链表;
- 数据的不一致性。
头插法与尾插法
头插法
如上图,这些过程可总结为两句话:
- 将头指针指向下一结点的地址赋给新增结点的next;
- 再将新增节点的地址赋值给头指针的下一个结点。
node->next = head->next;
head->next = node;
通俗一点的白话就是说新来的值会取代原有的值,原有的值就顺推到链表的下一个节点。
尾插法
同样的,总结为:
从新增第二个结点开始,尾指针总指向下一个节点next,最后一结点指向null;
白话就是说新来的值会顺序的添加到链表中。
为什么会形成环形链表(JDK1.7版本)
首先我们先来了解以下hashmap的扩容机制(resize过程):
分为两步:
-
创建一个新的Entry空数组,长度是原来的两倍。(因为hashmap的扩容机制是2^n);
-
ReHash:由于数组长度改变,Hash的规则也随之改变(公式为hash&length-1),相当于重新将数据插入一遍。
其实我所理解的ReHash,其实在另一方面是为了减少hash冲突(减少链表长度),因为rehash之后,每个桶上的节点数一定小于等于原来桶上的节点数。
然而其实问题也是出现在这个Resize,举个例子把:
假设我们现在往一个容量大小为2的put两个值,负载因子是0.75,阀值:2*0.75 = 1,所以我们在put第二个的时候就会进行resize。
然后我们现在用不同线程插入A,B,C,在未进行resize之前,我们看到的可能是这个样子的:
链表的指向A->B->C
A的下一个指针是指向B的
因为resize的赋值方式,也就是使用了单链表的头插入方式,同一位置上新元素总会被放在链表的头部位置,在旧数组中同一条Entry链上的元素,通过重新计算索引位置后,有可能被放到了新数组的不同位置上。
就可能出现下面的情况,大家发现问题没有?
B的下一个指针指向了A
一旦几个线程都调整完成,就可能出现环形链表
如果这个时候去取值,悲剧就出现了——Infinite Loop。
而JDK1.8版本在扩容时,如果重新hash的运算位为0,在新数组保持原位置,如果为1,则在新数组的位置为原始位置+扩容前的旧容量,这样做会保持链表元素原本的顺序不变,会大幅度降低链表成环的问题。
其中,在1.8版本还有一个改变,那就是将头插法改为尾插法,其实按照我的理解,是因为红黑树的引入,如果头插的话,你还得再去找到后一个进来的数据,这样不就造成了性能的浪费么。
(其实准确的说对于使用这个尾插法我还是抱有疑问的,真的是是为了效率???)
如您有看法,请留言。
解决方案1 collections.SynchronizedMap
原理与上述Collections.SynchronizedList相同,此处略过;
解决方案2 HashTable
其实感觉跟Vector有点类似,直接在方法上添加了Synchronized关键字,锁住了整个方法,并发度很低。
[图片来源]:https://cloud.tencent.com/developer/article/1447127
解决方案3 ConcurrentHashMap
首先,在说concurrentHashMap之前,我们首先需要了解到它JDK1.7版本跟1.8版本还是有很大区别的。
JDK1.7:
采用分段锁segment实现,通过继承ReentrantLock做同步处理,并在HashEntry部分元素结点处添加volatile关键字。底层为数组+单链表的结构。
JDK1.8:
去除了segement机制,改为了CAS与synchronized结合的同步方式,并在node部分元素结点处添加volatile关键字,保证了get方法时的可见性,(因为get方法没有添加锁)。底层为数组+单链表+红黑树的结构。
ConcurrentHashMap原理简述
在这里,我们不做大篇幅的原理介绍,如您对源码感兴趣,可自行查阅相关文章。
JDK1.7:
[图片来源]:https://cloud.tencent.com/developer/article/1447127
对于1.7版本的ConcurrentHashMap来讲,它的并发度为16,因为它最多只允许创建16个segment。
put操作如下
首先它会先定位到具体的segment,然后进行添加元素操作
public V put(K key, V value) {
Segment<K,V> s;
//concurrentHashMap不允许key/value为空
if (value == null)
throw new NullPointerException();
//hash函数对key的hashCode重新散列,避免差劲的不合理的hashcode,保证散列均匀
int hash = hash(key);
//定位segment
int j = (hash >>> segmentShift) & segmentMask;
if ((s = (Segment<K,V>)UNSAFE.getObject
(segments, (j << SSHIFT) + SBASE)) == null)
s = ensureSegment(j);
return s.put(key, hash, value, false);
}
接下来时segment内部的put方法:
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);//此处是一个自旋操作,尝试获取锁,其尝试次数由MAX_SCAN_RETRIES 控制,如果超过该值,则改为阻塞获取。
V oldValue;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;//定位HashEntry
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
if (e != null) {
K k;
// 遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
else {
// 不为空则需要新建一个 HashEntry 并加入到 Segment 中
if (node != null)
node.setNext(first);
else
node = new HashEntry<K,V>(hash, key, value, first);
//若c超出阈值threshold,需要扩容并rehash。
int c = count + 1;
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
get方法
public V get(Object key) {
Segment<K,V> s;
HashEntry<K,V>[] tab;
int h = hash(key);
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE; //先定位Segment,再定位HashEntry
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
e != null; e = e.next) {
K k;
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value;
}
}
return null;
}
将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。
由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。所以get在这里是无锁状态的。
JDK1.8
[图片来源]:https://cloud.tencent.com/developer/article/1447127
JDK1.8ConcurrentHashMap抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized
来保证并发安全性。
跟1.8的HashMap很像,也把之前的HashEntry改成了Node,但是作用不变,把值和next采用了volatile去修饰,保证了可见性,并且也引入了红黑树,在链表大于一定值的时候会转换(默认是8)。
注意这里的synchronized,只锁定了当前链表或红黑二叉树的首节点,也就是说只有发生了hash不冲突,才会触发synchronized同步机制。在效率上又会有提升。
put操作如下:
- 根据 key 计算出 hashcode 。
- 判断是否需要进行初始化。
- 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
- 如果当前位置的
hashcode == MOVED == -1
,则需要进行扩容。 - 如果都不满足,则利用 synchronized 锁写入数据。
- 如果数量大于
TREEIFY_THRESHOLD
则要转换为红黑树。
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();//K,V都不能为空,否则的话跑出异常
int hash = spread(key.hashCode()); //取得key的hash值
int binCount = 0; //用来计算在这个节点总共有多少个元素,用来控制扩容或者转移为树
for (Node<K,V>[] tab = table;;) { //
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable(); //第一次put的时候table没有初始化,则初始化table
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { //通过哈希计算出一个表中的位置因为n是数组的长度,所以(n-1)&hash肯定不会出现数组越界
if (casTabAt(tab, i, null, //如果这个位置没有元素的话,则通过cas的方式尝试添加,注意这个时候是没有加锁的
new Node<K,V>(hash, key, value, null))) //创建一个Node添加到数组中区,null表示的是下一个节点为空
break; // no lock when adding to empty bin
}
/*
* 如果检测到某个节点的hash值是MOVED,则表示正在进行数组扩张的数据复制阶段,
* 则当前线程也会参与去复制,通过允许多线程复制的功能,一次来减少数组的复制所带来的性能损失
*/
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
/*
* 如果在这个位置有元素的话,就采用synchronized的方式加锁,
* 如果是链表的话(hash大于0),就对这个链表的所有元素进行遍历,
* 如果找到了key和key的hash值都一样的节点,则把它的值替换到
* 如果没找到的话,则添加在链表的最后面
* 否则,是树的话,则调用putTreeVal方法添加到树中去
*
* 在添加完之后,会对该节点上关联的的数目进行判断,
* 如果在8个以上的话,则会调用treeifyBin方法,来尝试转化为树,或者是扩容
*/
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) { //再次取出要存储的位置的元素,跟前面取出来的比较
if (fh >= 0) { //取出来的元素的hash值大于0,当转换为树之后,hash值为-2
binCount = 1;
for (Node<K,V> e = f;; ++binCount) { //遍历这个链表
K ek;
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 = new Node<K,V>(hash, key, //为空的话把这个要加入的节点设置为当前节点的下一个节点
value, null);
break;
}
}
}
else if (f instanceof TreeBin) { //表示已经转化成红黑树类型了
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)
return oldVal;
break;
}
}
}
addCount(1L, binCount); //计数
return null;
}
拓展
HashTable与HashMap的区别?
- 散列方式
- hashMap采用的是hash&(length-1);
- hashtable采用的取模运算;
- 容器整体结构
- hashmap中的key和value值都允许为null;1.7版本底层为数组+链表,1.8版本之后则为数组+链表+红黑树;
- hashtable的key和value值都不允许为null,否则返回NullPointerException;底层为数组+链表。
- 扩容机制
- hashmap默认初始化容量为16,容器容量一定是2的N次方;
- hashtable默认初始化容量为11,扩容是以原容量的2倍+1扩容;
- 线程安全方面
- hashtable的操作方法都带有synchronized关键字修饰,为线程安全;
- hashmap为线程不安全。
Collections.synchronizedMap、HashTable与ConcurrentHashMap的使用场景
首先,我认为如果在并发度要求比较高的情况下,数据的一致性不是强一致性的话,首选ConcurrentHashMap。
相反的,对于强一致性问题,还是选择hashTable,因为在同一时间内,也只能让一个线程操作。
至于Collections.synchronizedMap,其实我是纠结的,或者说有点不确定性,因为我觉得它应该也可以保证强一致性,因为它锁住的当前对象,他对并发度的支持相对于hashTable来说,高一点。
如有大佬了解,或者我说的不正确,请联系我!!!!
总结
以上就是对集合不安全问题的总结了,其实要想完整的理解下来,对于初学者而言,难度还是很高的,其实有时候写文章的时候,总在思考怎么把文章写的浅显易懂,怎么有条理性,其实我还是很排斥源码的,因为有时候读者读者读者就会迷失。
这篇文章其实也花了很长的时间去写,如有不正确的地方,欢迎大佬们来指正~~~