线程不安全问题(ArrayList)
线程不安全案例(Fast-fail)
向ArrayList中添加随机元素,循环20轮
public class test {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < 20; i++) {
new Thread(()->{
//向集合添加内容
list.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
输出:
[9ef94f18, 6765e7f8]
[9ef94f18, 6765e7f8, f870e6b0]
[9ef94f18, 6765e7f8, f870e6b0, 89d21038]
[9ef94f18, 6765e7f8, f870e6b0, 89d21038, ecbff513]
[9ef94f18, 6765e7f8, f870e6b0, 89d21038, ecbff513, 71a24754]
[9ef94f18, 6765e7f8, f870e6b0, 89d21038, ecbff513, 71a24754, 8480170d]
[9ef94f18, 6765e7f8, f870e6b0, 89d21038, ecbff513, 71a24754, 8480170d, e3e5c629]
[9ef94f18, 6765e7f8, f870e6b0, 89d21038, ecbff513, 71a24754, 8480170d, e3e5c629, ace171fa]
[9ef94f18, 6765e7f8, f870e6b0, 89d21038, ecbff513, 71a24754, 8480170d, e3e5c629, ace171fa, 1f238147]
.........省略
Exception in thread "13" java.util.ConcurrentModificationException
at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1043)
at java.base/java.util.ArrayList$Itr.next(ArrayList.java:997)
at java.base/java.util.AbstractCollection.toString(AbstractCollection.java:472)
at java.base/java.lang.String.valueOf(String.java:2951)
at java.base/java.io.PrintStream.println(PrintStream.java:897)
at com.acerola.collection.lock.test.lambda$main$0(test.java:19)
at java.base/java.lang.Thread.run(Thread.java:834)
进程已结束,退出代码0
在进行到第14个线程时,出现ConcurrentModificationException
,即并发修改异常,打印内容的时候,内部自己在循环遍历这个List,会自动生成一个iterator 来遍历该 list,但同时该 list 正在被 Iterator.remove() 修改。Java 一般不允许一个线程在遍历 Collection 时另一个线程修改它。
那这个异常是如何抛出的呢,看源码:
首先进入add()方法
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
add()中对一个modCount的变量进行了自增,同时,定义了一个expectedModCount变量
final int expectedModCount = modCount;
在进行Iterator迭代时,modCount的值赋给expectedModCount后,会执行一个 checkForComodification()
方法,比较两者值,相等则为正常情况,继续运行,不相等则抛出ConcurrentModificationException
这个异常,也就是我们刚才遇到的
一般来说,单线程下,这些方法串行,是不会出现问题的,但是在多线程下,多个线程操作一个list,同时进行读写,在进行checkForComodification()
比较之前,可能modCount已经被其他线程修改了,就会造成异常抛出
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
// prevent creating a synthetic constructor
Itr() {}
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
checkForComodification()方法:
private void checkForComodification(final int expectedModCount) {
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
遍历过程中,要检测是否modCount是否等于期待的值
,如果不是抛出异常。
解决方案
synchronized
这个就很简单了,给list.add()抽离,加上synchronized
关键字,加锁
public class test {
ArrayList<String> list = new ArrayList<>();
private synchronized void ss(){
list.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(list);
}
public static void main(String[] args) {
test test = new test();
for (int i = 0; i < 20; i++) {
new Thread(()->{
test.ss();
},String.valueOf(i)).start();
}
}
}
Lock锁
跟synchronized大差不差,这里选用ReentrantLock()锁
public class test {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
ReentrantLock lock = new ReentrantLock();
for (int i = 0; i < 100; i++) {
new Thread(() -> {
//向集合添加内容
lock.lock();
try {
list.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(list);
} finally {
lock.unlock();
}
}, String.valueOf(i)).start();
}
}
}
Vector
vector使用了synchronized关键字,直接替换ArrayList即可,但是由于效率低下,不常用
Collections.synchronizedCollection
使用Collections.synchronizedCollection创建list,但,不常用
public class test {
public static void main(String[] args) {
List list = Collections.synchronizedList(new ArrayList<>());
for (int i = 0; i < 100; i++) {
new Thread(() -> {
//向集合添加内容
list.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(list);
}, String.valueOf(i)).start();
}
}
}
CopyOnWriteArrayList(写时复制技术)
使用CopyOnWriteArrayList创建list
public class test {
public static void main(String[] args) {
//List list = Collections.synchronizedList(new ArrayList<>());
List list = new CopyOnWriteArrayList();
for (int i = 0; i < 100; i++) {
new Thread(() -> {
//向集合添加内容
list.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(list);
}, String.valueOf(i)).start();
}
}
}
CopyOnWriteArrayList类最大的特点就是,在对其实例进行修改操作(add/remove等)会新建一个数据并修改,修改完毕之后,再将原来的引用指向新的数组(覆盖)。这样,修改过程没有修改原来的数组。也就没有了ConcurrentModificationException错误。
add源码:
public boolean add(E e) {
synchronized (lock) {
Object[] es = getArray();
int len = es.length;
es = Arrays.copyOf(es, len + 1);
es[len] = e;
setArray(es);
return true;
}
}
HashSet与HashMap也有相同的问题,使用CopyOnWriteArraySet解决
HashSet与HashMap也会出现并发修改异常,解决办法就是使用CopyOnWriteArraySet
线程不安全问题(Hashmap)
HashMap与HashSet是非线程安全的。其线程不安全主要体现在resize时的死循环及使用迭代器时的fast-fail上。
问题一 迭代器fast-fail
与ArrayList类似,在使用迭代器的过程中会更新modCount,并且比较mc与modCount,如果HashMap被别的线程修改,那么ConcurrentModificationException
将被抛出,详细可以参考上面ArrayList的解释说明,源码如下:
public final void forEach(Consumer<? super K> action) {
Node<K,V>[] tab;
if (action == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (Node<K,V> e : tab) {
for (; e != null; e = e.next)
action.accept(e.key);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
问题二 数据覆盖
put操作源码原因分析
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;
if ((p = tab[i = (n - 1) & hash]) == null) // 如果没有hash碰撞则直接插入元素
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
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;
}
}
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;
}
其中第六行代码是判断是否出现hash碰撞,假设两个线程A、B都在进行put操作,并且hash函数计算出的插入下标是相同的,当线程A执行完第六行代码后由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全。
除此之外,还有就是代码的第38行处有个++size,我们这样想,还是线程A、B,这两个线程同时进行put操作时,假设当前HashMap的zise大小为10,当线程A执行到第38行代码时,从主内存中获得size的值为10后准备进行+1操作,但是由于时间片耗尽只好让出CPU,线程B快乐的拿到CPU还是从主内存中拿到size的值10进行+1操作,完成了put操作并将size=11写回主内存,然后线程A再次拿到CPU并继续执行(此时size的值仍为10),当执行完put操作后,还是将size=11写回内存,此时,线程A、B都执行了一次put操作,但是size的值只增加了1,所有说还是由于数据覆盖又导致了线程不安全。
解决方案
synchronized
略,同ArrayList
Lock
略,同ArrayList
Collections.synchronizedMap
略,同ArrayList
ConcurrentHashMap(jdk1.8)
ConcurrentHashMap 的实现原理:
在 JDK8 及以上的版本中,ConcurrentHashMap 的底层数据结构依然采用“数组+链表+红黑树”,但是在实现线程安全性方面,抛弃了 JDK7 版本的 Segment分段锁的概念,而是采用了 synchronized + CAS 算法来保证线程安全。在ConcurrentHashMap中,大量使用 Unsafe.compareAndSwapXXX 的方法,这类方法是利用一个CAS算法实现无锁化的修改值操作,可以大大减少使用加锁造成的性能消耗。这个算法的基本思想就是不断比较当前内存中的变量值和你预期变量值是否相等,如果相等,则接受修改的值,否则拒绝你的而操作。因为当前线程中的值已经不是最新的值,你的修改很可能会覆盖掉其他线程修改的结果。