java高并发下CopyOnWriteArrayList替代ArrayList

目录
  • 一、ArrayList线程不安全
  • 二、解决ArrayList线程不安全的方案
    • 1、使用Vector类
    • 2、使用Collections类
    • 3、使用CopyOnWriteArrayList类
  • 三、CopyOnWriteArrayList
    • 1、简介
    • 2、主要方法源码分析
      • 1、初始化
      • 2、添加元素
      • 3、获取指定位置元素
      • 4、修改指定元素
      • 5、删除元素
      • 6、弱一致性的迭代器
  • 四、总结

一、ArrayList线程不安全

在Java的集合框架中,想必大家对ArrayList肯定不陌生,单线程的情况下使用它去做一些CRUD的操作是非常方便的,先来看看这个例子:

public class ListTest {
    public static void main(String[] args) {
        List<String> arrayList = new ArrayList<>();
        arrayList.add("a");
        arrayList.add("b");
        arrayList.add("c");
        for (String s : arrayList) {
            System.out.println(s);
        }
    }
}

其输出结果就是与元素被添加进ArrayList的顺序一样,即:

a
b
c

但是到了多线程的情况下,ArrayList还会像单线程一样执行结果符合我们的预期吗?我们再来看下这个例子:

public class ListTest {
    public static void main(String[] args) {
        List<String> arrayList = new ArrayList<>();
        for (int i = 0; i < 30; i++) {
            new Thread(()->{
                arrayList.add(UUID.randomUUID().toString().substring(0,5));
                //往list中添加一个长为5的随机字符串
                System.out.println(arrayList);
                //读取list
            },"线程"+(i+1)).start();
        }
    }
}

由输出结果:

Exception in thread "线程10" 
Exception in thread "线程1" 
Exception in thread "线程14"
Exception in thread "线程3" 
Exception in thread "线程5" 
Exception in thread "线程2"
Exception in thread "线程6" 
Exception in thread "线程21" 
Exception in thread "线程23" 
Exception in thread "线程28" 
Exception in thread "线程29" 
java.util.ConcurrentModificationException

我们发现,多线程的情况下,有多个线程对ArrayList添加元素,同时又会有多个元素对ArrayList进行元素的读取,这样使得程序抛出了ConcurrentModificationException并发修改异常,所以我们可以下定结论:ArrayList线程不安全!

二、解决ArrayList线程不安全的方案

1、使用Vector类

我们知道,Java集合中的Vector类是线程安全的,可以用Vector去解决上述的问题。

public class ListTest {
    public static void main(String[] args) {
        List<String> arrayList = new Vector<>();
        for (int i = 0; i < 30; i++) {
            new Thread(()->{
                arrayList.add(UUID.randomUUID().toString().substring(0,5));
                //往list中添加一个长为5的随机字符串
                System.out.println(arrayList);
                //读取list
            },"线程"+(i+1)).start();
        }
    }
}

翻看Vector的源码可知,其add方法是使用了synchronized同步锁,同一时刻只允许一个线程对List进行修改。虽然Vector能够保证线程安全,但通过前面几期推文的学习,synchronized的方案一般不是最优选择,会对程序的性能有一定的影响。

2、使用Collections类

Collections类中的synchronizedList方法可以使一个线程不安全的List转为线程安全的。

public class ListTest {
    public static void main(String[] args) {
        List<String> arrayList = Collections.synchronizedList(new ArrayList<>());
        for (int i = 0; i < 30; i++) {
            new Thread(()->{
                arrayList.add(UUID.randomUUID().toString().substring(0,5));
                //往list中添加一个长为5的随机字符串
                System.out.println(arrayList);
                //读取list
            },"线程"+(i+1)).start();
        }
    }
}

即使是不看synchronizedList的源码我们也可以通过它的名字猜到其底层也是使用synchronized来保证线程安全的,这也不是最优解。

