CopyOnWriteArrayList Vector及ArrayList线程安全源码分析

前言

      今天,我们接着上一篇文章继续聊ArrayList。我们今天的研究重点将放在ArrayList的线程安全实现类上,具体包括:CopyOnWriteArrayList、Vector和Collections.synchronizedList。
      关于三者的大致关系,在上一篇文章中,我们已经做了大致梗概的描述,现在我们来重新梳理一下。首先Vector类为与ArrayList类属于同一个包,并且同时继承于java.util.AbstractList,Vector类在实现上也与ArrayList类基本一致,只是在需要同步的操作上进行了线程安全的同步。另一方面,为了进行读的优化,Java在java.util.concurrent包中提供了CopyOnWriteArrayList类,该类的最大特点为写时复制。最后,在Java的设计之初,就提供了Collections.synchronzedList的实现,来对ArrayList提供同步实现。
      通过上面的分析,我们大致明白了这三个类的大体关系,我们在接下来的文章中,将详细分析各个类的重要特性和具体代码的实现逻辑。

CopyOnWriteArrayList类的特性分析

     CopyOnWriteArrayList类的最大特性就是基于写时复制的无阻塞式读操作。简单来说,CopyOnWriteArrayList类在提供读操作时是非阻塞的,在写操作上,需要进行加锁,因此在读多写少的场景中,该类可以具有非常好的性能。接下来,我们将详细分析CopyOnWriteArrayList的写时复制特性。
      首先来描述一下,什么是写时复制,具体的操作即是,在进行写操作、重置操作和删除操作时,首先使用锁进行同步限制,在同步语句块中,进行原数组的拷贝,然后在拷贝上进行具体的相关操作(写、删除和修改),完成后将原数组的引用指向该拷贝数组,完成操作并退出同步语句。因此,在读操作时,可以透明的使用同一个数组引用,而不用关心当前是否有写操作正在进行。通过分析,我们大致可以了解得到,这样的写时复制机制,可以保证任何时候的读操作都是非阻塞的,但是缺点也非常明显,会出现脏读的问题。但是CopyOnWriteArrayList可以保证数据的最终一致性。

我们通过下图来对写时复制做一个更形象的理解:

      如上图所示,读线程不需要阻塞式的获取数组引用,而写线程需要获取互斥锁进行具体的写操作,而该互斥锁的实现具体是可重入锁(ReentrantLock),在后文的代码分析中,我们将可以进一步的了解其原理。写操作的互斥可以保证多线程环境下,每一个线程的写操作都能被读线程引用到,且不发生写冲突。

      如上所示,在写线程的同步块结束时,会将数组的引用指向新的数组,此时读线程获取到的即是最新的数据。同时老数组由于不再有引用的产生,将会被GC进行回收,从而完成写操作。
      通过上面的分析可知,基于写时复制的CopyOnWriteArrayList类可以获得很好的读性能,但是由于写时复制过程中需要对数组进行复制操作,因此,如果数组非常大或者写操作非常频繁,该类的性能会是非常差的,因此需要根据具体的使用场景,决定是否使用该类。

CopyOnWriteArrayList类核心函数分析

首先我们来看两个该类的核心成员变量:

//全局写操作的互斥锁
final transient ReentrantLock lock = new ReentrantLock();
//用于元素存储的数组
private transient volatile Object[] array;
      可以看到,使用final和transient修饰的可重入锁lock,该锁在全局范围内,控制对数组array的写操作。fianl的修饰保证了该变量的访问是线程安全的,transient的修饰,保证了该变量的安全性,也表明该变量是不可序列化的。同时数组array是通过volatile进行修饰的,保证了每次读取该变量时,都是从主存中读取的。
ps:关于volatile和ReentrantLock的详细分析,请继续关注博客的更新,后续会有对Java多线程部分的详细分析。
接下来我们来看一看该类的核心函数:

1)set函数

