Java集合面试题

目录

1)、Java中集合的框架图

 2)、常用集合的分类

3)、List接口详解 

3.1)ArrayList集合类

3.2)、LinkedList集合类

3.3)、Vector集合类

4)、Map接口详解

4.1)、HashMap集合类

4.2)、HashTable集合类

4.3)、TreeMap集合类

5)、Set接口详解

5.1)、HashSet集合类        

6)、Map、List、Set他们的下面有哪些线程安全类和线程不安全的类

6.1)、Map

6.2)、List

6.3)、Set

7)、Collections和Collection有什么区别?

8)、线程安全集合类ConcurrentHashMap

8.1)、jdk1.7版本的ConcurrentHashMap

8.2)、jdk1.8版本的ConcurrentHashMap

9)、HashMap、HashTable和ConcurrentHashMap的区别

10)、线程安全集合类CopyOnWriteArrayList


1)、Java中集合的框架图

 2)、常用集合的分类

Collection 接口(单列集合)
├——-List 接口:元素按进入先后有序保存,可重复
│—————-├ LinkedList 接口实现类, 链表, 插入删除, 没有同步, 线程不安全
│—————-├ ArrayList 接口实现类, 数组, 随机访问, 没有同步, 线程不安全
│—————-└ Vector 接口实现类 数组, 同步, 线程安全
│ ———————-└ Stack 是Vector类的实现类
└——-Set 接口: 仅接收一次,不可重复,并做内部排序
├—————-HashSet 使用hash表(数组)存储元素
│————————LinkedHashSet 链表维护元素的插入次序
└ —————-TreeSet 底层实现为二叉树,元素排好序

Map 接口 键值对的集合 (双列集合)
├———Hashtable 接口实现类, 同步, 线程安全
├———HashMap 接口实现类 ,没有同步, 线程不安全-
│—————–├ LinkedHashMap 双向链表和哈希表实现
│—————–└ WeakHashMap
├ ——–TreeMap 红黑树对所有的key进行排序
└———IdentifyHashMap

3)、List接口详解 

        list是java中的一个结合接口,它继承自Collection。list集合的特点是存储的元素有序可重复,存进去和取出来的顺序相同。它有三个实现类,分别是ArrayList、LinkedList、Vector,其中ArrayList是最常用的一个。

//list接口
public interface List<E> extends Collection<E> {
}
//ArrayList类
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
}
//LinkedList类
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable{
}
//Vector类
public class Vector<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
}

3.1)ArrayList集合类

        ArrayList是基于动态数组的数据结构,是连续存储的。它是线程不安全的。

        我们可以构造一个ArrayList,添加一个元素,看一下其容量。最终的结果是容量:10,大小:1,如果我们ArrayList构造函数的手没有设置初始化的容量大小,在没有添加任何元素的时候,其初始容量是0,只有当我们添加第一个元素的时候,才会初始化容量为10。

/例子
ArrayList<String> list = new ArrayList<>();
Integer length = getCapacity(list);
int size = list.size();
System.out.println("容量: " + length);
System.out.println("大小: " + size);
//ArrayList类
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    //1,首先是默认初始值的大小:
    private static final int DEFAULT_CAPACITY = 10;,
    //2,接着是一个默认的空对象数组:
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    //3,然后是ArrayList 实际数据存储的一个数组:
    transient Object[] elementData;
    //4,elementData 的大小:
    private int size;
    //有参构造
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }
    //无参构造
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;//使用的是默认大小
    }
}

 (1)、ArrayList的扩容机制

        ArrayList的每次扩容都是原有容量的1.5倍。

