欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

面试官:你都工作3年了,怎么连ArrayList是否为线程不安全都没有搞清楚

程序员文章站 2022-04-07 12:37:12
...

作为一名java程序员,对ArrayList,相信再熟悉不过了。这个类我们平时接触得最多的一个列表集合类。

面试时,也有不少面试官会针对此知识点考察求职者。

 

小爱最近又去面试了,面试官刚好问到这个。

小爱最近又去面试了,最近到某知名互联网公司面试,做了笔试题后,面试官刚好问ArrayList是线程安全还是非线程安全?

小爱说是非线程安全,面试官问,你能说说为什么是非线程安全吗?

小爱一时间说不出个所以然。

面试官说:你都工作3年了,怎么连ArrayList是否为线程不安全都没有搞清楚

小爱感觉这次面试要黄了,类似这样的问题平时应该要掌握好才对。

01、线程安全和线程不安全

   

首先,我们来弄清楚两个概念,

什么是线程安全、什么是线程不安全

线程安全:指当多线程访问时,采用了加锁的机制;即当一个线程访问该类的某个数据时,会对这个数据进行保护,其他线程不能对其访问,直到该线程读取完之后,其他线程才可以使用。防止出现数据不一致或者数据被污染等意外情况。

 

线程不安全:就是不提供数据访问时的数据保护,多个线程能够同时操作某个数据,从而出现数据不一致或者数据污染等意外情况。

 

 

02、为什么ArrayList是线程不安全?

   

 

我们在多线程情况下用   List<String> list = new ArrayList<>();

在add(param)添加信息经常会遇到ConcurrentModificationException这样的异常。

 

我们写个例子来验证下。

public static void main(String[] args){
   List<String> list = new ArrayList<>();
    for (int i = 0; i < 20; i++) {
        new Thread(()->{
                list.add(UUID.randomUUID().toString().substring(0, 6));
                System.out.print(list);
                System.out.print("\n");
        },String.valueOf(i)).start();
    }
}

 

咋一看,程序没什么问题,运行有时候也是正常的,但运行几次就会抛出ConcurrentModificationException这样的异常。

 面试官:你都工作3年了,怎么连ArrayList是否为线程不安全都没有搞清楚

我们来看下  ArrayList的add方法具体做了什么。

  public boolean add(E e) {
     /*
      添加一个元素时,主要做了两步操作
      1.判断列表的capacity容量是否足够,是否需要扩容
      2.将元素放在列表的元素数组里面
     */
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
   }

 

ensureCapacityInternal()这个方法具体是做什么的,我们跟进去看看

 

private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
       minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    ensureExplicitCapacity(minCapacity);
  }

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
    grow(minCapacity);
 }

 private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
    newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
    newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
  }

 

 从源码我们可以知道,但size + 1的这个需求长度大于了elementData这个数组的长度,那么这个数组就要进行扩容。

 

 

通过分析可以得出,ArrayList在多线程环境下是不安全的,那么问题来了,ArrayList不安全为什么要使用

03、ArrayList不安全为什么要使用?

   

 

ArrayList效率还是挺高的,譬如比线程安全的Vector效率要高很多。

 

 

 

04、如何解决线程不安全?

   

 

解决方法也有很多种方式

 

CopyOnWriteArrayList

Vector

Collections.synchronizedList

synchronized

 

 List<String> list = new ArrayList<>(); 

换成

 List<String> list = new CopyOnWriteArrayList<>(); 

试试。

 

那么CopyOnWriteArrayList如何做到线程安全的?

CopyOnWriteArrayList使用了一种叫写时复制的方法,

当有新元素添加到CopyOnWriteArrayList时,

先从原有的数组中拷贝一份出来,然后在新的数组做写操作,

写完之后,再将原来的数组引用指向到新数组。

 

当有新元素add时,如下图,创建新数组,

 面试官:你都工作3年了,怎么连ArrayList是否为线程不安全都没有搞清楚

并往新数组中add一个新元素,这时候,array这个引用仍然是指向原数组的。

当元素在新数组添加成功后,将array这个引用指向新数组。

 

CopyOnWriteArrayList的add操作都是在锁的保护下进行的。 这样做是为了避免在多线程并发add的时候,复制出多个副本出来,把数据搞乱了,导致数组数据错乱。

CopyOnWriteArrayList的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();
    }
}

 

从源码中我们可以得知写操作是在新的数组进行的,这时候如果有线程并发地写,则通过锁来控制,如果有线程并发地读取数据,则要具体问题具体分析: 

  1. 如果写操作未完成,那么直接读取原数组的数据; 

  2. 如果写操作已完成,但是引用还未指向新数组,那么读取的还是原数组的数据; 

  3. 如果写操作已完成,且引用已指向了新数组,那么读取的是新数组的数据。

通过上面的梳理,我们可以留意到CopyOnWriteArrayList有下面的不足: 

  1. 由于写操作需要拷贝数组,会消耗内存,在原数组的内容较多的情况下,可能导致年轻代(Young Generation)或者年老代(Old Generation)

  2. 对于实时读体验一般,像拷贝数组、新增元素都需要一定的时间,可能在调用一个set操作后,读取到数据还是旧的,虽然CopyOnWriteArrayList 能保证数据最终的一致性,不能保证数据的实时一致性。

CopyOnWriteArrayList有一个很好的思想,那就是读写分离,读和写分开。这种思想还是挺值得我们借鉴的。

我们再用

Vector<String> list = new Vector<>()

来替换

List<String> list = new ArrayList<>()

程序同样正常编译通过。

对于上面的程序,我们也可以用synchronized ,synchronized可以保证在同一时刻,只有一个线程可以执行某个方法或某个代码块。

 public static void main(String[] args){
    List<String> list = new ArrayList<>();
    for (int i = 0; i < 100; i++) {
        new Thread(()->{
            synchronized (ArrayListDemo.class) {
                list.add(UUID.randomUUID().toString().substring(0, 6));
                System.out.print(list);
                System.out.print("\n");
            }
        },String.valueOf(i)).start();
    }
}

 

 

面试官:你都工作3年了,怎么连ArrayList是否为线程不安全都没有搞清楚

关于ArrayList线程不安全的知识点本节就先简单分析到这。由于笔者水平有限,文中错漏缺点在所难免,希望读者批评指正。

-END-

作者:洪生鹏 头条优质作者 10年软件开发经验,坚持不脱离一线。媒体合作、品牌宣传请加微信 : hongshengpeng2010

猜你喜欢

提交辞职申请时,领导挽留,要不要留下来

终于明白阿里百度这样的大公司,为什么经常拿ThreadLocal考验求职者了

优秀的程序员更重视阅读源码,不看源码那是假的

面试官:你都工作3年了,怎么连ArrayList是否为线程不安全都没有搞清楚

更多惊喜,请长按二维码识别关注

你若喜欢,别忘了点【在看

面试官:你都工作3年了,怎么连ArrayList是否为线程不安全都没有搞清楚