集合多线程下的安全性

ArrayList多线程下的安全性

多线程下运行问题

ArrayList之前也用过很多次,但是都是在单线程下使用的,今天测试了一些ArrayList在多少次情况下运行的状况

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
// 启动100个检查网list中添加数据
for(int i=0;i<100;i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(list);
}).start();
}
}

结果:

​ 抛出:java.util.ConcurrentModificationException

解决办法

  • 使用Vector,Vector是一个线程安全类,它在每个方法上都加了Synchronized同步锁
1
List<String> list = new Vector<String>();
  • 使用Collections.synchronizedList(new ArrayList());将list转化成一个线程安全的集合
1
List<String> list = Collections.synchronizedList(new ArrayList<String>());
  • 使用CopyOnWriteArrayList
1
List<String> list =new CopyOnWriteArrayList<String>();

CopyOnWriteArrayList是一种写时复制的容器,利用了ReentrantLock实现。它在添加元素的时候不会直接往Object数组中添加,它首先会拷贝一个数组 Object[] newElements,然后往这个新的数组中添加元素,最后再将原数组的引用指向新数组。CopyOnWriteArrayList利用了读写分离的思想,查询元素的时候不添加锁,增加元素的时候加锁.这样做的好处是可以对容器并发的读而不需要加锁。

HashSet,HashMap

HashSet与HashMap在并发情况下同样会产生java.util.ConcurrentModificationException

HashSet和Arraylist同样可以使用Collections.synchronizedSet()和CopyOnWriteArraySet()解决

对于HashMap使用ConcurrentHashMap,在这里先解释一下HashMap的源码

1.8HashMap底层源码的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 判断当前数组是否为空
if ((tab = table) == null || (n = tab.length) == 0)
// 初始化
n = (tab = resize()).length;
//根据数组长度和hash值获取到下标对应的节点,并判断是否为空
if ((p = tab[i = (n - 1) & hash]) == null)
//为空,创建新的Node
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 不为空,判断当前的的节点的hash值与新插入的hash值,以及key是否相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 相等将当前的节点复制给e
e = p;
// 遍历红黑数
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 变量链表
for (int binCount = 0; ; ++binCount) {
// 如果到了链表的最后一个节点,将新的节点插入到链表尾部
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 链表中存在与新节点相等的节点,跳出循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 如果e不为空 替换旧值,并将旧值返回
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
// 快速失败机制
++modCount;
// 如果数组中所有的值的大小,大于阈值进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

1.7的HashMap源码实现逻辑与1.8差不多,大概为下面的几步

1.判断数组是否为空,为空进行初始化inflateTable(threshold),初始化时数组容量为2的次方

​ 2.判断Key是否为空,如果key为空将key为空的值添加

​ 3.如果key不为空,获取key的hash值,根据key的hash值和数组长度获取数组对应的下标

​ 4.找到对应下标后,判断这个桶中的hash以及key是否相等,相等覆盖j旧值

​ 5.如果没有,将该节点加入到链表头部

​ 在第5步前面,也就是在插入之前其实是需要扩容的(resize)

​ 当size>=threshold===>resize====>transfer

了解了HashMap底层实现之后,对于ConcurrentHashMap就可以更好的了解了

对于1.8的ConcurrentHashMap,采用的 synchronized+CAS+红黑树,synchronized每次锁住的都是链表的头节点,在每次向map添加元素的时候,需要先获得头节点的对象锁。

对于1.7的ConcurrentHashMap,采用的Segment+Lock,初始化时将每个segment的大小设置为2的次方数,先构造第一个segment的大小,其它segment的大小都是根据第一个segment大小进行构建,在调用put方法的使用,调用的是segment中的put方法,而segment继承了ReentrantLock类,从而达实现线程安全的目的。

文章目录
  1. 1. ArrayList多线程下的安全性
  2. 2. 多线程下运行问题
  3. 3. 解决办法
  4. 4. HashSet,HashMap
|
载入天数...载入时分秒...