我是大白(●—●),这是我开始学习记录大白Java软件攻城狮晋升之路的第十二天。今天学习的是【尚硅谷】大厂必备技术之JUC并发编程
这里以ArrayList集合为例,首先看ArrayList的add()方法,底层没有synchronized修饰,因此会导致线程不安全的问题。
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return true (as specified by {@link Collection#add})
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
例子:
public class ArrayListDemo {
public static void main(String[] args) {
//创建ArrayList集合
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < 30; i++) {
new Thread(() ->{
//向集合添加内容
list.add(UUID.randomUUID().toString().substring(0,8));
//从集合获取内容
System.out.println(list);
}, String.valueOf(i)).start();
}
}
}
运行之后的结果将会看到抛出并发修改异常ConcurrentModificationException
由控制台输出可以知道在打印list的数据时,出现的异常。
ArrayList继承AbstractList并继承AbstractCollection,因此继承其toString()方法,由此可以看出问题是出在了Iterator的next()方法中
// String conversion
/**
* Returns a string representation of this collection. The string
* representation consists of a list of the collection's elements in the
* order they are returned by its iterator, enclosed in square brackets
* ("[]"). Adjacent elements are separated by the characters
* ", " (comma and space). Elements are converted to strings as
* by {@link String#valueOf(Object)}.
*
* @return a string representation of this collection
*/
public String toString() {
Iterator<E> it = iterator();
if (! it.hasNext())
return "[]";
StringBuilder sb = new StringBuilder();
sb.append('[');
for (;;) {
E e = it.next();
sb.append(e == this ? "(this Collection)" : e);
if (! it.hasNext())
return sb.append(']').toString();
sb.append(',').append(' ');
}
}
继续分析ArrayList中实现的next()方法。会发现有一个checkForComodification(),其作用是用来检查集合是否被修改过,如果修改过就抛出ConcurrentModificationException异常。
另外也可以看到i如果大于等于集合数据的长度,也会抛出ConcurrentModificationException异常,这里的i是迭代器的cursor,表示下一个要访问的元素的索引。
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() 方法。可以看到modCount和expectedModCount不一致的情况下就会抛出异常。其中expectedModCount表示对ArrayList修改次数的期望值,它的初始值为modCount。
modCount 是在 ArrayList 中赋值的,并且初始值为 0,在 add 和 remove 的时候(修改元素的时候)会增加 1。
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++; //add方法执行到这里会对modCount进行+1操作
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
因此就可以得出结果,是当A线程执行add()方法后再进行输出时已经对expectedModCount 赋值为当前的modCount并且正准备执行checkForComodification()
方法,另一个B线程此时也正在执行add()方法,并对modCount
进行了加1操作,就导致了此时ArrayList对象的modCount
与expectedModCount
不相等并抛出异常。
对于以前版本的JDK来说,对于集合的并发线程不安全的处理方式是使用Vector集合,他底层的add()方法和remove()方法都是有synchronized
修饰。因此调用add方法的时候会造成线程阻塞,因此来解决并发不安全问题,但是并发效率较低,不推荐使用。
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
public synchronized boolean removeElement(Object obj) {
modCount++;
int i = indexOf(obj);
if (i >= 0) {
removeElementAt(i);
return true;
}
return false;
}
例子:
public class VectorDemo {
public static void main(String[] args) {
//创建ArrayList集合
Vector<String> list = new Vector<>();
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();
}
}
}
Collentions是集合的工具类,它里面有一个_synchronizedList()_方法,其可以返回由指定列表支持的同步(线程安全的)列表来保证线程安全。
public class ArrayListDemo {
public static void main(String[] args) {
List<String> 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利用的是写时复制技术。
写时复制技术的核心思想是:如果有多个呼叫者(callers)同时要求相同资源,他们会共同取得相同的指标指向相同的资源,直到某个呼叫者(caller)尝试修改资源时,系统才会真正复制一个副本(private copy)给该呼叫者,以避免被修改的资源被直接察觉到,这过程对其他的呼叫只都是通透的(transparently)。此作法主要的优点是如果呼叫者并没有修改该资源,就不会有副本(private copy)被建立。
这个过程对其他的调用者是透明的
(transparently)。
查看CopyOnWriteArrayList
源码的add()方法,会发现首先进行上锁,然后copy数组,并在新数组中添加元素并合并数组。
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
public class HashSetDemo {
public static void main(String[] args) {
//创建ArrayList集合
Set<String> set = new HashSet<>();
for (int i = 0; i < 30; i++) {
new Thread(() ->{
//向集合添加内容
set.add(UUID.randomUUID().toString().substring(0,8));
//从集合获取内容
System.out.println(set);
}, String.valueOf(i)).start();
}
}
}
同样的也会抛出ConcurrentModificationException异常
类似ArrayList,针对HashSet的线程不安全的解决办法是使用CopyOnWriteArraySet集合。
由于HashSet的底层就是由HashMap实现的
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
static final long serialVersionUID = -5024744406713321676L;
private transient HashMap<E,Object> map;
...
}
因此使用HashMap时也是会出现ConcurrentModificationException异常
public class HashMapDemo {
public static void main(String[] args) {
//创建ArrayList集合
Map<String,String> map = new HashMap<>();
for (int i = 0; i < 30; i++) {
new Thread(() ->{
//向集合添加内容
map.put(UUID.randomUUID().toString().substring(0,8));
//从集合获取内容
System.out.println(map);
}, String.valueOf(i)).start();
}
}
}