//接下来,我们进行构造一个ArrayList 对象,同样调用的是无参构造函数。然后往里面添加11个对象,看起容量会如何进行动态扩展。最终结果,容量:15,大小:11
  ArrayList<String> list = new ArrayList<>();
     for (int i = 1; i <= 11; i++) {
        list.add("value" + i);
     }
  Integer length = getCapacity(list);
  int size = list.size();
  System.out.println("容量: " + length);
  System.out.println("大小: " + size);
  //要清楚其扩容机制,我们需要跟进去,看一下其add 方法,
  public boolean add(E e) {
      //① ensureCapacityInternal方法名的英文大致是“确保内部容量”,size表示的是执行添加之前的元素个数,并非ArrayList的容量,容量应该是数组elementData的长度。ensureCapacityInternal该方法通过将现有的元素个数与数组的容量比较。看如果需要扩容,则扩容。 
      //②是将要添加的元素放置到相应的数组中。 
      ensureCapacityInternal(size + 1);  // Increments modCount!!
      elementData[size++] = e;
      return true;
  }
  //看一下ensureCapacityInternal方法的实现:
  private void ensureCapacityInternal(int minCapacity) {
      DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}
      //判断elementData这个数组是否为空,如果为空,就让默认大小10与传过来的minCapacity比较,找打一个最大的
      if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
          minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
      }
      ensureExplicitCapacity(minCapacity);
  }
  private void ensureExplicitCapacity(int minCapacity) {
      modCount++;
      // overflow-conscious code
      //根据传入的最小需要容量minCapacity来和数组的容量长度对比,如果minCapacity大于或等于数组容量,则需要进行扩容。
      if (minCapacity - elementData.length > 0)    // 如果其元素个数大于其容量,则进行扩容。
          grow(minCapacity);    
  }
  //具体扩容流程:
  private void grow(int minCapacity) {
      // overflow-conscious code
      int oldCapacity = elementData.length;   // 原来的容量
      int newCapacity = oldCapacity + (oldCapacity >> 1);  // 新的容量,原来容量的1.5倍。
      if (newCapacity - minCapacity < 0)
          newCapacity = minCapacity;
      if (newCapacity - MAX_ARRAY_SIZE > 0)  // 如果大于ArrayList 可以容许的最大容量,则设置为最大容量。
          newCapacity = hugeCapacity(minCapacity);
      // minCapacity is usually close to size, so this is a win:
      elementData = Arrays.copyOf(elementData, newCapacity);  // 最终利用Arrays.coppy 进行扩容,生成一个1.5倍元素的数组。(即例子中的15个元素的数组。)
  }
  //ArrayList 的内部实现,其实是用一个对象数组进行存放具体的值,然后用一种扩容的机制,进行数组的动态增长。其扩容机制可以理解为,如果元素的个数,大于其容量,则把其容量扩展为原来容量的1.5倍。

3.2)、LinkedList集合类

        LinkedList是基于链表的数据结构,存储在分散的内存中。

        对于新增和删除操作add和remove,LinkedList是比较占优势的,因为ArrayList要移动数据。LinkedList适用于要头尾操作或插入指定位置的场景,它同样是线程不安全的。

        LinkedList中将添加的数据封装成一个Node对象,然后加入到链表中。

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
    transient int size = 0;
    transient Node<E> first;
    transient Node<E> last;
    public LinkedList() { //无参
    }
    public LinkedList(Collection<? extends E> c) {//有参
        this();
        addAll(c);
    }
}

3.3)、Vector集合类

        Vector的底层也是一个数组结构,它的初始容量是10,每次扩容是原有容量的2倍,Vector中所有的方法都是线程同步的,都带有synchronized关键字,是线程安全的。但是效率比较低,使用比较少。vector有四个构造函数,通过无参的来调用有参的构造函数创建一个初始容量为10的数组来存储数据。

public Vector() {
   this(10);
}//使用指定的初始容量和等于0的容量增量构造一个空向量。    
public Vector(int initialCapacity)//构造一个空向量,使其内部数据数组的大小,其标准容量增量为零。    
public Vector(Collection<? extends E> c)//构造一个包含指定 collection 中的元素的向量    
public Vector(int initialCapacity,int capacityIncrement)//使用指定的初始容量和容量增量构造一个空的向量    
//vector的添加方法
public synchronized boolean add(E e)
//vector的删除方法
public synchronized E remove(int index)

4)、Map接口详解

        Map是用来存储键值对的集合,它和Collection没有继承关系,interface Map<K,V>:K(key)键,V(value)值。将键值映射到值的对象,不能出现重复的键,每个键最多可以映射到一个值。

4.1)、HashMap集合类

        HashMap是基于哈希表的Map接口的非同步实现,是线程不安全的。