3、使用CopyOnWriteArrayList类

CopyOnWriteArrayList类就是我们今天要主要探讨的重头。

public class ListTest {
    public static void main(String[] args) {
        List<String> arrayList = Collections.synchronizedList(new ArrayList<>());
        for (int i = 0; i < 30; i++) {
            new Thread(()->{
                arrayList.add(UUID.randomUUID().toString().substring(0,5));
                //往list中添加一个长为5的随机字符串
                System.out.println(arrayList);
                //读取list
            },"线程"+(i+1)).start();
        }
    }
}

下面我们来聊聊CopyOnWriteArrayList是这么实现线程安全的。

三、CopyOnWriteArrayList

1、简介

java.util.concurrent包下的并发List只有CopyOnWriteArrayList。CopyOnWriteArrayList是一个线程安全的ArrrayList,对其进行的修改操作都是在底层的一个复制的数组上进行的,采用了写时复制,读写分离的思想。其类图结构如下:

通过类图可以清楚下面几点:

  • 每个CopyOnWriteArrayList对象中有一个array数组对象用来存放具体元素
  • ReentrantLock独占锁对象用来保证同一时刻只有一个线程对array进行修改

如果要我们自己来实现一个写时复制的线程安全的List,要考虑哪些点呢?

下面我们带着以下的问题与思考,来学习下CopyOnWriteArrayList吧!

  • 何时初试化list,初始化的list元素个数为多少,list的大小是是有限的吗?
  • 如何保证线程安全,多个线程对list进行读写时是如何保证线程安全的?
  • 如何确保使用迭代器变量list时的数据一致性?

2、主要方法源码分析

1、初始化

无参构造函数,其实是在内部创建了一个大小为0的Object数组作为array的初始值

 	public CopyOnWriteArrayList() {
        setArray(new Object[0]);
    }

有参构造函数有两个:

	public CopyOnWriteArrayList(E[] toCopyIn) {
        setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
    }

CopyOnWriteArrayList中的array数组元素是入参toCopyIn数组元素的拷贝。

	public CopyOnWriteArrayList(Collection<? extends E> c) {
        Object[] elements;
        if (c.getClass() == CopyOnWriteArrayList.class)
            elements = ((CopyOnWriteArrayList<?>)c).getArray();
        else {
            elements = c.toArray();
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elements.getClass() != Object[].class)
                elements = Arrays.copyOf(elements, elements.length, Object[].class);
        }
        setArray(elements);
    }

入参为集合时,将集合里的元素复制到CopyOnWriteArrayList中。

2、添加元素

CopyOnWriteArrayList中用来添加元素的方法有很多,原理均类似,故选取add(E e)方法来进行学习。

	 public boolean add(E e) {
	 	//(1)
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
        	//(2)
            Object[] elements = getArray();
            //(3)
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            //(4)
            setArray(newElements);
            return true;
        } finally {
        	//(5)
            lock.unlock();
        }
    }

上述代码中,独占锁的思想非常值得学习。

  • 调用add方法的线程会首先执行代码(1)去获取独占锁,如果有多个线程都调用add方法时则只有一个线程会去获取到该锁,其他线程会被阻塞挂起直到锁被释放。
  • 一个线程获取到锁后,就保证了该线程添加元素的过程中其他线程不会对array数组进行修改。
  • 线程获取到所后执行代码(2)获取array,然后执行代码(3)复制array到一个新数组中,新数组的大小是array数组的大小+1,所以CopyOnWriteArrayList是无界的List,并把新的元素添加进新数组中。
  • 代码(4)使用新数组替换原数组,并在返回前执行(5)释放锁。由于加了锁,所以整个add的过程是原子性操作。

小结一下就是,添加元素时,线程先获取独占锁,然后复制原数组到新数组,给新数组添加元素,再把添加完元素后的新数组复制回原数组,最后释放锁返回。这就是所谓的写时复制。

