java之CopyOnWriteArrayList源码分析

今天我们来分享一下与java相关的CopyOnWriteArrayList的技术点,可能有的人是第一次见到这个,也有可能早已了解,今天我们就来对其源码进行分析,来熟悉和了解一下什么是写时复制容器。

好了,按照以往的写作文章风格,到这里我们就还是按照写示例程序一步一步去进行接下来的流程了。

package com.wpw.asyncthreadpool;


import java.util.concurrent.CopyOnWriteArrayList;


public class CopyOnWriteArrayListTest {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> copyOnWriteArrayList=new CopyOnWriteArrayList<>();
        copyOnWriteArrayList.add("abcd");
        System.out.println(copyOnWriteArrayList.size());
        System.out.println("copyOnWriteArrayList = " + copyOnWriteArrayList);
    }
}


首先我们创建了一个JVM实例进程,我们创建了CopyOnWriteArrayList对象集合,接着我们调用了其提供的add()方法,输出我们集合元素的大小和内容,由于这里重写了toString()方法,所以打印出的内容都可以看懂。

接下来我们看下我们创建一个集合对象CopyOnWriteArrayList的时候发生了什么。

 /**
     * Creates an empty list.
     */
    public CopyOnWriteArrayList() {
        setArray(new Object[0]);
    }

第一步,我们创建集合的时候调用setArray()方法,接下来我们看下setArray()方法做了什么操作咯。

 /**
     * Sets the array.
     */
    final void setArray(Object[] a) {
        array = a;
    }
    //这一步的操作就是将传进来的数组的引用赋予array,那么array是什么呢,接下来我们看下array是什么
/** The array, accessed only via getArray/setArray. */
    private transient volatile Object[] array;//这是一个Object类型的对象数组,数组的目的就是用来装填集合元素的。
    

其实集合的本质就是数组,所以你会了数组操作,集合的操作也在你的掌握之下,说这句话是不是有点狂了,坏笑,别当真,这是句玩笑话,开心就好。好了,我们创建CopyOnWriteArrayList集合对象的流程分析已经走完了,看懂了吧,接下来我们继续往下面看。

首先我们想要在集合CopyOnWriteArrayList集合容器中添加一个元素,我们应该看下其提供的add()方法,这是我们看下我们写的示例程序。

 public static void main(String[] args) {
        CopyOnWriteArrayList<String> copyOnWriteArrayList=new CopyOnWriteArrayList<>();
        copyOnWriteArrayList.add("abcd");
        System.out.println(copyOnWriteArrayList.size());//集合元素大小size=1
        System.out.println("copyOnWriteArrayList = " + copyOnWriteArrayList);copyOnWriteArrayList = [abcd]
    }

接下来我们看下这个集合的add()方法的程序代码。

/**
     * Appends the specified element to the end of this list.
     */
    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);//调用setArray()方法将新数组引用替代原有数组引用,这时的数据就指向了最新的数组引用,大致上和C语言的指针很像
            return true;//上述操作完成之后就返回true,表示操作成功
        } finally {
            lock.unlock();//最后在finally语句块里进行解锁操作,这时此线程对集合的add操作就结束了。
        }
    }

首先add()方法的作用就是将一个指定的元素对象添加到集合的末尾,在add()方法这里面主要设计到操作,为什么要加锁?加锁的目的是为了防止高并发多线程情况下会拷贝出多个数组副本出来,加锁就保证了一个线程在操作完成之前,其他写操作线程只能处于等待状态。

在上面的add()方法的代码里面我对其进行了每一行代码的注释说明,看完之后你对add()方法的流程分析也就掌握了,好了,我们继续看下其它方法了。

既然上面的示例程序中我们已经使用的获取集合大小的size()方法,在这里我们就对其进行分析好了。看下程序代码咯。

/**
     * Returns the number of elements in this list.
     *
     * @return the number of elements in this list
     */
    public int size() {
        return getArray().length;
    }
    /**
     * Gets the array.  Non-private so as to also be accessible
     * from CopyOnWriteArraySet class.
     */
    final Object[] getArray() {
        return array;
    }

size()方法就是获取集合列表中数据元素个数,也就是数组中已存在元素的数量。

接下来我们继续看CopyOnWriteArrayList集合提供的contains()方法的程序代码,contains()方法见名知意,就是判断集合列表中是否包含指定的元素,包含返回true,不包含返回fasle。

  public boolean contains(Object o) {
        Object[] elements = getArray();//获取数组的引用,然后调用indexOf()方法
        return indexOf(o, elements, 0, elements.length) >= 0;
   }