(1)、HashMap的底层数据结构

        在jdk1.8之前使用的是“数组+链表”的底层数据结构。

        jdk1.8之后使用的是“数组+链表+红黑树”的底层数据结构。HashMap中将链表转为红黑树的前提是当链表长度大于阈值(默认为8)时且数组的大小为64以上,提高搜索效率。链表的查找性能是 O(n),而使用红黑树是 O(logn)。

  (2)、HashMap的重要属性及用途

        1)、size:HashMap已经存储的节点个数;

        2)、threshold:扩容阈值,当HashMap的个数达到该值,触发扩容;初始化时的容量,在我们新建HashMap对象时threshold还会被用来存初始化时的容量;HashMap直到我们第一次插入节点时,才会对table进行初始化,避免不必要的空间浪费;

        3)、loadFactor:负载因子,扩容阈值=容量*负载因子

(3)、HashMap的容量及对Null的支持

        HashMap默认的初始化大小为16,之后每次扩充,容量变为原来的2倍;

        HashMap中,null可作为键,这样的键只有一个,可以有一个或多个键所对应的值为null;

(4)、HashMap的插入流程

①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;

②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;

③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;

④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;

⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;

⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。

(5)、HashMap扩容(resize)流程

(6)、jdk1.8主要解决或优化了HashMap中的哪些问题 

        1)、底层数据结构从“数组+链表”改成了“数组+链表+红黑树”,主要是优化了hash冲突较严重时,链表过程的查找性能O(n)->O(logn);

        2)、计算table初始容量的方式发生了改变,老的方式是从1开始不断向左进行移位运算, 直到找到大于等于入参容量的值;新的方式则是通过“5个移位+或等于运算”来计算;

// JDK 1.7.0
public HashMap(int initialCapacity, float loadFactor) {
    // 省略
    // Find a power of 2 >= initialCapacity
    int capacity = 1;
    while (capacity < initialCapacity)
        capacity <<= 1;
    // ... 省略
}
// JDK 1.8.0_191
static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

       3)、优化了hash值的计算方式,老的通过4次位运算,5次异或运算(9次扰动),新的只是简单的让高16位参与了运算;

        4)、扩容时插入方式从“头插法”改成“尾插法”,避免了并发下的死循坏;

        5)、扩容时计算节点在新表的索引位置方式从“h & (length-1)”改成“hash & oldCap”,性能可能提升不大,但设计更巧妙、更优雅。

4.2)、HashTable集合类

        HashTable是一个线程安全的集合类,它的内部方法基本都经过synchronized关键字的修饰;在HashTable中只要put进去的键值只要有一个为null,直接抛出NullPointerException异常;HashTable默认的初始大小为11,之后每次扩充,容量变为原来的2n+1倍,另外,HashTable基本被淘汰,不要在代码中使用它。

public synchronized V get(Object key) {
    // ...
}
public synchronized V put(K key, V value) {
    // ...
}

4.3)、TreeMap集合类

        TreeMap的底层是通过红黑树实现的。可以指定比较器(Comparator比较器),通过重写compare方法来自定义排序;如果没有指定比较器,TreeMap默认是按key的升序排序(如果key没有实现Comparable结构,则会抛出异常),是非线程安全的。

        (1)、Comparable和Comparator的比较

                ①Comparable是排序接口,一个类实现了Comparable接口,意味着“该类支持排序”;Comparator是比较器,我们可以实现该接口,自定义比较算法,创建一个“该类的比较器”来进行排序。

                ②Comparable相当于“内部比较器”,而Comparator相当于“外部比较器”。

                ③Comparable的耦合性更强,Comparator的灵活性和扩展性更优。

                ④Comparable可以用作类的默认排序算法,而Comparator则用于默认排序不满足时,提供自定义排序。

                

5)、Set接口详解

        set和list都是继承自Collection接口;往set里面放入的元素是无序的,不可重复的,重复的元素会被覆盖掉(注意:元素虽然是无序放入的,但是元素在set中的位置是由该元素的HashCode决定的,其位置是固定的,加入Set的Object必须定义equals()方法,另外list支持for循坏,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为它无序,无法用下标来取得想要的值)。

        set检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置的改变。