3、获取指定位置元素

使用 E get(int index)获取下班为index的元素,如果元素不存在则抛出IndexOutOfBoundsException异常。

	public E get(int index) {
        return get(getArray(), index);
    }
    final Object[] getArray() {
        return array;
    }
    private E get(Object[] a, int index) {
        return (E) a[index];
    }

上述的代码中,当线程x调用get方法获取指定位置的元素时,需要分两步,首先获取array数组,然后通过下标访问指定位置的元素,这个过程是没有加锁同步的。假设这时候List的内容如图所示,里面有1、2、3的元素:

由于线程x调用get方法时是没有加锁的,这就可能导致线程x在获取完array数组之后、访问指定位置元素之前,另外一个线程y进行了remove操作,假设要删除元素1,remove操作首先或获取独占锁,然后进行写时复制,也就是复制一份当前array数组,然后在复制的数组里删除线程x要调用get方法访问的元素1,然后让array指向复制的数组。所以这个时候array之前指向的数组的引用计数为1而不为0,因为线程x还在使用它,这是线程x要访问指定位置的元素了,而它操作的数组就是线程B删除元素之前的数组。如下示意图:

虽然线程y删除了index处的元素,但是线程x还是能够读到index处的元素,这就是写时复制策略产生的弱一致性问题

4、修改指定元素

使用E set(int index, E element)方法修改list中指定元素的值时,如果指定位置元素不存在则抛出IndexOutOfBoundsException异常。

 	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;
                Object[] newElements = Arrays.copyOf(elements, len);
                newElements[index] = element;
                setArray(newElements);
            } else {
                // Not quite a no-op; ensures volatile write semantics
                setArray(elements);
            }
            return oldValue;
        } finally {
            lock.unlock();
        }
    }

上述代码也是先获取独占锁,从而阻止其他线程对array数组进行修改,然后获取当前数组,调用get方法获取指定位置的元素,若指定位置的元素值与新值不一致,则创建新数组并复制元素,然后在新数组上修改元素值,再将array指向新数组;如果指定位置的元素值与新值一直,则为了保证volatile语义,还是要重新设置array,虽然其值为改变。

5、删除元素

这里介绍E remove(int index)方法。

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

如上代码其实和新增元素的代码类似,首先获取独占锁以保证删除数据期间其他线程不能对 array 进行修改,然后获取数组中要被删除的元素,并把剩余的元素复制到新数组,之后使用新数组替换原来的数组,最后在返回前释放锁。

6、弱一致性的迭代器

在讲解什么是迭代器的弱一致性前,先看看下面的例子:

public class ListTest {
    public static void main(String[] args) {
        List<String> arrayList =new CopyOnWriteArrayList<>();
        arrayList.add("Hello");
        arrayList.add("HuYa");
        Iterator<String> iterator = arrayList.iterator();
        while (iterator.hasNext()){
            System.out.println(iterator.next());
        }
    }
}

输出如下:

Hello
HuYa

iterator的hasNext方法用于判断集合中是否还有元素,next用于返回具体元素。 那么CopyOnWriteArrayList中迭代器的弱一致性又是啥意思?所谓弱一致性,是指返回迭代器后,其他线程对list的增删改对迭代器是不可见的。

  	public Iterator<E> iterator() {
        return new COWIterator<E>(getArray(), 0);
    }
    static final class COWIterator<E> implements ListIterator<E> {
        //array的快照版本
        private final Object[] snapshot;
        //数组下标
        private int cursor;
        private COWIterator(Object[] elements, int initialCursor) {
            cursor = initialCursor;
            snapshot = elements;
        }
        public boolean hasNext() {
            return cursor < snapshot.length;
        }
        public E next() {
            if (! hasNext())
                throw new NoSuchElementException();
            return (E) snapshot[cursor++];
        }
    }

