浅谈Java中ArrayList线程不安全怎么办

ArrayList线程不安全怎么办?

有三种解决方法:

使用对应的 Vector 类,这个类中的所有方法都加上了 synchronized 关键字

  • 就和 HashMap 和 HashTable 的关系一样

使用 Collections 提供的 synchronizedList 方法,将一个原本线程不安全的集合类转换为线程安全的,使用方法如下:

List<Integer> list = Collections.synchronizedList(new ArrayList<>());

其实 HashMap 也可以用这招:

Map<String, String> map = Collections.synchronizedMap(new HashMap<>());

这个看上去有点东西,其实也是给每个方法加上一个 synchronized,不过不是直接加在方法上,而是加在方法内部,只有当线程获取到 mutex 这个对象的锁,才能进入代码块:

public E get(int index) {
    synchronized (mutex) {
        return list.get(index);
    }
}

使用 JUC 包下提供的 CopyOnWriteArrayList 类

  • 其实 ConcurrentHashMap 也是 JUC 包下的

这里具体讨论一下 CopyOnWriteArrayList 这个类,它采用了“写时复制”的技术,也就是说,每当要往这个 list 中添加元素时,并不是直接就添加了,而是会先复制一份 list,然后在这个复制中添加元素,最后再修改指针的指向,看看 add 的源码:

public boolean add(E e) {
    synchronized (lock) {
        //得到当前的数组
        Object[] es = getArray();
        int len = es.length;
        //复制一份并扩容
        es = Arrays.copyOf(es, len + 1);
        //把新元素添加进去
        es[len] = e;
        //修改指针的指向
        setArray(es);
        return true;
    }
}

有人可能会疑惑,这有什么意义,这不也加了 synchronized 吗,而且还要复制数组,这**不是比 Vector 还要烂吗?

确实是这样的,在写操作比较多的场景下,CopyOnWriteArrayList 确实比 Vector 还要慢,但它有两个优势:

虽然写操作烂了,但读操作快了很多,因为在 vector 中,读操作也是需要锁的,而在这里,读操作就不需要锁了,get 方法比较短可能不便于理解,我们看看 indexOf 这个方法:

public int indexOf(Object o) {
    Object[] es = getArray();
    return indexOfRange(o, es, 0, es.length);
}
private static int indexOfRange(Object o, Object[] es, int from, int to) {
    if (o == null) {
        for (int i = from; i < to; i++)
            if (es[i] == null)
                return i;
    } else {
        //****here****
        for (int i = from; i < to; i++)
            if (o.equals(es[i]))
                return i;
    }
    return -1;
}

可以发现,这个方法先把当前数组 array 交给了 es 这个变量,后续的所有操作都是基于 es 进行的(此时 array 和 es 都指向内存中的同一份数组 a1)

由于所有写操作都是在 a1 的拷贝上进行的(我们把内存中的这份拷贝称为 a2),因此不会影响到那些正在 a1 上进行的读操作,并且就算写操作执行完毕了,array 指向了 a2,也不会影响到 es 这个数组,因为 es 指向的还是 a1

试想,如果 vector 的读操作不加锁会出现什么情况?由于 vector 中所有的读写操作都是基于同一个数组的,因此虽然读操作一开始拿到的数组是没问题的,但在后续遍历的过程中(比如上面代码标注了 here 的地方),很可能出现其他线程对数组进行了修改,夸张点说,如果有个线程把数组给清空了,那么读操作就肯定会报错了,而对于 CopyOnWriteArrayList 来说,就算有清空的操作,那也是在 a2 上进行的,而读操作还是在 a1 上进行,不会有任何影响

在 forEach 遍历一个 vector 时,是不允许对 vector 进行修改的,会报出 ConcurrentModificationException 这个异常,理由很简单,因为只有一份数组,要是遍历到一半有其它线程把数组清空了不就出问题了吗,因此 java 干脆就直接禁止这种遍历时修改数组的行为了,但对于 CopyOnWriteArrayList 来说,它的遍历是一直在 a1 上进行的,其它写线程只能修改到 a2,这对 a1 是没有任何影响的,我们看一段代码来验证一下:

