本文 首发于 🍀 永浩转载 请注明 来源

12、【对线面试官】List

要不今天来讲讲Java的List吧,你对List了解多少?

  1. List在Java里边是一个接口,常见的实现类有ArrayList和LinkedList,在开发中用得最多的是ArrayList
  2. ArrayList的底层数据结构是数组,Linked List底层数据结构是链表。

那Java本身就有数组了,为什么要用ArrayList呢?

  1. 原生的数组会有一个特点:你在使用的时候必须要为它创建大小,而ArrayList不用。
  2. 在日常开发的时候,往往我们是不知道要给数组分配多大(不固定)
  3. 如果数组的大小指定多了,内存浪费;如果数组大小指定少了,装不下。
  4. 假设我们给定数组的大小是10,要往这个数组里边填充元素,我们只能添加10个元素。
  5. 而ArrayList不一样,ArrayList我们在使用的时候可以往里边添加20个,30个,甚至更多的元素
  6. 因为ArrayList是实现了动态扩容的
    1. 当我们new ArrayList()的时候,默认会有一个空的Object数组,大小为0。
    2. 当我们第一次add添加数据的时候,会给这个数组初始化一个大小,这个大小默认值为10
    3. 使用ArrayList在每一次add的时候,它都会先去计算这个数组够不够空间
    4. 如果空间是够的,那直接追加上去就好了。如果不够,那就得扩容

那怎么扩容?一次扩多少?

  1. 在源码里边,有个grow方法,每一次扩原来的1.5倍。比如说,初始化的值是10嘛。
  2. 现在我第11个元素要进来了,发现这个数组的空间不够了,所以会扩到15
  3. 空间扩完容之后,会调用arraycopy来对数组进行拷贝

我又想问问,为什么你在前面提到,在日常开发中用得最多的是ArrayList呢?

  1. 是由底层的数据结构来决定的,在日常开发中,遍历的需求比增删要多,即便是增删也是往往在List的尾部添加就OK了。
  2. 像在尾部添加元素,ArrayList的时间复杂度也就O(1)
  3. 另外的是,ArrayList的增删底层调用的copyOf()被优化过
  4. 现代CPU对内存可以块操作,ArrayList的增删一点儿也不会比LinkedList慢

了解,Vector你知道这个吗?

  1. 嗯,Vector是底层结构是数组,一般现在我们已经很少用了。
  2. 相对于ArrayList,它是线程安全的,在扩容的时候它是直接扩容两倍的
  3. 比如现在有10个元素,要扩容的时候,就会将数组的大小增长到20

嗯,那如果我们不用Vector,线程安全的List还有什么?

  1. 首先,我们也可以用Collections来将ArayList来包装一下,变成线程安全。`
  2. 在java.util.concurrent包下还有一个类,叫做CopyOnWriteArrayList
  3. 要讲CopyOnWriteArrayList之前,我还是想说说copy-on-write这个意思,下面我会简称为cow
  4. 比如说在Linux中,我们知道所有的进程都是init进程fork出来的
  5. 除了进程号之外,fork出来的子进程,默认跟父进程是一模一样的。
  6. 当使用了cow机制;子进程在被fork之后exec之前,两个进程用的是相同的内存空间的
  7. 这意味着子进程的代码段、数据段、堆栈都是指向父进程的物理空间
  8. 当父子进程中有更改的行为发生时,再为子进程分配相应物理空间。
  9. 这样做的好处就是,等到真正发生修改的时候,才去分配资源,可以减少分配或者复制大量资源时带来的瞬间延时。
  10. 简单来说,就可以理解为我们的懒加载,或者说单例模式的懒汉式。等真正用到的时候再分配
  11. 在文件系统中,其实也有cow的机制。
  12. 文件系统的cow就是在修改数据的时候,不会直接在原来的数据位置上进行操作,而是重新找个位置修改。
  13. 比如说:要修改数据块A的内容,先把A读出来,写到B块里面去。
  14. 如果这时候断电了,原来A的内容还在。这样做的好处就是可以保证数据的完整性,瞬间挂掉了容易恢复。

你还是回到CopyOnWriteArrayList上吧;你说的cow机制我了解了

  1. 额。CopyOnWriteArrayList是一个线程安全的List,底层是通过复制数组的方式来实现的。
  2. 要不我来简单说说它的add()方法的实现吧
  3. 在add()方法的实现里,首先他会加lock锁锁住然后会复制出一个新的数组,往新的数组里边add真正的元素最后把 array的指向改变为新的数组
  4. get()方法又或是size()方法只是获取array所指向的数组的元素或者大小
  5. 可以发现的是, CopyOnWriteArrayList
  6. 跟文件系统的COW机制是很像的

那你能说说CopyOnWriteArrayList有什么缺点吗?

  1. 很显然, CopyOnWriteArrayList是很耗费内存的,每次set()add()都会复制一个数组出来
  2. 另外就是 Copy On WriteArrayList只能保证数据的最终一致性,不能保证数据的实时一致性。
  3. 假设两个线程,线程A去读 CopyOnWri取 teArrayList的数据,还没读完
  4. 现在线程B把这个List给清空了,线程A此时还是可以把剩余的数据给读出来。