上述代码中,当CopyOnWriteArrayList对象调用iterator()方法获取迭代器时实际上会返回COWIterator对象,COWIterator对象的snapshot变量保存了当前list的内容, cursor是遍历list时数据的下标。

特别对snapshot快照进行一个说明:

  • 如果在一个线程使用list返回的迭代器遍历元素的过程中,其他线程没有对list进行修改,那么snapshot本身就是list的array了。
  • 如果在一个线程使用list返回的迭代器遍历元素的过程中,其他线程对list进行修改,那么snapshot就是array的一个快照了,因为修改后list里面的数组其实是被新数组替换了,这时候原来的数组是被snapshot继续引用。

这也说明一个线程获取了迭代器后,使用该迭代器元素时,其他线程对该list进行的修改是不可见的,因为它们操作的是两个不同的数组,这也就是弱一致性。

最后再来看看一个例子:

public class ListTest {
    public static void main(String[] args) throws InterruptedException {
        List<String> arrayList =new CopyOnWriteArrayList<>( );
        arrayList.add("Hello");
        arrayList.add("HuYa");
        arrayList.add("Welcome");
        arrayList.add("to");
        arrayList.add("Guangzhou");
        Thread thread = new Thread(() -> {
            arrayList.set(1, "WeiXin");
            arrayList.remove(2);
            arrayList.remove(3);
        });
        //保证在修改线程启动前先获取迭代器
        Iterator<String> iterator = arrayList.iterator();
        Thread.sleep(1000);
        //修改线程启动
        thread.start();
        //等待修改执行完毕
        thread.join();
        //迭代元素
        while (iterator.hasNext()){
            System.out.println(iterator.next());
        }
    }
}

输出结果如下:

Hello
HuYa
Welcome
to
Guangzhou

虽然说子线程对arrayList进行了修改,但是主线程在arrayList被修改之前先获取到了其迭代器,拿到了原来数组的快照,所以子线程的修改对于主线程使用迭代器进行迭代并没有影响。这就体现了CopyOnWriteArrayList迭代器的弱一致性。

四、总结

本期主要学习了CopyOnWriteArrayList一些主要方法的源码、思想,总结一下:

  • 最主要的是它写时复制的策略,来保证List的一致性,获取、修改、写入三步操作不是原子性的,故在增删改的过程中都使用了独占锁,来保证同一时刻只能有一个线程对list进行修改。
  • CopyOnWriteArrayList提供了弱一致性的迭代器,从而保证在获取迭代器后,其他线程对list的修改对于迭代器是不可见的。
  • 综合CopyOnWriteArrayList上述的特性,它适用于读多写少的高并发场景。

但是CopyOnWriteArrayList也有缺点,开发时要注意一下:

  • 内存占用问题。因为CopyOnWriteArrayList的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,即旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,这个时候很有可能造成频繁的GC,应用响应时间也随之变长。
  • 数据一致性问题。CopyOnWriteArrayList容器只能保证数据的最终一致性,不能保证数据的实时一致性。

以上就是java高并发下CopyOnWriteArrayList替代ArrayList的详细内容,更多关于java高并发CopyOnWriteArrayList的资料请关注我们其它相关文章!

(0)