5.1)、HashSet集合类        

        HashSet底层数据结构采用哈希表实现,元素无序且唯一,线程不安全,效率高,可以存储null元素,元素的唯一性是靠所存元素的类型是否重写hashCode()和equals()方法来保证的,如果没有重写这两个方法,则无法保证元素的唯一性。

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
{
    static final long serialVersionUID = -5024744406713321676L;
    private transient HashMap<E,Object> map;
    private static final Object PRESENT = new Object();
    public HashSet() { //无参构造
        map = new HashMap<>();//HashSet的底层是用哈希表来存储数据的,key就是要添加的对象,value是一个固定的Object对象
    }
    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }
}

(1)、HashSet如何实现唯一性

        HashSet在存储元素的时候还先使用hash()算法函数来生成一个int类型的hashCode散列值;

        然后和已经所存储的元素的hashCode值比较,如果hashCode不相等,则所存储的两个对象一定不相等,此时存储当前的新的hashCode值处的元素对象;

        如果hashCode值相等,存储元素的对象还是不一定相等,此时就会调用equals()方法来判断两个对象的内容是否相等,如果内容相等,那么就是一个对象,无需存储;

        如果比较的内容不相等,那么就是不同的对象,就该存储了,此时就要采用哈希的解决地址冲突算法,在当前hashCode值出类似一个新的链表,在同一个hashCode值的后面存储不同的对象,这样就保证了元素的唯一性。

5.2)、TreeSet集合详解

        TreeSet的底层数据结构采用二叉树来实现,元素唯一且已经排好序;唯一性同样需要重写hashCode()和equals()方法,二叉树结构保证了元素的有序性。

        根据构造方法不同,分为自然排序(无参构造)和比较器排序(有参构造),自然排序要求元素必须实现Compare接口,并重写里面的compareTo()方法,元素通过比较返回的int值来判断排序序列,返回0说明两个对象相同,不需要存储;比较器排序需要在TreeSet初始化的时候传入一个实现Comparator接口的比较器对象,或者采用匿名内部类的方式new一个Comparator对象,重写里面的compare()方法。

5.3)、LinkedHashSet

        LinkedHashSet底层数据结构采用链表和哈希表共同实现,链表保证了元素的顺序与存储顺序一致,哈希表保证了元素的唯一性。线程不安全,效率高。

6)、Map、List、Set他们的下面有哪些线程安全类和线程不安全的类

6.1)、Map

        线程安全:ConcurrentHashMap、HashTable

        线程不安全:HashMap、LinkedHashSetMap、TreeMap、WeakHashMap

6.2)、List

        线程安全:Vector、Stack、CopyONWriteArrayList

        线程不安全:ArrayList、LinkedList

6.3)、Set

        线程安全:CopyOnWriteArraySet(底层使用CopyOnWriteArrayList,通过在插入前判断是否存在实现set不重复的效果)

        线程不安全:HashSet(基于HashMap)、LinkedHashSet(基于LinkedHashMap)、TreeSet(基于TreeMap)、EnumSet

7)、Collections和Collection有什么区别?

        Collection是最基本的集合接口,Collection派生了两个子接口list和set,分别定义了两种不同的存储方式。

        Collections是一个包装类,它包含各种有关集合操作的静态方法(对集合的搜索、排序、线程安全化等),此类不能实例化,就像一个工具类,服务于Collection框架。

8)、线程安全集合类ConcurrentHashMap

        ConCurrentHashMap是HashMap的线程安全版本,和HashMap一样,在JDK1.8中进行了比较大的优化。

8.1)、jdk1.7版本的ConcurrentHashMap

        数据结构:jdk1.7下的ConcurrentHashMap中的底层数据结构为“分段的数组+链表”来实现线程的安全,分段锁(Segment,继承了ReentrantLock),如下图所示:

        元素查询:第一次Hash定位到Segment,第二次定位到元素所在的链表的头部

        锁:当有请求过来时,锁的是需要操作的那个Segment,其它的Segment不受影响;并发度为Segment的个数,可以通过构造函数指定,数组扩容不会影响到其他的Segment。

        锁的粒度是基于Segment的,包含多个节点(HashEntry)。

        Segment是一种可重入的锁(继承自ReentrantLock),当对HashEntry数组的数据进行修改时,必须首先获得对应的Segment锁。Segment数组在创建以后它的大小是不会改变的,改变的只是HashEntry数组的大小,当大小大于阈值0.75以后。对于ConcurrentHashMap1.7来说-调用构造方法,它的数组就被创建了,但是ConcurrentHashMap1.8是第一次放入元素的时候,底层数组才会被创建。 

