不怕难之ArrayList及其扩展

一、问题导读

1. ArrayList如何扩容? 

2. 什么时候会有ConcurrentModificationException?

3.  当我们一般提到ArrayList的话都会脱口而出它的几个特点:有序、可重复、查找速度快,但是插入和删除比较慢,线程不安全,那么问题来了:为什么说是有序的?怎么有序?为什么又说插入和删除比较慢?为什么慢?还有线程为什么不安全?所以带着这些问题,我们一一的来源码中来找找答案。

二、ArrayList数据结构


三、核心代码

1. add方法

//将指定的元素追加到列表的末尾

public boolean add(E e) {

ensureCapacityInternal(size +1);  // 增量模式计数!!

elementData[size++] = e;

    returntrue;

}

//确定内部容量的方法

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

}

扩容机制



2. Fail-Fast

modCount 用来记录 ArrayList 结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。

ArrayList自己实现了序列化和反序列化的方法,因为它自己实现了 private void writeObject(java.io.ObjectOutputStream s)和 private void readObject(java.io.ObjectInputStream s) 方法

private void writeObject(java.io.ObjectOutputStream s)

    throws java.io.IOException{

    // Write out element count, and any hidden stuff

    int expectedModCount = modCount;

    s.defaultWriteObject();

    // Write out size as capacity for behavioural compatibility with clone()

    s.writeInt(size);

    // Write out all elements in the proper order.

    for (int i=0; i<size; i++) {

        s.writeObject(elementData[i]);

    }

    if (modCount != expectedModCount) {

        throw new ConcurrentModificationException();

    }

}

3. 为什么说是线程不安全的

以add方法为例,整个add()方法可以拆为两步,第一步在elementData[Size] 的位置存放此元素,第二步增大 Size 的值。我们都知道我们的CUP是切换进程运行的,在单线程中这样是没有问题的,但是一般在我们项目中很多情况是在多线程中使用ArrayList的,这时候比如有两个线程,线程 A 先将元素存放在位置 0。但是此时 CPU 调度线程A暂停,线程 B 得到运行的机会。线程B也向此 ArrayList 添加元素,因为此时 Size 仍然等于 0 ,所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增加 Size 的值。这样就会得到元素实际上只有一个,存放在位置 0,而 Size 却等于 2。这样就造成了我们的线程不安全了。

四、COW 隆重登场

CopyOnWriteArrayList是Java并发包中提供的一个并发容器,它是个线程安全且读操作无锁的ArrayList,写操作则通过创建底层数组的新副本来实现,是一种读写分离的并发策略,我们也可以称这种容器为"写时复制器"。

为什么没有ConcurrentModificationException?

1. 实现原理

 集合框架中的ArrayList是非线程安全的,Vector虽是线程安全的,但由于简单粗暴的锁同步机制,性能较差。而CopyOnWriteArrayList则提供了另一种不同的并发处理策略(当然是针对特定的并发场景)。

很多时候,我们的系统应对的都是读多写少的并发场景。通过空间换时间的方式来提高读的效率并保证写的安全性。CopyOnWriteArrayList容器允许并发读,读操作是无锁的,性能较高。至于写操作,比如向容器中添加一个元素,则首先将当前容器复制一份,然后在新副本上执行写操作,结束之后再将原容器的引用指向新容器。


2. 核心代码

add方法

public boolean add(E e) {

//ReentrantLock加锁,保证线程安全

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

            returntrue;

        } finally {

//解锁

lock.unlock();

        }

}

remove方法

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)

                //如果要删除的是列表末端数据,拷贝前len-1个数据到新副本上,再切换引用

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

        }

    }

3. COW缺陷

       CopyOnWrite容器有很多优点,但是同时也存在两个问题,即内存占用问题和数据一致性问题。所以在开发的时候需要注意一下。

  内存占用问题。因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比如说200M左右,那么再写入100M数据进去,内存就会占用300M,那么这个时候很有可能造成频繁的Yong GC和Full GC。

针对内存占用问题,可以通过压缩容器中的元素的方法来减少大对象的内存消耗,比如,如果元素全是10进制的数字,可以考虑把它压缩成36进制或64进制。或者不使用CopyOnWrite容器,而使用其他的并发容器。

  数据一致性问题。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。


五、总结

ArrayList

增删改查中, 增导致扩容,则会修改modCount,删一定会修改。 改和查一定不会修改modCount。

扩容操作会导致数组复制,批量删除会导致 找出两个集合的交集,以及数组复制操作,因此,增、删都相对低效。 而 改、查都是很高效的操作。

和Vector的区别,Vector的内部也是数组做的,区别在于Vector在API上都加了synchronized所以它是线程安全的,以及Vector扩容时,是翻倍size,而ArrayList是扩容50%。

ArrayList自己实现了序列化和反序列化的方法。

COW

CopyOnWriteArrayList中写操作需要大面积复制数组,所以性能肯定很差,但是读操作因为操作的对象和写操作不是同一个对象,读之 间也不需要加锁,读和写之间的同步处理只是在写完后通过一个简单的“=”将引用指向新的数组对象上来,这个几乎不需要时间,这样读操作就很快很安全,适合 在多线程里使用,绝对不会发生ConcurrentModificationException ,所以最后得出结论:CopyOnWriteArrayList适合使用在读操作远远大于写操作的场景里,比如缓存。

疑问点:

为什么CopyOnWriteArrayList的set/remove/add等方法上锁代码不是this.lock.lock(),而非要来弄个引用指向?有心还是无意。CopyOnWriteArrayList的这些方法为什么要通过数组的拷贝来实现?不是已经有锁来保证了安全吗?这样做性能不是很低吗。 

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容