ArrayList数据结构、扩容原理进来瞅瞅?

274 阅读6分钟

本文探究的问题

  • 从ArrayList的构造函数分析数据结构。

  • 底层动态扩容的过程和加载因子。

  • ArrayList有哪些线程安全问题?有哪些手段来规避线程安全问题?

提醒:各位看官姥爷,注意看我代码里面的注释哈。

ArrayList的初始化

1.无参构造 - ArrayList()

ArrayList arrayList = new ArrayList();

//先上结论:在无给定大小的情况下初始化数组的长度是 0 ;在第一次添加的时候才会给定 10 的长度

transient Object[] elementData; //ArrayList存放数据的数组 空数组

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; 

//无参构造
public ArrayList() {        
  this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;  //集合初始化一个空数组 
}

简单一看我们就得到这样的信息:构造函数有两个变量elementData、DEFAULTCAPACITY_EMPTY_ELEMENTDATA。而且ArrayList的底层是由数组实现的,且我们没有给初始长度的时候默认给的是一个空数组;

[问题:空的怎么存数据??试试给个初始大小]

2.有参构造 - ArrayList(1)

ArrayList arrayList = new ArrayList(1);

public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];//直接创建给定长度的数组
        } else if (initialCapacity == 0) {//如果是 0的话
            this.elementData = EMPTY_ELEMENTDATA; //还是个空数组
        } else {//其他情况的话 就是非法参数
            throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
        }
}

源码中看到还是能传0的啊?那么我们add()加个数据试试,看看什么情况!

//新增数据 
public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!! ??? 这干嘛的???
        elementData[size++] = e;
        return true;
    }

private void ensureCapacityInternal(int minCapacity) { 
    //这里会有判断的啊,如果是空的就从 DEFAULT_CAPACITY, minCapacity 选个大的。
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        
		//private static final int DEFAULT_CAPACITY = 10;  这里才给个大小为10的啊。        
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    ensureExplicitCapacity(minCapacity);
}

关于初始数组为0,为啥也能添加的数据的过程已经get到了,下一个构造函数看看。

3.集合类参数 - ArrayList(Collection<? extends E> c)

参数说明:

Collection:集合的子类可以当做参数

.<? extends E > :泛型,必须是E的子类

 public ArrayList(Collection<? extends E> c) {
     
        elementData = c.toArray();//将参数c(c是个数组)转换成数组
     
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)??这什么意思??
            //c.toArray() 有坑??
            if (elementData.getClass() != Object[].class) 
                //当真的出现 c.toArray() ,jdk 也做了处理方案。拷贝复制!!
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA; //这不就是相当于无参构造了嘛,弄了个寂寞
        }
    }

小小脑袋,大大的问号。不禁感慨自己太菜!!!

c.toArray()会有什么坑????[谁能告诉我?]

elementData.getClass()与Object[].class ,class 比较又是个什么玩意?(* ̄︶ ̄)

搁置疑问,接着看扩容吧... ...

动态扩容的过程

要想动态的扩容,肯定是在添加数据的时候。集合自己判断要不要扩容,去看看add咋搞的。

扩容吧小宝贝 - add(E e)

ensureCapacityInternal(size + 1) 似曾相识啊,初始化 为0的时候为啥能添加数据,刚刚看过。

private int size;//记录集合的实际存放的元素个数;

public boolean add(E e) {
    //size+1 的目的:因为是添加元素,所以就要把当前数组的实际长度增加1           
    //看看是否超出了集合的当前容量
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    
    //当前存放数组元素个数+1
    elementData[size++] = e;
    return true;
}

来再看下 ensureCapacityInternal(int minCapacity)

private static final int DEFAULT_CAPACITY = 10;//默认容量10;

private void ensureCapacityInternal(int minCapacity) {
    //如果当前数组是一个空数组的话,就取两个数的最大值。这里取得是容量10,当第一次使用add方法
    //才去给数组一个固定的容量!可能是为了节约内存吧。脑壳疼。
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    //咳咳,去下面看这个方法的讲解
    ensureExplicitCapacity(minCapacity);
}

老母猪戴胸罩,一套又一套 。来看这个tao~ ensureExplicitCapacity(minCapacity);