相关推荐

  • java并发编程专题(十)----(JUC原子类)基本类型详解

    这一节我们先来看一下基本类型: AtomicInteger, AtomicLong, AtomicBoolean.AtomicInteger和AtomicLong的使用方法差不多,AtomicBoolean因为比较简单所以方法比前两个都少,那我们这节主要挑AtomicLong来说,会使用一个,其余的大同小异. 1.原子操作与一般操作异同 我们在说原子操作之前为了有个对比为什么需要这些原子类而不是普通的基本数据类型就能满足我们的使用要求,那就不得不提原子操作不同的地方. 当你在操作一个普通变量时,

  • 详细总结Java中常用的原子类

    一.什么是原子类 Java中提供了一些原子类,原子类包装了一个变量,并且提供了一系列对变量进行原子性操作的方法.我们在多线程环境下对这些原子类进行操作时,不需要加锁,大大简化了并发编程的开发. 二.原子类的底层实现 目前Java中提供的原子类大部分底层使用了CAS锁(CompareAndSet自旋锁),如AtomicInteger.AtomicLong等:也有使用了分段锁+CAS锁的原子类,如LongAdder等. 三.常用的原子类 3.1 AtomicInteger与AtomicLong At

  • Java并发LinkedBlockingQueue源码分析

    目录 简介 常量 构造方法 put await isOnSyncQueue signal 简介 LinkedBlockingQueue是一个阻塞的有界队列,底层是通过一个个的Node节点形成的链表实现的,链表队列中的头节点是一个空的Node节点,在多线程下操作时会使用ReentrantLock锁来保证数据的安全性,并使用ReentrantLock下的Condition对象来阻塞以及唤醒线程. 常量 /** * 链表中的节点类 */ static class Node<E> { //节点中的元素

  • java高并发下解决AtomicLong性能瓶颈方案LongAdder

    目录 一. LongAdder简介 二.LongAdder代码分析 (1)LongAdder的结构 (2)add方法实现 (3)add方法中longAccumulate方法的实现 三.总结 一. LongAdder简介 LongAdder类是JDK1.8新增的一个原子性操作类.上一节说到,AtomicLong通过CAS提供了非阻塞的原子性操作,相比用阻塞算法的synchronized来说性能已经得到了很大提升.在高并发下大量线程会同时竞争更新同一个原子变量,但由于只有一个线程的CAS操作会成功,

  • 详解Java并发编程之原子类

    目录 原子数组 AtomicIntegerArray 原子更新器 AtomicIntegerFieldUpdater 原子累加器 LongAdder 原子数组 原子数组有AtomicIntegerArray.AtomicLongArray.AtomicReferenceArray,主要是用来对数组中的某个元素进行原子操作.三个类的方法基本类似,这里只介绍一下AtomicIntegerArray的方法. AtomicIntegerArray 两个构造方法,第一个构造方法传入数组长度初始化一个所有值

  • java并发编程专题(十一)----(JUC原子类)数组类型详解

    上一节我们介绍过三个基本类型的原子类,这次我们来看一下数组类型: AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray.其中前两个的使用方式差不多,AtomicReferenceArray因为他的参数为引用数组,所以跟前两个的使用方式有所不同. 1.AtomicLongArray介绍 对于AtomicLongArray, AtomicIntegerArray我们还是只介绍一个,另一个使用方式大同小异. 我们先来看看AtomicLong

  • java高并发下CopyOnWriteArrayList替代ArrayList

    目录 一.ArrayList线程不安全 二.解决ArrayList线程不安全的方案 1.使用Vector类 2.使用Collections类 3.使用CopyOnWriteArrayList类 三.CopyOnWriteArrayList 1.简介 2.主要方法源码分析 1.初始化 2.添加元素 3.获取指定位置元素 4.修改指定元素 5.删除元素 6.弱一致性的迭代器 四.总结 一.ArrayList线程不安全 在Java的集合框架中,想必大家对ArrayList肯定不陌生,单线程的情况下使用

  • java实用型-高并发下RestTemplate的正确使用说明

    目录 前言 一.RestTemplate是什么? 二.如何使用 1.创建一个bean 2.使用步骤 三.高并发下的RestTemplate使用 1.设置预热功能 2.合理设置maxtotal数量 总结 前言 如果java项目里有调用第三方的http接口,我们可以使用RestTemplate去远程访问.也支持配置连接超时和响应超时,还可以配置各种长连接策略,也可以支持长连接预热,在高并发下,合理的配置使用能够有效提高第三方接口响应时间. 一.RestTemplate是什么? RestTemplat

  • java中申请不定长度数组ArrayList的方法

    如下所示: import java.util.ArrayList; //java中申请不定长度数组 public class Test01 { public static void main(String[] args) { // TODO Auto-generated method stub ArrayList list=new ArrayList(); list.add("123"); list.add("5"); list.add("5")

  • Java源码解析CopyOnWriteArrayList的讲解

    本文基于jdk1.8进行分析. ArrayList和HashMap是我们经常使用的集合,它们不是线程安全的.我们一般都知道HashMap的线程安全版本为ConcurrentHashMap,那么ArrayList有没有类似的线程安全的版本呢?还真有,它就是CopyOnWriteArrayList. CopyOnWrite这个短语,还有一个专门的称谓COW. COW不仅仅是java实现集合框架时专用的机制,它在计算机中被广泛使用. 首先看一下什么是CopyOnWriteArrayList,它的类前面

  • Java并发包之CopyOnWriteArrayList类的深入讲解

    前言 大家在学习Java的过程中,或者工作中,始终都绕不开集合.在单线程环境下,ArrayList就可以满足要求.多线程时,我们可以使用CopyOnWriteArrayList来保证数据安全.下面我们一起来看看CopyOnWriteArrayList类中的一些值得学习的方法. CopyOnWriteArrayList是一个线程安全的ArrayList,对其进行的修改操作都是在底层的一个复制的数组(快照)上进行的,也就是使用了写时复制策略实现的. 说明:代码部分,均基于JDK1.8 一.添加元素

  • 解决使用redisTemplate高并发下连接池满的问题

    用JMeter进行高并发测试的时候,发现报错: org.springframework.data.redis.RedisConnectionFailureException: Cannot get Jedis connection; nested exception is redis.clients.jedis.exceptions.JedisException: Could not get a resource from the pool 连不上redis,是因为连接池不够用了 我用的是red

  • Java高并发测试框架JCStress详解

    前言 如果要研究高并发,一般会借助高并发工具来进行测试.JCStress(Java Concurrency Stress)它是OpenJDK中的一个高并发测试工具,它可以帮助我们研究在高并发场景下JVM,类库以及硬件等状况. JCStress学起来很简单,而且官方也提供了许多高并发场景下的测试用例,只要引入一个jar包,即可运行研究. 如何使用JCStress 此演示用maven工程,首先需要引入jar包,核心包是必须要的,样例包非必须要,此是为了演示其中的例子. <dependencies>

  • java高并发情况下高效的随机数生成器

    前言 在代码中生成随机数,是一个非常常用的功能,并且JDK已经提供了一个现成的Random类来实现它,并且Random类是线程安全的. 下面是Random.next()生成一个随机整数的实现: protected int next(int bits) { long oldseed, nextseed; AtomicLong seed = this.seed; do { oldseed = seed.get(); nextseed = (oldseed * multiplier + addend)

  • 总结高并发下Nginx性能如何优化

    目录 特点 优势 安装和命令 配置文件 代理模式和配置反向代理 正向代理(forward proxy) : 反向代理(reverse proxy)︰ 透明代理∶ 动静分离 日志管理 日志格式 日志切割 高并发架构分析 什么是高并发? 如何提升系统的并发能力? 三种方式实现 限制连接流 限制请求流(限速) 后台服务限制 安全配置 Nginx优化 Nginx压缩 我们终将在,没有黑暗的地方相见. ~乔治<1984> Nginx同Apache一样都是一种WEB服务器.基于REST架构风格,以统一资源

  • 高并发下restTemplate的错误分析方式

    目录 高并发下restTemplate的错误分析 1. 问题现象和分析 2. 问题解决 使用restTemplate出现的异常 高并发下restTemplate的错误分析 1. 问题现象和分析 org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection 此问题很明显是连接等待超时,而且是从连接池中获取的连接. 那么就有一个很诧异的问题,这里哪来的连接池呢?然后我去跟踪restTemplat

随机推荐