文章目录
Java集合
集合结构图
Java 集合类型分为 Collection 和 Map,它们是 Java 集合的根接口,这两个接口又包含了一些子接口或实现类。
Collection
Collection 接口的接口对象的集合(单列集合):它包含三类,List、Set、Queue
List 接口:
元素按进入先后有序保存,可重复
1.LinkedList 接口实现类, 链表, 插入删除, 没有同步, 线程不安全
2.ArrayList 接口实现类, 数组, 随机访问, 没有同步, 线程不安全
3.Vector 接口实现类 数组, 同步, 线程安全3.1Stack 是Vector类的实现类
LinkedList和ArrayList之间的区别?
存储结构:LinkdedList底层是以双向链表的数据结构进数据存储,每个元素都有指向前后元素的指针。ArrayList是以数组列表的结构进行数据存储。
特点:由于LinkedList是链表存储结构,ArrayList是数组存储结构。所以,在随机访问get和set,ArrayList优于LinkedList,因为ArrayList可以随机定位,而LinkedList要移动指针一步一步的移动到节点处;对于新增和删除操作add和remove,LinedList比较占优势,只需要对指针进行修改即可,而ArrayList要移动数据来填补被删除的对象的空间。
线程安全是什么?LinkedList和ArrayList为什么是线程不安全?
线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。
线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据
针对什么是线程安全,线程安全是对访问的数据进行加锁机制,而LinkedList和ArrayList是没有进行加锁,因此,可能会出现数据污染。
例如,为一个数组 添加数据。分为两个步骤:
1.Items[Size] 的位置存放此元素;
2.增大 Size 的值。
在单线程运行的情况下,如果 Size = 0,添加一个元素后,此元素在位置 0,而且 Size=1;
而如果是在多线程情况下,比如有两个线程,线程 A 先将元素存放在位置 0。但是此时 CPU 调度线程A暂停,线程 B 得到运行的机会。线程B也向此 ArrayList 添加元素,因为此时 Size 仍然等于 0 (注意哦,我们假设的是添加一个元素是要两个步骤哦,而线程A仅仅完成了步骤1),所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增加 Size 的值。那好,现在我们来看看 ArrayList 的情况,元素实际上只有一个,存放在位置 0,而 Size 却等于 2。这就是“线程不安全”了。
目前线程安全解决办法有?
四种方法
LinkedList和ArrayList换成线程安全的集合,如CopyOnWriteArrayList,ConcurrentLinkedQueue
方法1: 使用Lock接口下的实现类。Lock是juc(java.util.concurrent)包下面的一个接口。常用的实现类就是ReentrantLock 类,它其实也是一种悲观锁。
CopyOnWriteArrayList的源码:使用的是Lock实现同步
原理看上图估计也可以看出来,就是先拷贝一份,等修改完毕之后在把变量的引用地址修改成新的数组即可,在修改的时候读操作不影响。
方法二:使用乐观锁机制。(使用版本控制和CAS实现)
ConcurrentLinkedQueue是基于链表的队列,并且它是线程安全的,采用乐观锁CAS来保证原子操作
ConcurrentLinkedQueue:(LinkedList有offer和poll,ArrayList没有)
基本都是采用 CAS 自旋来实现的,保证了线程的安全性。(CAS:Compare and Swap,即比较再交换。)
ConcurrentLinkedQueue 是一个线程安全且 非阻塞 的 无界 队列,它采用先进先出的规则,实现了 AbstractQueue 基础抽象类和 Queue 接口。
用offer()实现入队列,poll()实现出队列(offer与add相似,poll和remove相似),在ConcurrentLinkedQueue中的offer和poll区别与LinkedList。
方法3:使用synchronized,是一种悲观锁。
①: Collections.synchronizedList(new LinkedList())
②:Vector(内部主要使用synchronized关键字实现同步)
在Vecter中,对数据进行加synchronized关键字,synchronized独占锁,将多线程执行变成串行化。进行数据加锁,保证数据安全,防止数据出现脏读。
synchronized关键字底层实现主要是通过monitorenter 与monitorexit计数 ,如果计数器不为0,说明资源被占用,其他线程就不能访问了,但是可重入的除外。说到这,就来讲讲什么是可重入的。这里其实就是指的可重入锁:指的是同一线程外层函数获得锁之后,内层递归函数仍然有获取该锁的代码,但不受影响,执行对象中所有同步方法不用再次获得锁。避免了频繁的持有释放操作,这样既提升了效率,又避免了死锁。
方法4:使用线程本地存储ThreadLocal。
当多个线程操作同一个变量且互不干扰的场景下,可以使用ThreadLocal来解决。它会在每个线程中对该变量创建一个副本,即每个线程内部都会有一个该变量,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能。
拓展链接:synchronized与ReentrantLock区别
https://blog.csdn.net/zxd8080666/article/details/83214089
Set 接口:
仅接收一次,不可重复,并做内部排序
1.HashSet 使用hash表(数组)存储元素
1.1LinkedHashSet 链表维护元素的插入次序
2.TreeSet 底层实现为二叉树,元素排好序
HashSet是Set接口的典型实现,大多数时候使用Set集合时就是使用这个实现类。HashSet按Hash法来存储集合中的元素,因此具有很好的存取和查找性能。底层数据结构是哈希表。HashSet底层是基于HashMap实现的,HashMap底层数据结构是基于数组+链表实现的。
内部存储机制
当向HashSet集合中存入一个元素时,HashSet会调用该对象的hashCode方法来得到该对象的hashCode值,然后根据该hashCode值决定该对象在HashSet中的存储位置。
当使用HashSet 时,hashCode()方法就会得到调用,判断已经存储在集合中的对象的hash code 值是否与增加的对象的 hash code 值一致;如果不一致,直接加进去;如果一致,再进行 equals 方法的比较,equals 方法如果返回 true,表示对象已经加进去了,就不会再增加新的对象,否则加进去。
Queue:
用于模拟"队列"这种数据结构(先进先出 FIFO)。Queue的实现类有LinkedList和PriorityQueue。最常用的实现类是LinkedList。
特别的类LinkedList
LinkedList类是一个比较奇怪的类,它即是List接口的实现类,这意味着它是一个List集合,可以根据索引来随机访问集合中的元素。除此之外,LinkedList还实现了Deque接口,Deque接口是Queue接口的子接口,它代表一个双向队列(个人理解:offer和poll是队列独有的方法)
Map
Map中的元素是两个对象,一个对象作为键(Key),一个对象作为值(Value)。键不可以重复,但是值可以重复。key和value可以是任意的引用类型的数据
Map集合存储的元素都是成对出现的,把这样的元素理解为:夫妻对。
Collection集合存储的元素都是单独出现的,Collection接口下面的Set是元素唯一的, List集合中元素是可以重复的。这样的单独出现的元素,可以理解为单身
Map 接口:
键值对的集合 (双列集合)
实现类
1.Hashtable 接口实现类, 同步,线程安全(加入synchronized关键字),不可以存入null键,null值。
2.HashMap 接口实现类 ,没有同步, 线程不安全,可以存入null键,null值。要保证键的唯一性,需要覆盖hashCode方法,和equals方法。HashMap的线程不安全主要是发生在扩容函数中,即根源是在transfer函数中
关于HashMap能够有一个Key,
对于HashMap的containsKey和get的源码中,都调用了一个getNode方法,关键在于key上面的hash方法。发现,最终的返回值取决于key.hashCode()方法。我们发现key是否相同,取决于HashCode是否相等。String和Interger的HashCode方法,重写了此方法,并且只和value值有关。因此只要内容相同,他们的hashCode就是相等的
2.1LinkedHashMap。双向链表和哈希表实现;该子类基于哈希表又融入了链表。可以Map集合进行增删提高效率。
3.WeakHashMap, 和HashMap一样,WeakHashMap 也是一个散列表,它存储的内容也是键值对(key-value)映射,而且键和值都可以是null。不过WeakHashMap的键是“弱键”。在 WeakHashMap 中,当某个键不再正常使用时,会被从WeakHashMap中被自动移除。
4.TreeMap 红黑树对所有的key进行排序,是线程不安全的。底层是二叉树数据结构。可以对map集合中的键进行排序。
TreeMap是存储键值对(key-value结构)的自平衡二叉树,又称红黑树。TreeMap的key是有序且不可为空的,但是value是可以为空的。
首先红黑树是一棵特殊的二叉查找树,二叉查找树的每个节点键值大于左孩子的键值,小于或等于右孩子的键值;二叉查找树在特殊情况下所有子节点都在左边或者右边,这被称为二叉查找树的左倾或右倾,极端情况下,二叉查找树就变成了一个有序的链表了,而红黑树呢通过对二叉查找树的着色,将二叉查找树变成了一棵相对平衡的树,不会出现左倾或右倾的情况:
(1) 每个节点或者是黑色,或者是红色。
(2) 根节点是黑色。
(3) 每个叶子节点是黑色。 [注意:这里叶子节点,是指为空的叶子节点!]
(4) 如果一个节点是红色的,则它的子节点必须是黑色的。
(5) 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
5.IdentifyHashMap:允许key重复
IdentityHashMap,比较key值,直接使用的是==
功能:
1、添加功能: put(K key,V value)将指定的值与该映射中的指定键相关联
2、删除功能:remove(Object key)如果存在,从该map集合中删除一个键的映射
void clear()从该map集合中删除所有的映射
3、长度功能:int size()返回此地图中键值映射的数量
集合的遍历
方式一、根据键找值,借助Set keySet()遍历
//方式一,用增强for循环
Set<String> keys = map.keySet();
for(String s : keys){
String key = s;
String value = map.get(s);
System.out.println(key+"="+value);
}
//方式二,用迭代器
//为Set集合创建一个迭代器Iterator
Set<String> itkeys = map.keySet();
Iterator<String> it = itkeys.iterator();
while(it.hasNext()){
String key = it.next();
String value = map.get(key);
System.out.println(key+"="+value);
}
方式二、根据键值对对象找键和值
//方式一、增强for循环
Set<Map.Entry<String,String>> entrySet = map.entrySet();
for(Map.Entry<String,String> s : entrySet){
String key = s.getKey();
String value = s.getValue();
System.out.println(key+"="+value);
}
//方式二、迭代器遍历
Set<Map.Entry<String,String>> entrySet1 = map.entrySet();
Iterator<Map.Entry<String,String>> it = entrySet.iterator();
while(it.hasNext()){
Map.Entry<String,String> entry = it.next();
String key = entry.getKey();
String value = entry.getValue();
System.out.println(key+"="+value);
}