Java面试——不安全的集合类

2023-04-28,,

Java 中有许多的集合,常用的有ListSetQueueMap。 其中 List,Set,Queue都是Collection(集合),List<String>中<>的内容表示其中元素的类型,是泛型的一种使用。不能直接使用简单数据类型做泛型的原因:集合类(比如Set)在进行各种 "操作" ( 如contains()) 时都会调用元素本身提供的 "方法" ( 如hashCode() 和 equals()),而不是由集合类自身去实现这些 "方法"。这就要求如果某人想要用这个集合执行某些 "操作",那就必须在要加入集合的元素中实现相应的 "方法"。

fail-fast 机制是 java集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生 fail-fast事件。例如:当某一个线程A通过 iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生 fail-fast事件。

一、ArrayList 不安全阐述


List(列表)相当于数组。长度可变,元素存放有一定的顺序,下标从0开始。在JDK中,List作为接口,本身已经声明好了所有的方法(比如add(), contains()......),所以不管是选择 ArrayList还是 LinkedList,完成各种操作的时候依然是使用 List中已经声明过的这一套方法,对使用者来说没有区别。二者只是内部实现逻辑不同,所以在不同的应用场景下会有不同的效率。

【1】ArrayList<> 底层通过数组实现数据的存储。初始的大小为10,超过默认值时,通过 Arrays 进行扩容,如下:允许存空元素,有专门保存容量capacity属性

1 //elementData 需要扩容的数组对象 , newCapacity 扩容的大小 int 类型
2 elementData = Arrays.copyOf(elementData, newCapacity);

【2】ArrayList 线程不安全的例子如下:

 1 public class Test {
2 public static void main(String[] args) {
3 List<String> list = new ArrayList<>();
4 for (int i=1;i<3000;i++){
5 new Thread(){
6 @Override
7 public void run(){
8 list.add(UUID.randomUUID().toString().substring(0,7));
9 System.out.println(list);
10 }
11 }.start();
12 }
13 }
14 }

【3】故障现象:java.util.ConcurrentModificationException

【4】导致原因:因并发无锁导致数据修改异常。
【5】解决方案:① Vector 是线程安全的,可以解决上面的问题。但是性能会急剧下降(不建议使用)。
   ② 使用Collections工具类 Collections.synchronizedList(new ArrayList<>()); 解决上述问题。
   ③ new CopyOnWriteArrayList<>():写时复制,CopeOnWrite 容器既写时复制的容器。往一个容器添加元素的时候,不直接往当前容器 Object[] 添加,而是先将当前容器 object[] 进行 copy,复制出一个新的容器 Object[] newElements,然后往新的容器中添加元素,添加完元素之后,再将原容器的引用指向新容器 setArray(newElements); 这样做的好处是可以对 CopeOnWrite 容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以 CopeOnWrite 容器也是一种读写分离的思想,读和写不同的容器。底层源码如下:

 1 public boolean add(E e) {
2 final ReentrantLock lock = this.lock;
3 lock.lock();
4 try {
5 Object[] elements = getArray();
6 int len = elements.length;
7 Object[] newElements = Arrays.copyOf(elements, len + 1);
8 newElements[len] = e;
9 setArray(newElements);
10 return true;
11 } finally {
12 lock.unlock();
13 }
14 }

扩展一:Arrays.asList 遇到的问题


使用Arrays.asList()方法时把一个数组转化成 List列表,对得到的 List列表进行 add()和 remove()操作, 会导致 java.lang.UnsupportedOperationException异常。

【1】查看 Arrays.asList 源码

1 public static <T> List<T> asList(T... a) {
2 return new ArrayList<>(a);
3 }

【2】查看此 ArrayList结构:add 和 remove 方法继承自 AbstractList

1 private static class ArrayList<E> extends AbstractList<E> {
2 ArrayList(E[] array) {
3 a = Objects.requireNonNull(array);
4 }
5 }

【3】在查看 AbstractList结构:add 和 remove 方法直接返回 UnSupportedOperationException

 1 public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
2
3 public boolean add(E e) {
4 add(size(), e);
5 return true;
6 }
7
8 public void add(int index, E element) {
9 throw new UnsupportedOperationException();
10 }
11
12 public E remove(int index) {
13 throw new UnsupportedOperationException();
14 }
15 }

