关于ArrayList和CopyOnWriteArrayList,线程不安全与安全----------干戈的想法

ArrayList类

关于这个类想必大家已经非常熟悉了,是我们编写代码的好帮手。今天我们来从源码的角度在深入熟悉我们的ArrayList.并且对比CopyOnWriteArrayList比较两者之间区别。首先我们先来看ArrayList。

ArrayList的构造函数

// 这是类的成员们
	// DEFAULT_CAPACITY这个是默认数组的大小。前提是我们并没有设置数组的长度
    private static final int DEFAULT_CAPACITY = 10;
    // 下面的这两个数组,这是用来分辨elementData数组,后面再说
    private static final Object[] EMPTY_ELEMENTDATA = {};
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    // 元素真正存放的地方
    transient Object[] elementData;
    // 数组里面元素的个数。
    private int size;

这两种初始化方法,一种是用户设置了数组大小,一种是没有设置数组大小。没有设置大小的话,那么数组默认为DEFAULTCAPACITY_EMPTY_ELEMENTDATA ,并且默认大小为10;如果设置了大小,且大于0,那么会为数组初始化设置的大小的数组;如果设置的大小为0,那么数组默认为EMPTY_ELEMENTDATA 。默认大小为0;

可是不论是EMPTY_ELEMENTDATA 还是DEFAULTCAPACITY_EMPTY_ELEMENTDATA ,数组一开始的长度为0。

	public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

添加一个元素

   public boolean add(E e) {
   // 在添加一个元素的时候,首先要判断是否需要进行扩容
        ensureCapacityInternal(size + 1);  
        // 将元素加入
        elementData[size++] = e;
        return true;
    }

我们着重来看一下扩容这部分,这分了两种情况:

  1. 当数组是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,刚一开始加第一个元素时,minCapacity为1,从1和10之间选出最大的数作为现在数组的最小容量。现在数组的实际长度为0,当然需要扩容,并且我们需要将数组扩容到10.
  2. 当数组不为DEFAULTCAPACITY_EMPTY_ELEMENTDATA
    此时数组的实际长度要不是用户设置的大小,要不是0;

如果是用户设置的大小,那么只要minCapacity比这个大小小,那么就不需要扩容。

但如果用户设置的大小为0那么从第一次开始,以后每一次都需要扩容。前面几种情况下,只有等到数组满了之后才开始进行扩容,且新扩容的大小是newCapacity = oldCapacity + oldCapacity / 2;比如之前是10 扩容之后就是15;最后的这种情况每一次加元素都要扩容,可想而知有多麻烦。

人家好歹一次扩容,还能歇一会。最后这个,每次都要扩容。

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

        ensureExplicitCapacity(minCapacity);
    }

    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    
    private void grow(int minCapacity) {
     int oldCapacity = elementData.length;
     int newCapacity = oldCapacity + (oldCapacity >> 1);
     if (newCapacity - minCapacity < 0)
         newCapacity = minCapacity;
     if (newCapacity - MAX_ARRAY_SIZE > 0)
         newCapacity = hugeCapacity(minCapacity);
     elementData = Arrays.copyOf(elementData, newCapacity);
 }

删除一个元素

    public E remove(int index) {
    // 当前下标是否超出边界
        rangeCheck(index);

        modCount++;
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved > 0)
        // 将数组从index + 1处的到最后一个元素,挪到数组从index下标开始的地方
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

CopyOnWriteArrayList

这个类是为了弥补ArrayList的不足。因为ArrayList在多线程的情况下,无法保证线程安全。如果一个线程加数据,一个线程删数据,那很有可能会出现下面这种错误。

在这里插入图片描述

线程1将m放入最后一格中,上面我们说过,加元素之前要先判断是否需要扩容,我们现在还不需要扩容。但是很不幸的,我们执行完这一句时间片段到了,此时的size为9
而线程2要进行删除,假如要删除d,这个线程得到了运行机会,当他已经删除了d之后,但是还没有将size减1,也没有将8位置上的值赋值为null时间片段就到了。
回到线程1,现在要将m加到index为9的那,size变为10。线程1结束,线程2开始将size-1,并且将size-1下标处的元素赋值为null;

那么怎么解决这个问题呢,CopyOnWriteArrayList这个类就可以很好地解决。

添加一个元素

这里面涉及到一个很重要的类ReentrantLock ,作用和synchronized很相似,lock和unlock相当于synchronized的左右花括号。但还是有一些不同的有兴趣的同学可以去看他的源码,这里就不做过多介绍。

CopyOnWriteArrayList不需要进行扩容。

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

删除一个元素

    public E remove(int index) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            E oldValue = get(elements, index);
            int numMoved = len - index - 1;
            // 这表示要删除的是数组的最后一个元素
            if (numMoved == 0)
                setArray(Arrays.copyOf(elements, len - 1));
            else {
            // 产生一个新的数组,长度为之前的长度减一
                Object[] newElements = new Object[len - 1];
                System.arraycopy(elements, 0, newElements, 0, index);
                System.arraycopy(elements, index + 1, newElements, index,
                                 numMoved);
                setArray(newElements);
            }
            return oldValue;
        } finally {
            lock.unlock();
        }
    }

我们发现现在不论是删除还是添加,都已经加上了锁,并且是同一把锁,这样就可以保证线程安全性了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值