8.2)、jdk1.8版本的ConcurrentHashMap

        jdk1.8下的ConcurrentHashMap中的底层数据结构和HashMap是一样的“数组+链表+红黑树”,其底层是一个Node数组,通过对Node数组以CAS的方式实现扩容和对Node数组的每个元素的synchronized来保证ConcurrentHashMap整体的线程安全。

        在链表数组结构上进行了优化,和HashMap1.8的优化一样,当链表的长度达到8且数组的长度超过64的时候会把链表转为红黑树,以此来提高查找性能。

 

   (1)、jdk1.8中ConcurrentHashMap的属性

// 散列表最大容量
private static final int MAXIMUM_CAPACITY = 1 << 30;
// 散列表默认容量
private static final int DEFAULT_CAPACITY = 16;
// 最大数组长度
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
// 默认并发级别 jdk1.7 之前遗留的 1.8只用于初始化
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
// 负载因子
private static final float LOAD_FACTOR = 0.75f;
// 链表树化条件
static final int TREEIFY_THRESHOLD = 8;
// 取消树化条件
static final int UNTREEIFY_THRESHOLD = 6;
// 结点树化条件 
static final int MIN_TREEIFY_CAPACITY = 64;
// 线程迁移数据最小步长 控制线程迁移任务最小区间的一个值
private static final int MIN_TRANSFER_STRIDE = 16;
//  扩容用  计算扩容生成一个标识戳
private static final int RESIZE_STAMP_BITS = 16;
// 65535 标识并发扩容最大线程数量
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
// 扩容相关
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
// node 结点的hash 是-1 表示 当前结点是forwardingNode结点
static final int MOVED     = -1; // hash for forwarding nodes
// 红黑树的代理结点
static final int TREEBIN   = -2; // hash for roots of trees
// 临时保留的散列表
static final int RESERVED  = -3; // hash for transient reservations
// 0x7fffffff = 31个1  用于将一个负数变成一个正数 但是不是取绝对值
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
// 系统CPu数量
static final int NCPU = Runtime.getRuntime().availableProcessors();
// 散列表
transient volatile Node<K,V>[] table;
// 扩容用的临时散列表
private transient volatile Node<K,V>[] nextTable;
// LongAdder 的baseCount 
private transient volatile long baseCount;
/**
sizeCtl <0 
	1. -1 的时候 表示table正在初始化(有线程正在初始化 , 当前线程应该自旋等待)
	2. 其他情况 表示当前map正在进行扩容 高16位表示 扩容的标识戳 , 低16位表示 扩容线程数量
sizeCtl = 0 
	表示创建数组 使用默认容量 16
sizeCtl >0
	1. 如果table 未初始化 表示 初始化大小
	2. 如果table 已经初始化 表示下次扩容的阈值
*/
private transient volatile int sizeCtl;
//扩容过程中,记录当前进度,所有线程都需要从transferIndex中分配区间任务,去执行自己的任务
private transient volatile int transferIndex;
// 0 表示 无锁 1 表示加锁
private transient volatile int cellsBusy;
 // LongAdder 中的cells 数组 当baseCount发生竞争后 会创建cells 数组
 // 线程会通过计算hash值 取到自己的cell中
 private transient volatile CounterCell[] counterCells;

ConcurrentHashMap中的源码,例如put方法、初始化方等详解refConcurrentHashMap详解_唐芬奇的博客-CSDN博客

9)、HashMap、HashTable和ConcurrentHashMap的区别

10)、线程安全集合类CopyOnWriteArrayList

        在进行CopyOnWriteArrayList的源码讲解之前,先看下同样实现了线程安全Vector,很多文章都说不推荐使用Vector,其主要原因是性能太差了,那性能为什么这么差呢?可以看一下Vector add和get的源码:

