这篇文章用于总结有关于Java中集合相关知识,背完老是忘,遂做个文档记录一下。
首先明确集合的概念,就是用于存储数据的容易,可以存储基本数据类型,也可以存储对象或者集合本身。
一张图总结有关集合类的特点:
其中每类集合中又有一些比较重要的类,下面单独拿出来记录。
List:
特点:存放的元素都是有序并且可重复的。
常见实现类:
1:ArrayList:
底层数据结构:动态数组
扩容方式:首先如果在创建ArrayList对象的时候没有传递初始值的话,会给一个默认值10,但是这个容量并不是在创建对象的就初始化,而是会在添加第一个元素的时候才会初始化。然后当添加元素后会判断当前长度+1后是否比当前数组容量小,如果小的话可以添加,如果超过的话就会进行扩容,会按照1.5倍进行扩容,并把原数组的值全部拷贝到新数组中。
是否线程安全:不安全 可以使用 Collections.synchronizedList(new ArrayList());
得到线程安全的对象。或者使用CopyOnWriteArrayList
类进行替代。
CopyOnWriteArrayList 原理:
CopyOnWrite 就是当我们往一个容器添加元素的时候,不直接往容器中添加,而是先复制出一个新的容器,然后在新的容器里添加元素,添加完之后,再将原容器的引用指向新的容器。多个线程在读的时候,不需要加锁,因为当前容器不会添加任何元素。这样就实现了线程安全。
2:LinkedList:
底层数据结构:双向链表
扩容方式:因为是通过链表连接所以不需要扩容,只是会多占用一些内存空间。
set:
特点:元是无序且不可重复的
常见实现类:
1:HashSet:
HashSet 的底层数据结构是哈希表。在 Java 中,HashSet 内部实际上是使用 HashMap 来存储元素的。存到key上!!!
1:不允许存储重复元素:在添加元素时,如果集合中已经存在相同的元素(通过 hashCode 和 equals 方法判断),则添加操作会失败。
2:无序:元素的存储顺序不固定,不保证迭代顺序与添加顺序一致。
3:基于哈希表实现:内部使用哈希表来存储元素,因此查找、添加和删除元素的平均时间复杂度为 O(1)
2:TreeSet
TreeSet 底层基于红黑树结构来存储元素。
TreeSet 的主要特点包括:
元素有序:元素会按照自然顺序(对于实现了 Comparable 接口的元素)或者指定的比较器(Comparator)所定义的顺序进行排序。
元素唯一:不允许存储重复的元素。
实际应用:TreeSet 常用于需要对元素进行排序且不允许重复的场景。
Map:
特点:双列集合:key值唯一,value值不唯一。
常见实现类:
1:HashMap:
1:底层数据结构:在 Java 8 及以后,HashMap 由数组、链表和红黑树组成。当链表长度超过一定阈值(默认为 8)时,会将链表转换为红黑树,以提高查找效率。1.7是数组+链表组成(头插法)
2:键值唯一性:键不能重复,如果添加重复的键,新的值会覆盖旧的值。
3:无序性:不保证键值对的存储和遍历顺序。
快速查找、插入和删除:平均情况下,这些操作的时间复杂度为 O(1)。
在 Java 1.8 中,HashMap 的扩容机制如下:
当 HashMap 中元素的数量大于等于阈值(threshold)时,会进行扩容。阈值等于负载因子(loadFactor)乘以当前数组的容量(capacity),默认负载因子为 0.75。
扩容时,会使用一个容量为原来两倍的新数组来代替原数组。
在将原数组元素拷贝到新数组的过程中,元素在新数组中的位置计算方式较为巧妙。由于数组的容量是以 2 的幂次方进行扩容的,在二进制上表现为多了一个高位参与数组下标计算。对于一个元素,只需要看其原来的哈希值新增的那个高位(bit)是 1 还是 0,是 0 的话索引不变,是 1 的话索引变成“原索引 + oldCap”(其中 oldCap 为原数组的容量)。
这样做可以省去重新计算哈希值的时间,并且由于新增的 1 bit 是 0 还是 1 可以认为是随机的,因此 resize 的过程会均匀地把之前冲突的节点分散到新的 bucket 中,从而提高了 HashMap 的性能。
例如,原数组长度为 16(二进制为 00010000),扩容后长度为 32(二进制为 00100000)。如果原哈希值的倒数第五位是 0,与新数组长度减 1(00011111)进行与操作后,得到的索引不变;如果原哈希值的倒数第五位是 1,与新数组长度减 1 进行与操作后,索引就变成了原索引加上 16(原数组长度)。
具体来说,在 put 方法执行过程中,如果发现当前存储的数量大于等于阈值,就会触发扩容操作。首先创建一个新的容量为原来两倍的数组,然后采用尾插法将原数组元素拷贝到新数组中,根据元素原来的哈希值来确定其在新数组中的位置。
写一下自己理解中的ConcurrentHashMap(重要) :
先讲一下为什么要用到ConcurrentHashMap ,是因为HashMap 在多线程环境下不安全,所以才会出现ConcurrentHashMap
HashMap 不安全的原因主要有以下几点:
1:在多线程环境下,同时进行 put 操作可能会导致数据结构的不一致。例如,当两个线程同时进行扩容操作时,可能会导致链表形成环形结构,从而在后续的操作中出现死循环。在1.7中
2:在多线程环境下,同时进行 put 和 resize 操作可能会导致数据丢失。
为了使 HashMap 能够在多线程环境下安全使用,可以使用 ConcurrentHashMap 进行改进。
ConcurrentHashMap 相对于 HashMap 的主要改进在于其并发安全性。它采用了分段锁的机制,将数据分成多个段(Segment),每个段内部类似于一个小型的 HashMap,并且拥有自己独立的锁。这样,在多线程并发访问时,不同的线程可以同时访问不同段的数据,只有在对同一段进行操作时才需要获取锁,从而大大提高了并发性能。
下面是详细介绍:
ConcurrentHashMap是 Java 中一个线程安全的哈希映射表实现。它解决了HashMap在多线程环境下不安全的问题,同时避免了Hashtable使用全局锁导致效率低下的问题。
在 Java 1.7 和 Java 1.8 中,ConcurrentHashMap的实现方式有所不同。
Java 1.7中的实现:
采用分段锁的技术。由segment数组结构和HashEntry数组结构组成。segment是一种可重入锁ReentrantLock,扮演锁的角色。一个ConcurrentHashMap里包含一个segment数组,每个segment的结构和HashMap类似,是一种数组和链表结构,包含一个HashEntry数组,每个HashEntry是链表结构的元素,每个segment守护着一个HashEntry数组里的元素。当对HashEntry数组的数据进行修改时,必须首先获得它对应的segment锁。
segment的默认数量为16,即并发度为16,这个值可以在构造函数中设置。如果自己设置了并发度,ConcurrentHashMap会使用大于等于该值的最小的2的幂指数。
Java 1.8中的实现:
数据结构上选择了与HashMap相同的node数组+链表+红黑树结构。锁的实现上,抛弃了原有的segment分段锁,采用CAS+synchronized实现更加细粒度的锁,将锁的级别控制在更细粒度的哈希桶数组元素级别,只需锁住链表头节点(红黑树的根节点),不影响其他哈希桶数组元素的读写,大大提高了并发度。
另一个解释:
在 JDK 1.7 中,ConcurrentHashMap 的底层数据结构是由 segments 数组+hashentry 数组+链表组成的。它采用了分段锁的技术来保证线程安全。
具体来说,一个 ConcurrentHashMap 中有一个 segments 数组,数组中的每个元素是一个 segment 对象。segment 继承自 ReentrantLock 锁,它类似于一个可重入锁。每个 segment 中包含一个 hashentry 数组,每个 hashentry 是一个链表结构的元素。
在进行操作时,首先将数据分成一段一段地存储,然后给每一段数据配一把锁。这样在多线程访问时,不同段的数据可以并发访问,而不会存在锁竞争,只有访问同一段数据时才需要获取对应的锁,从而有效地提高了并发访问效率。
而在 JDK 1.8 中,ConcurrentHashMap 放弃了段锁的概念,借鉴了 HashMap 的数据结构(数组+链表+红黑树),同时结合了 CAS(Compare and Swap)乐观锁和 synchronized 锁来实现线程安全。其主要的数据结构包括:
数组:用于存储元素。
链表:当元素发生哈希冲突时,形成链表结构。
红黑树:当链表长度达到一定阈值时,将链表转换为红黑树,以提高查找效率。
在 JDK 1.8 中,锁的粒度更细,锁是锁的链表的头节点,不影响其他元素的读写,从而进一步提高了并发性能。并且,数组和节点的一些属性(如 node 的 val 和 next)使用 volatile 修饰,保证了可见性。
在 JDK 1.8 中,put 操作的大致流程如下:
先判断 node 数组有没有初始化,如果没有则先初始化。
根据 key 进行 hash 操作,找到 node 数组中的位置,如果不存在 hash 冲突(即该位置是 null),直接用 CAS 插入。
如果存在 hash 冲突,就先对链表的头节点或红黑树的头节点加 synchronized 锁。
如果是链表,就遍历链表,如果 key 相同就执行覆盖操作,如果不同就将元素插入到链表的尾部,并且在链表长度大于 8,node 数组的长度超过 64 时,会将链表转化为红黑树。
如果是红黑树,则按照红黑树的结构进行插入。
get 操作全程无锁,因为 node 元素的 val 和指针 next 是用 volatile 修饰的,在多线程环境下,线程 A 修改节点的 val 或者新增节点时对线程 B 可见。其操作流程大致为:
计算 hash 值,定位到 node 数组中的位置。
如果该位置为 null,则直接返回 null。
如果该位置不为 null,再判断该节点是红黑树节点还是链表节点。如果是红黑树节点,使用红黑树的查找方式进行查找;如果是链表节点,遍历链表进行查找。