文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

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

2024-04-02 19:55

关注

ArrayList线程不安全怎么办?

有三种解决方法:

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

使用 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 类

这里具体讨论一下 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 的优点有两个:

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

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

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

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