上面的功能在代码里面已注释说明,接下来我们来看下indexof()方法的实现吧。

 private static int indexOf(Object o, Object[] elements,
                               int index, int fence) {
        if (o == null) {//由于集合里面可以添加null值的元素,所以在这里需要对null进行判断
            for (int i = index; i < fence; i++)
                if (elements[i] == null)
                    return i;//在数组元素为null的情况下进行循环遍历和判断,找到了就返回数组的索引下标
        } else {
            for (int i = index; i < fence; i++)
                if (o.equals(elements[i]))
                    return i;//返回数组索引的下标值
        }
        return -1;//如果上面的两种情况都不存在就返回-1,这样在contains()方法调用indexof()方法时就与0进行比较,返回true或者false
    }

上面的indexOf()方法就是循环遍历整个数组,然后分两种情况进行判断,因为集合里面是可以添加null值的,所以这一种情况是需要考虑在内的。由于在程序的代码里面已经对方法进行了详细的说明了,自己理解掌握一下。

下面我们继续分析集合的其它方法吧。接下来我们看下集合的clear()方法。

/**
     * Removes all of the elements from this list.
     * The list will be empty after this call returns.
     */
    public void clear() {
        final ReentrantLock lock = this.lock;//获取锁对象
        lock.lock();//加锁操作
        try {
            setArray(new Object[0]);//我们看下下面的setArray()方法的操作
        } finally {
            lock.unlock();//解锁操作,也就是释放锁的操作
        }
    }
    
    /**
     * Sets the array.
     */
    final void setArray(Object[] a) {
        array = a;
    }
    我们继续看下array是啥?是一个空数组的引用。
    private transient volatile Object[] array;

clear()方法也是需要加锁操作的,因为避免多线程情况下同时对集合数据进行清理带来的问题。好了,这里的clear()方法的程序代码流程我们就分析完了。

集合常用的方法判空操作也是很常用的,接下来我们继续对集合提供的isEmpty()方法进行分析了。

public boolean isEmpty() {
        return size() == 0;//判断集合列表的size值是否等于0
 }

在项目中,我们有的时候会根据指定的索引值进行集合元素的获取,这是我们可以用到集合的get()方法了。

 public E get(int index) {
        return get(getArray(), index);//调用了get()方法,我们看下get方法的实现
 }
private E get(Object[] a, int index) {
        return (E) a[index];//根据数组下标索引获取元素值
  }

在这里说下,索引一定要注意,不能超过集合的索引大小,或者小于集合的索引值,不然就会抛出索引越界异常信息了,一般在项目里面使用时自己注意一下就行了。

既然可以根据索引下标依靠get方法获取元素值,同理,我们可以根据元素值来获取该元素的索引下标,这时我们就用到了indexOf()方法。

public int indexOf(Object o) {
        Object[] elements = getArray();
        return indexOf(o, elements, 0, elements.length);//在这里调用下面的indexOf方法
    }
private static int indexOf(Object o, Object[] elements,
                               int index, int fence) {
        if (o == null) {
            for (int i = index; i < fence; i++)
                if (elements[i] == null)
                    return i;
        } else {
            for (int i = index; i < fence; i++)
                if (o.equals(elements[i]))
                    return i;
        }
        return -1;
    }

上面的indexOf()方法就是循环遍历数组,分别针对null和非null情况进行判断,找到了就返回对应的索引下标,找不到就返回-1。

我们看下下面的这个方法好了,lastIndexOf()方法,这个方法的程序代码我们还是继续分析下好了,其实在这里说下,这篇文章很长,因为我要对每一个方法都要进行分析一下,看下它提供了哪些,这样我们在使用集合的时候可以针对具体情况使用不同的方法去解决问题。

public int lastIndexOf(Object o) {
        Object[] elements = getArray();
        return lastIndexOf(o, elements, elements.length - 1);
    }

我们继续看下lastIndexOf的程序代码。

private static int lastIndexOf(Object o, Object[] elements, int index) {
        if (o == null) {
            for (int i = index; i >= 0; i--)
                if (elements[i] == null)
                    return i;//针对null值进行判断,返回指定元素值的索引下标
        } else {
            for (int i = index; i >= 0; i--)
                if (o.equals(elements[i]))
                    return i;//返回数组的指定元素的索引下标
        }
        return -1;//找不到返回-1
    }

同样的道理,也是分两种情况进行循环判断,其实这个方法和indexOf方法都是循环遍历数组得到数组下标索引值,lastIndexOf方法就是从集合列表的末尾反向查找指定元素的下标。

文章篇幅有点长,需要一些耐心,我们继续吧。

一般的集合都会提供addAll()方法,这个方法是可以一次操作就可以将另外一个集合列表的元素添加到集合中,我们看下addAll()方法吧。由于方法很长,自己就在方法里面注释说明了,自己理解一下就好。