//来了,宝?
private void ensureExplicitCapacity(int minCapacity) {
    //这个东西是记录,修改次数的。咱们这次就不管这个东西了
    modCount++;

    //不是从一开始就传进来一个size+1么?到这里才用上,到底扩不扩容就看这次的比较了
    //当前实际大小+1之后大于了数组定义的长度,那肯定不行啊。妥妥的扩容
    if (minCapacity - elementData.length > 0)
        //下面去看扩容解释 
        grow(minCapacity);
}

扩容吧!小宝贝~

//ლ(°◕‵ƹ′◕ლ) 
private void grow(int minCapacity) {
    //咱们先把原数组的容量存一下
    int oldCapacity = elementData.length;
    //这里看出来是扩容多少倍没? 
    // 新容量 = 原容量 + (原容量/2^1)
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    //这里判断这个为什么?假设初始容量设置为1,,扩容后还是1!所以需要这个判断
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    //超出宇宙限制了,宇宙装不下你了,就给你整个宇宙吧。
    //赋值最大值
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    //划重点,划重点。
    //解释:把原来的数组elementData 拷贝一份 给elementData,且容量变为newCapacity
    elementData = Arrays.copyOf(elementData, newCapacity);//可真是一套一套又一套
}

Arrays.copyOf (elementData, newCapacity)看着有点眼熟啊!在 3.集合类参数 - ArrayList(Collection<? extends E> c) 中有个他兄弟 Arrays.copyOf(elementData, size, Object[].class); 忘了嘛?[赶紧关注点赞~]

 public static <T> T[] copyOf(T[] original, int newLength) {
     //真是个不干活的家伙,我们接着tao~
     return (T[]) copyOf(original, newLength, original.getClass());//前后呼应,nice~
 }
 public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
    //源码里面也用这个啊,啧啧啧啧~
     @SuppressWarnings("unchecked")
     T[] copy = ((Object)newType == (Object)Object[].class)
         ? (T[]) new Object[newLength]
         : (T[]) Array.newInstance(newType.getComponentType(), newLength);
     //上面那一坨坨不想讲了,累了
     
     //看看这个终极boss 吧。
     System.arraycopy(original, 0, copy, 0,
                      Math.min(original.length, newLength));
     return copy;
    }
//芭比Q了,native了。没得看了
public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);

没关系。我已经从别人家的博客文章学会了。我可以的

怎么来对应参数含义呢?(按顺序解释)

  • src:被复制的原数组

  • srcPos:开始复制的下标

  • dest:接收的数组

  • destPos:从哪个位置开始接收数据

  • length:复制长度是多少

莫着急,下面用图说话

进化吧小宝贝 - add(int index, E element)

这个是按照下标位置进行插入的,别急看完这个你就不用芭比Q了。

public void add(int index, E element) {
    //是否存在index下标
    rangeCheckForAdd(index);
    //这个不说了,看上面的add(E e)讲解
    ensureCapacityInternal(size + 1);  // Increments modCount!!

    //这里就是扩容的原理,在Arrays.copyOf()就是调用的这个方法   
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    elementData[index] = element;
    size++;
}
    
//下标判断
private void rangeCheckForAdd(int index) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

//又来到这里
public static native void arraycopy(Object src,  int  srcPos,
                                    Object dest, int destPos,
                                    int length);

假设原数组 elementData[] = {"a","b","c","d",""}(已经扩容了,最后一位为空),现在想在下标为1的位置插入“w”。最终为elementData[] = {"a","w","b","c","d"}

image.png

//我们再来解释下参数的含义

System.arraycopy(elementData, index, elementData, index + 1, size - index);

elementData:被复制的原数组

index:开始复制的下标(从这里开始复制,我例子中的"b"元素开始复制)

elementData:接收的数组

index + 1:从哪个位置开始接收数据(我例子中的"c"元素开始复制)

size - index:复制长度是多少

上面数组复制结束后应该是 elementData[] = {"a","b","b","c","d"}

然后elementData[index] = element;

最终elementData[] = {"a","w","b","c","d"}

至于这个过程,已经不需要java代码来控制了。明白这个东西就好了。

扩容后的长度为啥是1.5倍?

我不明白,我解释不来~

因为1.5 可以充分利用移位操作,减少浮点数或者运算时间和运算次数。 抄的(^▽^)

int newCapacity = oldCapacity + (oldCapacity >> 1);

看看别人咋解释的。

www.cnblogs.com/fortunely/p…

线程不安全问题

