面试官:ArrayList是线程安全的吗?如果不是,请举例说明?

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,也可以构造同步的 SetMap ,这样也就能反面证明 Set 和 Map 也是不安全的集合类。

image-20210207155407847
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();
         }
    }
}
image-20210207215746359
3.1 解决方案
  1. 通过Collections类构造同步的Set

    Set<String> set = Collections.synchronizedSet(new HashSet<>());
    
  2. 采用CopyOnWriteArraySet类

    Set<String> set = new CopyOnWriteArraySet<>();
    
3.2 CopyOnWriteArraySet

该类的底层数据结构其实是通过 CopyOnWriteArrayList 类实现的

image-20210207220634676
	/**无参构造方法
     * 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&nbsp;?&nbsp;e2==null&nbsp;:&nbsp;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();
         }
    }
}
image-20210207223004722
4.1 解决方案
  1. 与HashSet类似,也可以通过Collections类构造同步的Map

    Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
    
  2. 采用ConcurrentHashMap类
    切记,HashMap没有类似HashSet和ArrayList的 CopyOnWrite××× 的类,取而代之的是 ConcurrentHashMap 类。

    Map<String, String> map = new ConcurrentHashMap<>();
    

从以上三个类的线程不安全示例可以看出Java设计的框架体系是有规可循的,不仅出现的问题类似,解决办法也十分相似。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值