Java集合线程不安全举例


Java 集合,也叫做容器,主要由两大接口派生而来: Collection接口(存放单一元素)、 Map 接口(存放键值对)。对于Collection 接口,有三个主要的子接口:ListSetQueue。有关内容学习可以看 JavaGuide(Java 面试+学习指南)中 Java 集合这一章节

ArrayList线程不安全

我们都知道 ArrayList 是线程不安全的,究其原因是因为其在进行写操作的时候,并没有对并发条件下进行相应的处理,我们可以进入 ArrayList 中查看 add() 方法的实现(如下),从中可以看出未作任何处理,未在方法上加 synchronized 修饰

下面我们可以通过一个实例来证明它是线程不安全的:

/**
 * 线程不安全举例
 */
public class ListDemo {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 30; i++){
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(list);
            },String.valueOf(i)).start();
        }
    }
}

当我们运行这个代码时,会出现错误 java.util.ConcurrentModificationException,并发修改错误

解决方法

  • 方案一:Vector

第一种方法,就是不用 ArrayList 这种不安全的 List 实现类,而采用 Vector,其是线程安全的
关于 Vector 如何实现线程安全的,而是在方法上加了锁,即 synchronized。这样每次只能有一个线程进行操作,不会出现线程不安全的问题,但因为加锁的缘故,并发性能下降了

  • 方案二:Collections.synchronized()
List<String> list = Collections.synchronizedList(new ArrayList<>());

采用 Collections 集合工具类,在 ArrayList 外面包装一层同步机制

  • 方案三:采用 JUC 里面的方法

采用 CopyOnWriteArrayList :写时复制,采用了一种读写分离的思想

List<String> list = new CopyOnWriteArrayList<>();

下面我们查看底层 add() 方法源码来体验写时复制

所谓写时复制简单来说,就是平时查询的时候,都不需要加锁,随意访问,只有在更新的时候,才会加锁并把原来的数据复制出一个副本,然后修改这个副本,最后把原数据替换成当前的副本。值得注意的是写时复制修改操作的同时,读操作不会被阻塞,而是继续读取旧的数据。这点要跟读写锁区分一下

写时复制的优缺点

优点:对于一些读多写少的操作,写入时复制的做法就很不错。同时 CopyOnWriteArrayList 并发安全且性能比 Vector 好,原因是Vector 增删改查方法都加了 synchronized 来保证同步,因此每个方法执行的时候都要去获得锁,性能大大下降,而 CopyOnWriteArrayList 只是在增删改上加锁,读不加锁,因此性能好于 Vector

缺点:数据一致性问题。这种实现只是保证数据最终的一致性,在添加到拷贝数据而还没进行替换的时候,读到的仍然是旧数据。此外还有内存占用问题。如果对象比较大,频繁地进行替换会消耗内存,从而引发 Java 的 GC 问题

HashSet线程不安全

如果查看源码就会发现 HashSet 是用 HashMap 来实现的,只不过 HashSet 只是用 HashMap 的 key 来进行存储,而 value 存储的是一个 Object 类型的常量

解决方法

  • 方案一:Collections.synchronized()
Set<String> set = Collections.synchronizedSet(new HashSet<>());
  • 方案二:采用 JUC 里面的方法
Set<String> set = new CopyOnWriteArraySet<>();

查看 CopyOnWriteArraySet 源码会发现它的底层使用了 CopyOnWriteArrayList

HashMap线程不安全

这里简要说一下 HashMap 的数据结构(JDK1.8 以后):数组 + 链表 + 红黑树,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树),以减少搜索时间

HashMap 的源码讲解可以看这篇文章:HashMap源码&底层数据结构分析

解决方法

  • 方案一:Collections.synchronized()
Map<Integer, String> map = Collections.synchronizedMap(new HashMap<>());
  • 方案二:采用 JUC 里面的方法
Map<Integer, String> map = new ConcurrentHashMap<>();

参考

  • 尚硅谷 Java 大厂面试题第 2 季
  • JavaGuide(Java面试+学习指南)

文章作者: 不才叶某
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 不才叶某 !
评论
  目录