public class Test {
    public static void main(String[] args) {
        CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
        for (int i = 0; i < 1000; i++) {
            list.add(i);
        }
        //遍历时把数组清空
        for (Integer i : list) {
            System.out.println(i);
            list.clear();
        }
    }
}

结果是没有报错,并且完整输出了 0~999 所有的数字,可见这里遍历的就是最开始的那个数组 a1,期间哪怕有再多的写操作也不会影响到 a1,因为所有的写操作都是在 a2 a3 a4 上进行的
综上所述,CopyOnWriteArrayList 的优点有两个:

  • 读操作不需要锁,因此读读可以并发,读写也能并发,性能较好
  • forEach 遍历时也不需要锁(其实遍历也算是一种读操作吧),主要是遍历时数组可以被修改,不会报错(因为遍历的是 a1,改的是 a2 a3,对 a1 不会有影响)

但它的缺点也很明显,主要有两点:

  • 首先,写操作的内存消耗非常大,每次修改数组都会进行一次拷贝,如果数组比较大或者修改次数比较多,很快就会消耗掉大量内存,触发 GC,因此在写多的场景下一定要慎用这个类
  • 其次,所有读操作和 forEach 遍历都是基于旧数组 a1 的,就算遍历途中新增了一个很重要的数据,这个数据也是在 a2 中,遍历 a1 是无法得到这个数据的,总之就是,所有的读操作一旦开始,就无法再感知到最新的那些数据

可以发现一个有趣的事情,就是成也旧数组,败也旧数组,正因为所有读取都是基于旧数组 a1 的,因此可以不加锁就大胆进行,不怕有线程把数组改了,因为改动都是在 a2 a3 上的,跟 a1 没有关系,但也正因为所有读取都是基于旧数组 a1 的,因此一旦读取操作开始,就算有线程在数组中加入了一个很重要的数据,这个读取操作也是感知不到这个最新的数据的,因为这个最新的数据只会在 a2 中有