public E set(int index, E element) {
        //首先获取全局可重入锁
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            //获得当前数组的引用
            Object[] elements = getArray();
            //获得老数组中对应索引位置的值
            E oldValue = get(elements, index);
            //若新值和旧值不相等,则进行基于写时复制的写操作
            if (oldValue != element) {
                int len = elements.length;
                //通过数组工具类Arrays.copyOf进行数组的复制
                Object[] newElements = Arrays.copyOf(elements, len);
                newElements[index] = element;
                setArray(newElements);
            } else {
                //保证volatile的写语意,重置elements函数的引用
                setArray(elements);
            }
            return oldValue;
        } finally {
            lock.unlock();
        }
}

      如果仔细研读了上文中对写时复制的描述,那么该set函数的逻辑就非常简单了。其中需要注意的是,set操作并非直接替换索引位置的值,而是做了一次值比较,值不同时进行替换操作,值相等时,为确保volatile语意,还是调用了setArray函数,进行引用的替换(其实引用并没有发生变化)。
      此处说一下我的理解,可能存在偏差,若有不正之处望各位指出。此处在值相等时,为了确保的set函数的语义完整执行,才依旧调用了setArray函数对elements进行赋值。因为全局锁只会保护该函数的互斥执行,但是针对setArray函数,并不会互斥执行,也就是说别处的其他函数依旧会调用该函数来修改array的引用,造成set函数在值相等的情况下,数组引用依旧发生了变化,因此为保证set函数能够在值不变的时候,数组的引用也不发生变化,所以才最后调用了setArray函数,确保array引用没有发生变化。
      本函数中,还有一个不确定的点就是对于共享变量array的引用了,为什么不直接去使用array引用,而是定义了一个getArray的函数,具体代码如下:

final Object[] getArray() {
        return array;
    }

望CSDN大神能指出此处设计之原由,小弟不胜感激~

2)add函数:

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

      若仔细研读了set函数,该add函数则更为简单,同样也是写时复制的代码逻辑。具体的代码逻辑,在如上注释所示,就不做过多分析了。

CopyOnWriteArrayList类总结

      关于CopyOnWriteArrayList类的分析就到此为止,我们在源码分析上,也主要强调了其写时复制的特性,而未完全的从代码框架逻辑上对其分析。具体代码框架逻辑,请参考本人的ArrayList源码分析部分的分析。两者之间,在数组元素的插入、删除、修改和查找上近乎是一样的,因此在本文中不做过多的赘述了。
      关于此类,最令人激动的设计也就是写时复制了。通过写时复制的机制,可以实现数组的无阻塞式读操作,这种特性也常常应用在数据库系统的设计中,例如MySQL的InnoDB存储存储引擎提供的MVCC机制,该机制也是通过多版本并发控制,来保证非阻塞的读操作。关于写时复制机制,在上文中,已经分析的很清楚了,望各位看官能仔细研读透彻,并结合代码,了解该机制是如何实现的。

      一句话总结一下:CopyOnWriteArrayList应用于读多写少的使用场景,在读上具有非常好的性能,在写上会花费一定的代价。当数据量较大或者写操作频繁时,都不建议使用该类。

Vector类的特性分析

      关于Vector类的介绍,我们也将着重放在其如何实现线程安全上,至于该类对于数组操作的支持,与ArrayList基本一致,因为两者实现了同样的抽象类和相关接口,因此两者实现的函数都基本是一致的,只是Vector做了同步操作而已。

      该类已经存在非常久远了,在JDK1.0中,就已经使用该类来作为ArrayList线程安全的支持版本,因此关于其特性分析也是与ArrayList的使用是一致的,所以我们就直接来看其如何实现线程安全的吧。

Vector类核心函数分析

    //元素添加
    public synchronized boolean add(E e) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
    }
    //元素删除
    public synchronized boolean removeElement(Object obj) {
        modCount++;
        int i = indexOf(obj);
        if (i >= 0) {
            removeElementAt(i);
            return true;
        }
        return false;
    }
    //元素修改
    public synchronized void setElementAt(E obj, int index) {
        if (index >= elementCount) {
            throw new ArrayIndexOutOfBoundsException(index + " >= " +
                                                     elementCount);
        }
        elementData[index] = obj;
    }
    //元素大小
    public synchronized int size() {
        return elementCount;
    }
      如上所示,Vector实现线程安全的方法就已经非常明显了,即将各个需要同步的函数使用synchronized进行修饰。其中Vector中还有部分没有使用synchronized修饰的函数,是因为这些函数是同步函数的辅助函数,且调用也只会存在同一个函数中,不存在多处调用,则不需要进行重复的同步了。