由于jdk并没有对ArrayList的方法、成员变量,加上synchronized 或其他线程安全的处理,导致ArrayList是线程不安全的类

异常模拟代码,如果出不来。再多开个线程。

 public static void main(String[] args) throws InterruptedException {
      final List<Integer> list = new ArrayList<Integer>();

      new Thread(()->{
          for (int i = 1; i < 1000; i++) {
              list.add(i);
              try {
                  Thread.sleep(1);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
      }).start();

      new Thread(()->{
          for (int i = 1; i < 1000; i++) {
              list.add(i);

              try {
                  Thread.sleep(1);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
      }).start();

      new Thread(()->{
          for (int i = 1; i < 1000; i++) {
              list.add(i);

              try {
                  Thread.sleep(1);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
      }).start();

      Thread.sleep(1000);

      // 打印所有结果
      for (int i = 0; i < list.size(); i++) {
          System.out.println("第" + (i + 1) + "个元素为:" + list.get(i));
      }
  }
  • 下标越界 image.png

  • 数据为null

image.png

  • 数量不对

image.png

数组下标越界

为什么会出现数组下标越界呢?

由于ArrayList添加元素是如上面分两步进行,可以看出第一个不安全的隐患,在多个线程进行add操作时可能会导致elementData数组越界。

具体逻辑如下:

  1. 列表大小为9,即size=9
  2. 线程A开始进入add方法,这时它获取到size的值为9,调用ensureCapacityInternal方法进行容量判断。
  3. 线程B此时也进入add方法,它获取到size的值也为9,也开始调用ensureCapacityInternal方法。
  4. 线程A发现需求大小为10,而elementData的大小就为10,可以容纳。于是它不再扩容,返回。
  5. 线程B也发现需求大小为10,也可以容纳,返回。
  6. 线程A开始进行设置值操作, elementData[size++] = e 操作。此时size变为10。
  7. 线程B也开始进行设置值操作,它尝试设置elementData[10] = e,而elementData没有进行过扩容,它的下标最大为9。于是此时会报出一个数组越界的异常ArrayIndexOutOfBoundsException.

元素值覆盖和为空问题

elementData[size++] = e 设置值的操作同样会导致线程不安全。从这儿可以看出,这步操作也不是一个原子操作,它由如下两步操作构成:

elementData[size] = e;
size = size + 1;

在单线程执行这两条代码时没有任何问题,但是当多线程环境下执行时,可能就会发生一个线程的值覆盖另一个线程添加的值,具体逻辑如下:

  1. 列表大小为0,即size=0
  2. 线程A开始添加一个元素,值为A。此时它执行第一条操作,将A放在了elementData下标为0的位置上。
  3. 接着线程B刚好也要开始添加一个值为B的元素,且走到了第一步操作。此时线程B获取到size的值依然为0,于是它将B也放在了elementData下标为0的位置上。
  4. 线程A开始将size的值增加为1
  5. 线程B开始将size的值增加为2

这样线程AB执行完毕后,理想中情况为size为2,elementData下标0的位置为A,下标1的位置为B。而实际情况变成了size为2,elementData下标为0的位置变成了B,下标1的位置上什么都没有。并且后续除非使用set方法修改此位置的值,否则将一直为null,因为size为2,添加元素时会从下标为2的位置上开始。

线程安全处理

1 Collections.synchronizedList

List<Object> list =Collections.synchronizedList(new ArrayList<Object>);

2 为list.add()方法加锁

synchronized(list.get()) { list.get().add(model); }

3 CopyOnWriteArrayList

使用线程安全的 CopyOnWriteArrayList 代替线程不安全的 ArrayList。

List<Object> list1 = new CopyOnWriteArrayList<Object>();

4 使用ThreadLocal

使用ThreadLocal变量确保线程封闭性(封闭线程往往是比较安全的, 但由于使用ThreadLocal封装变量,相当于把变量丢进执行线程中去,每new一个新的线程,变量也会new一次,一定程度上会造成性能[内存]损耗,但其执行完毕就销毁的机制使得ThreadLocal变成比较优化的并发解决方案)。

ThreadLocal<List<Object>> threadList = new ThreadLocal<List<Object>>() {
    @Override          
    protected List<Object> initialValue() {               
    	return new ArrayList<Object>(); 
    }  
  ;

上面涉及了并发编程,JUC下的一些类。必须开个专题来记录一下。

看到这了,点个赞吧。~