到此这篇关于浅谈Java中ArrayList线程不安全怎么办的文章就介绍到这了,更多相关ArrayList线程不安全内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • ArrayList源码和多线程安全问题分析

    1.ArrayList源码和多线程安全问题分析 在分析ArrayList线程安全问题之前,我们线对此类的源码进行分析,找出可能出现线程安全问题的地方,然后代码进行验证和分析. 1.1 数据结构 ArrayList内部是使用数组保存元素的,数据定义如下: transient Object[] elementData; // non-private to simplify nested class access 在ArrayList中此数组即是共享资源,当多线程对此数据进行操作的时候如果不进行同步控

  • 浅谈Java中ArrayList线程不安全怎么办

    ArrayList线程不安全怎么办? 有三种解决方法: 使用对应的 Vector 类,这个类中的所有方法都加上了 synchronized 关键字 就和 HashMap 和 HashTable 的关系一样 使用 Collections 提供的 synchronizedList 方法,将一个原本线程不安全的集合类转换为线程安全的,使用方法如下: List<Integer> list = Collections.synchronizedList(new ArrayList<>());

  • 浅谈Java中spring 线程异步执行

    多线程并发处理起来通常比较麻烦,如果你使用spring容器来管理业务bean,事情就好办了多了.spring封装了Java的多线程的实现,你只需要关注于并发事物的流程以及一些并发负载量等特性,具体来说如何使用spring来处理并发事务: 1.了解 TaskExecutor接口 Spring的TaskExecutor接口等同于java.util.concurrent.Executor接口. 实际上,它存在的主要原因是为了在使用线程池的时候,将对Java5的依赖抽象出来. 这个接口只有一个方法exe

  • 浅谈java中守护线程与用户线程

    Java线程分为两类分别为daemon线程(守护线程)和User线程(用户线程),在JVM启动时候会调用main函数,main函数所在的线程是一个用户线程,这个是我们可以看到的线程,其实JVM内部同时还启动了好多守护线程,比如垃圾回收线程.那么守护线程和用户线程有什么区别那?区别之一是当最后一个非守护线程结束时候,JVM会正常退出,而不管当前是否有守护线程,也就是说守护线程是否结束并不影响JVM的退出.言外之意是只要有一个用户线程还没结束正常情况下JVM就不会退出. 那么Java中如何创建一个守

  • 浅谈 java中ArrayList、Vector、LinkedList的区别联系

    以前面试的时候经常会碰到这样的问题.,叫你写一下ArrayList.LinkedList.Vector三者之间的区别与联系:原先一直搞不明白,不知道这三者之间到底有什么区别?哎,惭愧,基础太差啊,木有办法啊委屈 现在得去说说这三者之间的区别与联系了:这三者都是实现了List接口,都拥有List接口里面定义的方法,并且同时拥有Collection接口的方法: ArrayList:采用的是数组的方式进行存储数据的,查询和修改速度快,但是增加和删除速度慢:线程是不同步 LinkedList:采用的是链

  • 浅谈Java中常用数据结构的实现类 Collection和Map

    线性表,链表,哈希表是常用的数据结构,在进行Java开发时,JDK已经为我们提供了一系列相应的类来实现基本的数据结构.这些类均在java.util包中.本文试图通过简单的描述,向读者阐述各个类的作用以及如何正确使用这些类. Collection ├List │├LinkedList │├ArrayList │└Vector │ └Stack └Set Map ├Hashtable ├HashMap └WeakHashMap Collection接口 Collection是最基本的集合接口,一个C

  • 浅谈Java中Collection和Collections的区别

    1.java.util.Collection 是一个集合接口.它提供了对集合对象进行基本操作的通用接口方法.Collection接口在Java 类库中有很多具体的实现.Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式. Collection ├List │├LinkedList │├ArrayList │└Vector │ └Stack └Set 2.java.util.Collections 是一个包装类.它包含有各种有关集合操作的静态多态方法.此类不能实例化,就像一

  • 浅谈java中对集合对象list的几种循环访问

    java中对集合对象list的几种循环访问的总结如下  1 经典的for循环 public static void main(String[] args) { List<String> list = new ArrayList(); list.add("123"); list.add("java"); list.add("j2ee"); System.out.println("=========经典的for循环=======

  • 浅谈Java中的四种引用方式的区别

    强引用.软引用.弱引用.虚引用的概念 强引用(StrongReference) 强引用就是指在程序代码之中普遍存在的,比如下面这段代码中的object和str都是强引用: Object object = new Object(); String str = "hello"; 只要某个对象有强引用与之关联,JVM必定不会回收这个对象,即使在内存不足的情况下,JVM宁愿抛出OutOfMemory错误也不会回收这种对象. 比如下面这段代码: public class Main { publi

  • 浅谈Java中各种修饰符与访问修饰符的说明

    JAVA中的类只能是public 或者package的.这是符合逻辑的:人们定义类的初衷就是为了让别人用的.倘若是private,别人怎么调用?但是有一个内部类可以被定义为private.严格上说,内部类,算不得上是一种光明正大的类,内部类在某种意义上是类这个王国里的特务和地下工作者.特务和地下工作者为王国起了不少作用,但是几乎从来不敢在公众场合抛投露面.就算要露面,也要在主人(class)的同意下,向导(Interface)的引导下,才敢战战兢兢的走出来.下面是常规的一些类的修饰符和访问修饰符

  • 浅谈Java中强制类型转换的问题

    为了更好的理解我们先看下面的例子: package com.yonyou.test; import java.util.ArrayList; import java.util.Iterator; import java.util.List; /** * 测试类 * @author 我们 * @创建日期 2016-5-31 */ public class Test{ public static void main(String[] args) { List<String> list=new Ar

随机推荐