所以说 Arrays.asList 返回的 List 是一个不可变长度的列表,此列表不再具备原 List 的很多特性,因此慎用 Arrays.asList 方法。

下面代码输出是什么?

1 public static void main(String[] args) {
2 int[] data = {1,2,3,4};
3 List list = Arrays.asList(data);
4 System.out.println(list.size());
5 }

由上面 asList 源码我们可以看到返回的 Arrays的内部类 ArrayList 构造方法接收的是一个类型为 T 的数组,而基本类型是不能作为泛型参数的,所以这里参数 a只能接收引用类型,自然为了编译通过编译器就把上面的 int[] 数组当做了一个引用参数,所以 size 为 1,要想修改这个问题很简单,将 int[] 换成 Integer[] 即可。所以原始类型不能作为 Arrays.asList 方法的参数,否则会被当做一个参数。

扩展二:ArrayList 的 subList的注意事项


《阿里巴巴Java开发手册》泰山版中是这样描述的:

使用起来很简单,也很好理解,不过还是有以下几点要注意,否则会造成程序错误或者异常:
【1】修改原集合元素的值,会影响子集合;
【2】修改原集合的结构,会引起 ConcurrentModificationException异常;
【3】修改子集合元素的值,会影响原集合;
【4】修改子集合的结构,会影响原集合;

看下它的源码:

1 public List<E> subList(int fromIndex, int toIndex) {
2 subListRangeCheck(fromIndex, toIndex, size);
3 return new SubList(this, 0, fromIndex, toIndex);
4 }

可以看到,它调用了 SubList类的构造函数,该构造函数的源码如下图所示:

 1 public class ArrayList<E> extends AbstractList<E>
2 implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
3 ......
4 private class SubList extends AbstractList<E> implements RandomAccess {
5 private final AbstractList<E> parent;
6 private final int parentOffset;
7 private final int offset;
8 int size;
9
10 SubList(AbstractList<E> parent,
11 int offset, int fromIndex, int toIndex) {
12 this.parent = parent;
13 this.parentOffset = fromIndex;
14 this.offset = offset + fromIndex;
15 this.size = toIndex - fromIndex;
16 this.modCount = ArrayList.this.modCount;
17 }
18 ......

可以看出,SubList类是 ArrayList的内部类,该构造函数中也并没有重新创建一个新的 ArrayList,所以修改原集合或者子集合的元素的值,是会相互影响的。

二、Set 不安全阐述


Set 与 List 是相同的,都是线程不安全的,都会出现 ConcurrentModificationException 异常,解决办法常见的有两种:
【1】Collections.synchronizedSet(new HashSet<>()); 通过工具类中的同步代码块可以解决此问题,但性能会受影响。
【2】new CopyOnWriteArraySet<>() 和 List 相同,通过写时复制即可高效解决此问题。底层通过 CopyOnWriteArrayList 实现:

1 public CopyOnWriteArraySet() {
2 al = new CopyOnWriteArrayList<E>();
3 }

扩展:hashSet 的底层是怎么实现的:底层其实是一个 hashMap,源代码如下:

1 public HashSet() {
2 map = new HashMap<>();
3 }

我们的疑惑是,Map 不应该存放的是两个值么,而 Set 存储的都是一个值呀,其实是因为 Map 中的 Key 与 Set 具有相同的特性。因此 Set 的值都存储在 Map 中的 key 中,而 value 存储一个固定的 Object 常量。源代码如下:

1 //存储的 value 值
2 private static final Object PRESENT = new Object();
3 public boolean add(E e) {
4 return map.put(e, PRESENT)==null;
5 }

三、Map 不安全阐述


Map 与 List/Set 是相同的,都是线程不安全的,都会出现 ConcurrentModificationException 异常,解决办法常见的有三种:
【1】Collections.synchronizedMap(new HashMap<>()); 通过工具类中的同步代码块可以解决此问题,但性能会受影响。
【2】HashTable: HashTable 容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下。
【3】new ConcurrentHashMap<>(); 推荐使用,此方法创建的 Map 是线程安全的。而 JDK1.7 之前的 ConcurrentHashMap 使用分段锁机制实现,JDK1.8 则使用数组+链表+红黑树数据结构和CAS原子操作实现 ConcurrentHashMap;
具体可以查看:连接

Java面试——不安全的集合类的相关教程结束。

《Java面试——不安全的集合类.doc》

下载本文的Word格式文档,以方便收藏与打印。