Vector类总结

      综上所述,Vector类的线程安全就是通过synchronized修饰同步函数来实现的。所以可以了解到,该类的性能其实是非常差的,一旦涉及到需要同步的地方,都是同步的执行。最终造成该类相比于ArrayList来说,性能差了很多。因此,很多地方已经不再支持使用该同步类了。

Collections.SynchronizedList分析

      该实现为Collections工具类中的静态内部类,相比于Vector对函数直接使用synchronized做同步,该工具类提供的同步方案是使用同一个变量mutex做锁,由synchronized基于该锁实现同步代码块。
      除此之外就没有其他特别的地方了,如果各位读者有兴趣可以详细的去了解一下该类的实现,我们此处只简单贴出一段该内部类的代码,给各位做了解。

static class SynchronizedList<E>
        extends SynchronizedCollection<E>
        implements List<E> {
        private static final long serialVersionUID = -7754090372962971524L;

        final List<E> list;

        SynchronizedList(List<E> list) {
            super(list);
            this.list = list;
        }
        public boolean equals(Object o) {
            if (this == o)
                return true;
            synchronized (mutex) {return list.equals(o);}
        }
        public E get(int index) {
            synchronized (mutex) {return list.get(index);}
        }
        public E set(int index, E element) {
            synchronized (mutex) {return list.set(index, element);}
        }
        public void add(int index, E element) {
            synchronized (mutex) {list.add(index, element);}
        }
        public E remove(int index) {
            synchronized (mutex) {return list.remove(index);}
        }

        ....

}

后言

      关于ArrayList类的线程安全支持,在上文中我们已经做了详细的分析,除了使用CopyOnWriteArrayList基于写时复制的方式能够提高读性能以外,其他两种线程安全方案都是基于简单的同步来实现的,即通过synchronized修饰函数和synchronized修饰程序代码块。

      关于这一部分的分析,我们最大的收获,就是写时复制机制了。所谓写时复制,本质也是采用读写分离的方案,来解决读写冲突,这种方案在数据库中也使用的非常多,最著名的就是数据库的读写分离,来减轻数据库的访问压力,该方案所存在的问题也较为明显,存在一定时延范围内的脏读,但是可以确保数据的最终一致性。这又提出了关于CAP问题的权衡,这些更加深入的设计与架构问题,其实早早的在Java类的设计中就已初见端倪。Java类的设计也不断的借鉴和使用其他更加高级的架构和设计方案,同时自身也提出了非常多的令人振奋的设计手法,因此在Java的源码分析中,可以不断的收获惊喜,提高架构和设计的能力,为今后从事更高层次的工作提供基础。
      因此,希望更多的朋友能够加入到Java源码分析的行列中,支持本系列博客,同时有任何错误的地方,请望指出,谢谢各位的支持~






  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
ArrayList本身是线程不安全的数据结构。这意味着当多个线程同时访问和修改ArrayList时,可能会导致数据不一致或者发生其他的并发问题。然而,我们可以采取一些措施来使ArrayList变得线程安全。以下是几种常见的线程安全ArrayList实现方式: 1. 使用Collections类的synchronizedList方法:通过使用Collections类的synchronizedList方法,可以将ArrayList转换为线程安全的List。这个方法返回一个线程安全的List,它会在每个公共方法上加锁,从而保证多线程访问的安全性。但是,这种方式会造成性能的下降,因为它会对整个List进行加锁,导致多个线程不能并发执行。 2. 使用CopyOnWriteArrayList类:CopyOnWriteArrayList是Java.util.concurrent包下提供的线程安全ArrayList实现。它的特点是,在进行写操作(如添加、删除元素)时,会创建一个新的数组,并将原来的数组复制过去,然后再进行修改。这样可以保证在写操作期间,其他线程可以继续读取原来的数组而不会出现问题。然而,由于写操作需要复制整个数组,因此性能相对较低。适用于读操作频繁,写操作较少的场景。 3. 使用线程安全的List实现:除了CopyOnWriteArrayList,还可以使用其他实现线程安全的List接口的类,如Vector和ConcurrentLinkedArrayList。它们都是线程安全的,但在性能和用法上可能会有所不同。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值