CopyOnWriteArrayList你都不知道,怎么拿offer?(一)

简介: 笔记

前一阵子写过一篇COW(Copy On Write)文章,结果阅读量很低啊…COW奶牛!Copy On Write机制了解一下

可能大家对这个技术比较陌生吧,但这项技术是挺多应用场景的。除了上文所说的Linux、文件系统外,其实在Java也有其身影。

大家对线程安全容器可能最熟悉的就是ConcurrentHashMap了,因为这个容器经常会在面试的时候考查。

比如说,一个常见的面试场景:

  • 面试官问:“HashMap是线程安全的吗?如果HashMap线程不安全的话,那有没有安全的Map容器”
  • 3y:“线程安全的Map有两个,一个是Hashtable,一个是ConcurrentHashMap”
  • 面试官继续问:“那Hashtable和ConcurrentHashMap有什么区别啊?”
  • 3y:“balabalabalabalabalabala"
  • 面试官:”ok,ok,ok,看你Java基础挺不错的呀“

那如果有这样的面试呢?

  • 面试官问:“ArrayList是线程安全的吗?如果ArrayList线程不安全的话,那有没有安全的类似ArrayList的容器”
  • 3y:“线程安全的ArrayList我们可以使用Vector,或者说我们可以使用Collections下的方法来包装一下”
  • 面试官继续问:“嗯,我相信你也知道Vector是一个比较老的容器了,还有没有其他的呢?”
  • 3y:“Emmmm,这个…“
  • 面试官提示:“就比如JUC中有ConcurrentHashMap,那JUC中有类似"ArrayList"的线程安全容器类吗?“
  • 3y:“Emmmm,这个…“
  • 面试官:”ok,ok,ok,今天的面试时间也差不多了,你回去等通知吧。“

今天主要讲解的是CopyOnWriteArrayList~

本文力求简单讲清每个知识点,希望大家看完能有所收获


一、Vector和SynchronizedList


1.1回顾线程安全的Vector和SynchronizedList


我们知道ArrayList是用于替代Vector的,Vector是线程安全的容器。因为它几乎在每个方法声明处都加了synchronized关键字来使容器安全。

1.jpg                                                              Vector实现

如果使用Collections.synchronizedList(new ArrayList())来使ArrayList变成是线程安全的话,也是几乎都是每个方法都加上synchronized关键字的,只不过它不是加在方法的声明处,而是方法的内部

2.jpg                                       Collections.synchronizedList()的实现


1.2Vector和SynchronizedList可能会出现的问题


在讲解CopyOnWrite容器之前,我们还是先来看一下线程安全容器的一些可能没有注意到的地方~

下面我们直接来看一下这段代码:

// 得到Vector最后一个元素
    public static Object getLast(Vector list) {
        int lastIndex = list.size() - 1;
        return list.get(lastIndex);
    }
    // 删除Vector最后一个元素
    public static void deleteLast(Vector list) {
        int lastIndex = list.size() - 1;
        list.remove(lastIndex);
    }

以我们第一反应来分析一下上面两个方法:在多线程环境下,是否有问题

  • 我们可以知道的是Vector的size()和get()以及remove()都被synchronized修饰的。

答案:从调用者的角度是有问题

我们可以写段代码测试一下:

import java.util.Vector;
public class UnsafeVectorHelpers {
    public static void main(String[] args) {
        // 初始化Vector
        Vector<String> vector = new Vector();
        vector.add("关注公众号");
        vector.add("Java3y");
        vector.add("买Linux可到我下面的链接,享受最低价");
        vector.add("给3y加鸡腿");
        new Thread(() -> getLast(vector)).start();
        new Thread(() -> deleteLast(vector)).start();
        new Thread(() -> getLast(vector)).start();
        new Thread(() -> deleteLast(vector)).start();
    }
    // 得到Vector最后一个元素
    public static Object getLast(Vector list) {
        int lastIndex = list.size() - 1;
        return list.get(lastIndex);
    }
    // 删除Vector最后一个元素
    public static void deleteLast(Vector list) {
        int lastIndex = list.size() - 1;
        list.remove(lastIndex);
    }
}

可以发现的是,有可能会抛出异常的:

3.jpg                                                         代码抛出异常

原因也很简单,我们照着流程走一下就好了:

  • 线程A执行getLast()方法,线程B执行deleteLast()方法
  • 线程A执行int lastIndex = list.size() - 1;得到lastIndex的值是3。同时,线程B执行int lastIndex = list.size() - 1;得到的lastIndex的值是3
  • 此时线程B先得到CPU执行权,执行list.remove(lastIndex)将下标为3的元素删除了
  • 接着线程A得到CPU执行权,执行list.get(lastIndex);,发现已经没有下标为3的元素,抛出异常了.

