文章目录
1、ArrayList线程不安全演示示例
/**
* @ClassName: CollectionDemo1
* @Auther: 戏中人
* @Description: ArrayList线程不安全演示
*/
public class CollectionDemo1 {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < 30; i++) {
final int temp = i;
new Thread(()->{
list.add("thread" + temp);
System.out.println(list);
},String.valueOf(i+1)).start();
}
}
}
运行可能会报类似如下异常:
Exception in thread "×××" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at java.util.AbstractCollection.toString(AbstractCollection.java:461)
......
ConcurrentModificationException 即并发修改异常,是Java并发编程中常见的异常,在本例中产生该异常的主要原因是并发争抢修改集合导致的。
以生活中的例子来举例就是一个A同学正在名单上签到,此时在还没签完的情况下又有另一个B同学直接把名单抢过去进行签名,这就可能出现名单上出现一些非正常的数据,如一条长长的笔迹,这是我们不希望看到的。
2、解决ArrayList线程不安全方案
2.1 采用Vector类
List<String> list = new Vector<>();
从下面的源码中可以看到Vector的 add(E e)
方法添加了 synchronized
关键字以保证数据一致性,但是同时也降低了并发的性能 。
/**
* Appends the specified element to the end of this Vector.
*
* @param e element to be appended to this Vector
* @return {@code true} (as specified by {@link Collection#add})
* @since 1.2
*/
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
2.2 通过Collections类构造同步的List
List<String> list = Collections.synchronizedList(new ArrayList<>());
当然,除了可以构造同步的List,也可以构造同步的 Set
和Map
,这样也就能反面证明 Set 和 Map 也是不安全的集合类。
2.3 采用 CopyOnWriteArrayList类
List<String> list = new CopyOnWriteArrayList<>();
该类位于 java.util.concurrent
包下 ,从类名上大致可以知道其意思就是 写时复制 。
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
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();
}
}
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array; //存储元素的数组
/**
* Gets the array. Non-private so as to also be accessible
* from CopyOnWriteArraySet class.
*/
final Object[] getArray() {
return array; //返回存储元素的数组
}
/**
* Sets the array.
*/
final void setArray(Object[] a) {
array = a; //array指向数组 a
}
从上面的源码中可以看出,在往CopyOnWrite容器中添加元素的时候,不是直接往当前的容器 Object[] array
添加,而是
-
先将当前容器
array
进行复制一份得到新的容器newElements
,也就是代码中的Object[] newElements = Arrays.copyOf(elements, len + 1);
; -
然后往新的容器
newElements
里面添加元素,newElements[len] = e;
; -
最后再将原容器的引用指向新容器 ,
setArray(newElements);
;
这样复制数组的做法,有个明显的好处就是可以对 CopyOnWriteArrayList
进行并发的读,而不需要加锁,因为当前容器不会添加任何元素,只会复制当前容器进行添加操作后再更新容器的引用。这在一定程度上体现了读写分离 的思想,读和写的容器是不同的容器,是分开的。
3、HashSet
由上面可知, HashSet 同样也是线程不安全的,不安全示例如下:
public class CollectionDemo2 {
public static void main(String[] args) {
Set<String> set = new HashSet<>();
for (int i = 0; i < 30; i++) {
final int temp = i;
new Thread(()->{
set.add("thread" + temp);
System.out.println(set.toString());
},String.valueOf(i+1)).start();
}
}
}
3.1 解决方案
-
通过Collections类构造同步的Set
Set<String> set = Collections.synchronizedSet(new HashSet<>());
-
采用CopyOnWriteArraySet类
Set<String> set = new CopyOnWriteArraySet<>();
3.2 CopyOnWriteArraySet
该类的底层数据结构其实是通过 CopyOnWriteArrayList 类实现的
/**无参构造方法
* Creates an empty set.
*/
public CopyOnWriteArraySet() {
al = new CopyOnWriteArrayList<E>();
}
3.3 HashSet 面试点补充
-
第一问:HashSet 底层数据结构是什么?
HashSet 底层数据结构是HashMap ,无参构造方法会创建一个初始容量为16,加载因子为0.75的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; // Dummy value to associate with an Object in the backing Map private static final Object PRESENT = new Object(); /** * Constructs a new, empty set; the backing <tt>HashMap</tt> instance has * default initial capacity (16) and load factor (0.75). */ public HashSet() { map = new HashMap<>(); } //其他方法省略 }
-
第二问:如果HashSet底层是HashMap,那么为什么在调用HashSet的 add(E e) 方法时只有一个参数,而不是 K-V 键值对?
直接看 add(E e) 源码 ,/** * Adds the specified element to this set if it is not already present. * More formally, adds the specified element <tt>e</tt> to this set if * this set contains no element <tt>e2</tt> such that * <tt>(e==null ? e2==null : e.equals(e2))</tt>. * If this set already contains the element, the call leaves the set * unchanged and returns <tt>false</tt>. * * @param e element to be added to this set * @return <tt>true</tt> if this set did not already contain the specified * element */ public boolean add(E e) { return map.put(e, PRESENT)==null; }
从源码中可以看出,add(E e)方法中的元素是作为HashMap中的key值,而value值是一个Object类型的常量 PRESENT ,其中该常量定义如下:
// Dummy value to associate with an Object in the backing Map private static final Object PRESENT = new Object();
4、HashMap
HashMap线程不安全演示示例:
/**
* @ClassName: CollectionDemo2
* @Auther: 戏中人
* @Description: HashMap不安全演示示例
*/
public class CollectionDemo3 {
public static void main(String[] args) {
Map<String,String> map = new HashMap<>();
for (int i = 0; i < 30; i++) {
final int temp = i;
new Thread(()->{
map.put(("thread" + temp), UUID.randomUUID().toString().substring(0, 5));
System.out.println(map.toString());
},String.valueOf(i+1)).start();
}
}
}
4.1 解决方案
-
与HashSet类似,也可以通过Collections类构造同步的Map
Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
-
采用ConcurrentHashMap类
切记,HashMap没有类似HashSet和ArrayList的 CopyOnWrite××× 的类,取而代之的是 ConcurrentHashMap 类。Map<String, String> map = new ConcurrentHashMap<>();
从以上三个类的线程不安全示例可以看出Java设计的框架体系是有规可循的,不仅出现的问题类似,解决办法也十分相似。