public boolean addAll(Collection<? extends E> c) {
//首先我们判断集合的类型是否是CopyOnWriteArrayList
        Object[] cs = (c.getClass() == CopyOnWriteArrayList.class) ?
            ((CopyOnWriteArrayList<?>)c).getArray() : c.toArray();
        if (cs.length == 0)//如果添加的集合列表为空,返回fasle,也就是说你什么都没给我,我只好给你返回false值了
            return false;
        final ReentrantLock lock = this.lock;//一般对这个集合的修改操作都是要加锁的,这里就是获取锁对象的
        lock.lock();//进行加锁操作
        try {
            Object[] elements = getArray();
            int len = elements.length;//获取原有集合列表的长度
            //下面的判断就是判断长度是否为0,判断是否为Object[]类型
            //长度为0,类型一样,说明原有集合列表为空,直接对其进行操作就好了
            if (len == 0 && cs.getClass() == Object[].class)
                setArray(cs);
            else {
            //若原有的集合操作不为空,先通过工具类拷贝一个数据出来,在拷贝出来的数组上面进行元素的拷贝
            //这里采用了System.arraycopy()方法就是直接调用native关键字修饰额本地方法。
                Object[] newElements = Arrays.copyOf(elements, len + cs.length);
                System.arraycopy(cs, 0, newElements, len, cs.length);
                setArray(newElements);//将原有数组的引用替换为新数组的引用,这样数据就是新数组中的元素集合了。
            }
            return true;//操作完成返回true
        } finally {
            lock.unlock();//进行解锁操作,也就是释放锁操作,使其下一个线程可以获取锁操作
        }
    }

到这里我们再看下集合提供的移除集合元素的remove操作。

public boolean remove(Object o) {
        Object[] snapshot = getArray();
        int index = indexOf(o, snapshot, 0, snapshot.length);
        return (index < 0) ? false : remove(o, snapshot, index);//这一句就根据返回的索引下标是否小于0来进行不同的操作
    }
 private static int indexOf(Object o, Object[] elements,
                               int index, int fence) {
                               //这个方法就是返回指定元素的索引下标的,上面的方法已经介绍过,这里就暂时不分析了
        if (o == null) {
            for (int i = index; i < fence; i++)
                if (elements[i] == null)
                    return i;
        } else {
            for (int i = index; i < fence; i++)
                if (o.equals(elements[i]))
                    return i;
        }
        return -1;
    }
 private boolean remove(Object o, Object[] snapshot, int index) {
        final ReentrantLock lock = this.lock;//获取锁对象
        lock.lock();//加锁操作
        try {
            Object[] current = getArray();
            int len = current.length;//获取当前数组的长度
            //判断传进来的数组引用与当前获取的数组引用是否一样,不一样就进行下面的操作
            if (snapshot != current) findIndex: {
                int prefix = Math.min(index, len);//获取当前数组长度和传进的索引的最小值
                for (int i = 0; i < prefix; i++) {
                    if (current[i] != snapshot[i] && eq(o, current[i])) {
                        index = i;
                        break findIndex;
                    }
                }
                if (index >= len)
                    return false;
                if (current[index] == o)
                    break findIndex;
                index = indexOf(o, current, index, len);
                if (index < 0)
                    return false;
            }
            //下面的方法就是将移除元素后的数组进行向前移动的操作
            Object[] newElements = new Object[len - 1];
            System.arraycopy(current, 0, newElements, 0, index);
            System.arraycopy(current, index + 1,
                             newElements, index,
                             len - index - 1);
            setArray(newElements);//将新数组的引用进行设置
            return true;
        } finally {
            lock.unlock();//解锁操作
        }
    }

我们到这里再说一下集合提供的set操作,这篇文章就要结束了,由于集合的方法很多上面我们介绍了大部分,剩余的自己了解下就行了。

当我在看set这个方法时觉得很有意思,下面我们看下它的程序代码。

public E set(int index, E element) {
        final ReentrantLock lock = this.lock;//进行锁对象的获取
        lock.lock();//进行加锁操作
        try {
            Object[] elements = getArray();//获取数组
            E oldValue = get(elements, index);//根据指定索引获取数组的oldValue值


            if (oldValue != element) {//判断获取的oldValue与当前需要设置的值是否相等,不相等进行下面的操作
                int len = elements.length;
                Object[] newElements = Arrays.copyOf(elements, len);//将原有的数组进行拷贝一份,然后在新数组上进行操作,可以提高并发读的操作
                newElements[index] = element;//将需要设置的元素进行赋值操作
                setArray(newElements);//设置w诶新数组引用,引用的指向替换
            } else {
                // Not quite a no-op; ensures volatile write semantics
                setArray(elements);
            }
            return oldValue;//返回oldValue
        } finally {
            lock.unlock();//解锁操作
        }
    }

此时,到这里我们今天需要分享的文章内容就结束了,喜欢文章的可以关注公众号,分享转发一下。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值