4.jpg                                                    交替执行导致异常发生

出现这个问题的原因也很简单:

  • getLast()deleteLast()这两个方法并不是原子性的,即使他们内部的每一步操作是原子性的(被Synchronize修饰就可以实现原子性),但是内部之间还是可以交替执行。
  • 这里的意思就是:`size()和get()以及remove()`都是原子性的,但是如果并发执行`getLast()`和`deleteLast()`,方法里面的`size()和get()以及remove()`是可以交替执行的。

要解决上面这种情况也很简单,因为我们都是对Vector进行操作的,只要操作Vector前把它锁住就没毛病了

所以我们可以改成这样子:

// 得到Vector最后一个元素
    public static Object getLast(Vector list) {
        synchronized (list) {
            int lastIndex = list.size() - 1;
            return list.get(lastIndex);
        }
    }
    // 删除Vector最后一个元素
    public static void deleteLast(Vector list) {
        synchronized (list) {
            int lastIndex = list.size() - 1;
            list.remove(lastIndex);
        }
    }

ps:如果有人去测试一下,发现会抛出异常java.lang.ArrayIndexOutOfBoundsException: -1,这是没有检查角标的异常,不是并发导致的问题。

经过上面的例子我们可以看看下面的代码:

public static void main(String[] args) {
        // 初始化Vector
        Vector<String> vector = new Vector();
        vector.add("关注公众号");
        vector.add("Java3y");
        vector.add("买Linux可到我下面的链接,享受最低价");
        vector.add("给3y加鸡腿");
        // 遍历Vector
        for (int i = 0; i < vector.size(); i++) {
            // 比如在这执行vector.clear();
            //new Thread(() -> vector.clear()).start();
            System.out.println(vector.get(i));
        }
    }

同样地:如果在遍历Vector的时候,有别的线程修改了Vector的长度,那还是会有问题

  • 线程A遍历Vector,执行vector.size()时,发现Vector的长度为5
  • 此时很有可能存在线程B对Vector进行clear()操作
  • 随后线程A执行vector.get(i)时,抛出异常

5.jpg                                                Vector遍历抛出异常

在JDK5以后,Java推荐使用for-each(迭代器)来遍历我们的集合,好处就是简洁、数组索引的边界值只计算一次

如果使用for-each(迭代器)来做上面的操作,会抛出ConcurrentModificationException异常

6.jpg                         迭代器遍历会抛出ConcurrentModificationException

SynchronizedList在使用迭代器遍历的时候同样会有问题的,源码已经提醒我们要手动加锁了。

网络异常,图片无法展示
|

目录
相关文章
|
2月前
|
存储 安全 算法
JUC集合: ConcurrentLinkedQueue详解
与此同时,它的无界特性在使用时需要注意,因为过多的数据累积可能会导致内存消耗过大。合理应用 `ConcurrentLinkedQueue` 不仅可以提升应用性能,还能提高程序在并发环境下的可靠性。在实际的开发过程中,合理选择适当的并发容器对于构建高效稳定的系统至关重要。
46 2
|
存储 算法 Java
PriorityQueue 源码分析
这两个函数其实就是首先将其他集合转化为连续的array,再将其堆化。
57 0
|
算法 安全
CopyOnWriteArrayList
CopyOnWriteArrayList
|
Java 索引
面试官:说说你对LinkedList的了解……
面试官:说说你对LinkedList的了解……
121 0
|
缓存 安全 Java
看了CopyOnWriteArrayList后自己实现了一个CopyOnWriteHashMap
看了CopyOnWriteArrayList后自己实现了一个CopyOnWriteHashMap
203 0
看了CopyOnWriteArrayList后自己实现了一个CopyOnWriteHashMap
|
存储 安全 Java
CopyOnWriteArrayList 你了解多少?
相信大家对 ConcurrentHashMap 这个线程安全类非常熟悉,但是如果我想在多线程环境下使用 ArrayList,该怎么处理呢?阿粉今天来给你揭晓答案!
CopyOnWriteArrayList 你了解多少?
|
存储 搜索推荐 数据安全/隐私保护
面试侃集合 | PriorityBlockingQueue篇
面试侃集合 | PriorityBlockingQueue篇
116 0
面试侃集合 | PriorityBlockingQueue篇
|
安全 API 索引
面试侃集合 | ArrayBlockingQueue篇
面试侃集合 | ArrayBlockingQueue篇
96 0
面试侃集合 | ArrayBlockingQueue篇
|
存储 安全 Java
面试侃集合 | LinkedBlockingQueue篇
面试侃集合 | LinkedBlockingQueue篇
183 0
面试侃集合 | LinkedBlockingQueue篇