public synchronized E get(int index) {
      if (index >= elementCount)
          throw new ArrayIndexOutOfBoundsException(index);
          return elementData(index);
  }
  public synchronized E set(int index, E element) {
      if (index >= elementCount)
          throw new ArrayIndexOutOfBoundsException(index);
          E oldValue = elementData(index);
          elementData[index] = element;
          return oldValue;
  }

        Vector的添加和读取操作都被加上了synchronized锁,当并发情况下,因为锁的存在相当于变成了单线程的操作,所以效率肯定低,同样这样的优点就是保证了数据的唯一性,不会读取到脏数据。

首先我们看一下CopyOnWriteArrayList的全局变量有哪些?

public class CopyOnWriteArrayList<E>
      implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
      private static final long serialVersionUID = 8673264195747942595L;
      /** The lock protecting all mutators */
      final transient ReentrantLock lock = new ReentrantLock();
      /** The array, accessed only via getArray/setArray. */
      private transient volatile Object[] array;

        我们可以看到里面有两个全局变量,其中lock锁就是每次在做写操作时,锁的句柄,array就是具体存储数据的数组,注意这里的array被volatile所修饰,因此可以在并发情况下实现数据的可见性。

        当用new创建了一个CopyOnWriteArrayList时,如果是使用无参的构造函数,则将array的长度默认成0,创建了一个空的数组。

public CopyOnWriteArrayList() {//无参构造
       setArray(new Object[0]);
  }
  final void setArray(Object[] a) {
       array = a;
  }

然后我们在使用CopyOnWriteArrayList中的add添加数据时,先使用lock锁,并获取到当前的array数组,然后对array进行copyof,新的数组的长度时之前的长度+1,这样才能存放当前新的值,将新值填充后,在替换掉旧的array数组后,释放当前锁。

public boolean add(E e) {
         final ReentrantLock lock = this.lock;
         lock.lock(); //加锁
         try {
             Object[] elements = getArray(); //获取array数组
             int len = elements.length;//得到它的长度
             Object[] newElements = Arrays.copyOf(elements, len + 1); //使用copyof方法将旧的数组在原先的长度上加1变成一个新的数组,
             newElements[len] = e; //在新数组的最后把e添加进去
             setArray(newElements);//替换旧的数组
             return true;
         } finally {
             lock.unlock(); //释放锁
         }
  }

我们使用CopyOnWriteArrayList中的remove方法删除元素时,先使用lock上锁,然后再获取当前的array数组,如果传入的index正好是最后一个索引,那么numMoved计算出来的就是0,则使用copyOf,长度进行-1去除最后一个数据。如果传入的不是最后一个,先声明一个新的array数组,数组的长度就是旧的arra的长度-1,再将0到index的数据array复制到新的array数组,然后再将index+1后的再array复制新的array数组,最后将新的array数组替换旧的,然后释放锁。

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));//直接将长度-1的数组设置到array
              else {//如果不是最后一个计算一下,分两部分0到index,index+1到最后,然后将新的替换旧的
                  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();//释放锁
          }
  }

总结:

1、当new新建一个CopyOnWriteArrayList后会生成一个数组array来存放添加的内容,如果是无参的构造函数,则array的长度为0,添加数据时再进行扩容。同时会声明一个ReentrantLock锁。

2、当进行add操作时,先进行上锁,然后对当前的array进行copyOf,并且新的长度时之前长度的+1,这样才能存放当前新的值,将新值填充后,再替换掉旧的array数组后,释放当前锁。

3、当使用get获取数据时,无需上锁,直接读取当前array数组的指定位置。

4、当使用remove时,同样先进行上锁,然后再获取当前的array数组,如果传入的index正好是最后一个,则使用copyOf,长度进行-1,否则的话先声明一个新的array数组,现将0到index的数据arraycopy至新的array数组,然后再将index+1后的再arraycopy至新的array数组,最后将新的array数组替换旧的,然后释放锁。

        CopyOnWriteArrayList实现了写写隔离,但读读是可以共享的,这就有可能出现当某个数据再修改时,读进行了操作,导致读取到的还是旧的数据。还有就是每次写操作都对数组就行copy。假如数据量非常大的情况下,进行Copy消耗的资源则会进行*2,因此使用CopyOnWriteArrayList时,需要考虑下自己的数据量以及读写的频次。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值