集合概念
集合与数组
数组是固定长度;集合是动态长度的数据结构,需要动态增加或删除元素
数组可以包含基本数据类型和对象;集合只能包含对象
数组可以直接访问元素;集合需要通过迭代器访问元素
线程安全的集合?
java.util包:
- Vector:线程安全的动态数组,内部方法大部分都经过synchronized修饰
- Hashtable:线程安全的哈希表,内部方法大部分都经过synchronized修饰,这样被所著的就是整个Table对象(底层是数组+链表)
并发Map:
- ConcurrentHashMap:
- JDK7,ConcurrentMap加的是分段锁(Segment锁),每个分段锁含有整个table的一部分,不同分段之间的并发互不影响【每个Segment都类似一个小的HashMap,对于插入、更新、删除操作时,需要先定位到具体的Segment,再在Segment上加锁】
- JDK8,取消了Segment字段,直接在table元素上枷锁,实现对每一行进行加锁,进一步减少了并发冲突。主要通过volatile + CAS(乐观锁)或 synchronized(悲观锁)来实现的线程安全。
添加元素时,先判断容器是否为空:
- 为空:直接使用volatile + CAS来初始化
- 不为空:根据存储的元素计算该位置是否为空
- 如果存储的元素计算结果为空,利用CAS设置该节点
- 如果存储的元素计算结果不为空,使用synchronized,然后遍历桶中的元素,并替换或新增节点到桶中,再判断是否需要转成红黑树。(当发生hash碰撞时说明容量不够用或已经有大量线程访问,因此使用synchronized来处理hash比CAS效率高)
- ConcurrentSkipListMap:实现了一个基于跳表(SkipList)算法的可排序的并发集合,通过维护多个指向其他元素的跳跃链接来实现高效查询
并发Set:
- ConcurrentSkipListSet:线程安全的有序集合,底层是使用ConcurrentSkipListMap实现
- CopyOnWriteArraySet:线程安全的HashSet
并发List:
- CopyOnWriteArrayList:线程安全的ArrayList,底层通过一个数组保存数据,使用volatile关键字修饰数组,保证当前线程对数组对象重新赋值后,其他线程可以感知到;在写入新元素时,会将原来的数组拷贝一份并让原来数组的长度+1后就得到一个新数组,将元素放入新数组的最后一个位置,用新数组地址替换旧数组地址就能得到最新数据了;读操作时不加锁,一直都能读的。
并发Queue:
- ConcurrentLinkedQueue:高并发场景下的队列,通过无锁(CAS)的方式,实现高并发状态下的高性能。
- BlockingQueue:提供一种读写等待的机制,简化多线程间的数据共享。
并发Deque:
-
LinkedBlockingDeque:线程安全的双端队列,内部使用双向链表结构
-
ConcurrentLinkedQueue:基于链表节点的无限并发链表,可以安全的并发执行插入、删除、访问的操作
List
ArrayList是线程安全的嘛?有什么办法可以把ArrayList变成线程安全的?
- 使用Collections类的synchronizedList方法将ArrayList包装成线程安全的List
List<String> synchronizedList = Collections.synchronizedList(arrayList);
- 使用CopyOnWriteArrayList(这是一个线程安全的List)代替ArrayList
CopyOnWriteArrayList<String> copyOnWriteArrayList = new CopyOnWriteArrayList<>(arrayList);
- 使用Vector类代替ArrayList
Vector<String> vector = new Vector<>(arrayList);
Map
HashMap是线程安全的吗?
HashMap不是线程安全的,在多线程环境下会存在以下问题:
- JDK7,采用数组+链表,多线程环境下,数组扩容时会存在Entry链死循环和数据丢失的问题
- JDK8,采用数组+链表+红黑树,解决了Entry链死循环和数据丢失的问题,但是多线程环境下,put元素会出现覆盖的情况
保证线程安全:
- 使用Collections.synchronizedMap同步加锁的方式,还可以使用HashTable。
- 使用ConcurrentHashMap。(JDK7使用分段锁;JDK8使用CAS+synchronized)
HashMap一般用什么作为Key?为什么String适合做key?
用String做key,因为String对象是不可变的,一旦创建旧不能被修改,保证了key的稳定性。如果key是可变的,会导致hashCode和equals方法的不一致。
为什么HashMap要用红黑树而不是平衡二叉树?
平衡二叉树追求的是绝对平衡,导致每次在插入或删除节点时,都需要通过左旋或右旋来调整,使它再次称为一颗符合要求的平衡二叉树
红黑树不支持这种完全平衡的规则, 牺牲了一部分的查找效率,但是可以换取一部分维持平衡状态的成本。
HashMap的key可以为null嘛
HashMap使用hash()方法来计算key的哈希值,当key为空时,直接设置hash值为0,不走hashCode方法。
static final int hash(){return (key == null) ? 0 : 走hashCode()后处理
}
null作为key只能有一个,作为value可以有多个
重写HashMap的equals和hashCode方法需要注意什么?
从HashMap中获取值时,这些方法也会被用到,如果这些方法没有被正确的实现,两个不同的Key可能会产生相同的hashCode()和equals()输出,HashMap会认为他们是相同的,把他们存在不同的地方。
重写HashMap的equals方法不当会出现什么情况?
HashMap在比较元素时,先比较hashCode方法,如果hash值相同,再比较equals方法。
如果重写hashCode方法,但是不重写equals方法,会出现equals方法返回false,导致hashMap中存储多个一样的对象,与hashMap只有唯一的key的规范不符合。
HashMap在多线程下可能出现的问题?
- JDK7使用头插法插入元素,多线程环境下,扩容时可能导致环形链表的出现,形成死循环。因此JDK8使用尾插法插入元素,在扩容时保持链表原本的顺序不变,不会出现环形链表的问题
因为JDK7使用头插法,对数组长度进行扩容时,如果原来是1->2->3,扩容后就会变成3->2->1
如果多个线程同时对HashMap进行扩容操作,可能会导致链表的指针混乱,形成环形链表。
- 多线程同时执行put操作,如果计算出来的索引位置是想通的,那会造成前一个key被后一个key覆盖,从而导致元素丢失的问题
HashMap的扩容机制
在扩充HashMap时,不需要重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0
索引 = (数组下标 - 1) & 哈希值
- 如果是0:索引没变
- 如果是1:索引 = 原索引 + 旧容量
所以HashMap的大小是2的n次方,这样就不需要重新计算hash值。
HashMap和HashTable的区别
- HashMap线程不安全,可以存储null的key和value,每次扩容成原来的2n,要保证线程安全也可以用ConcurrentHashMap。
- HashTable线程安全,所有的方法都加synchronized锁,不可以有null的key和value,效率低,每次扩容为原来的2n + 1。
HashTable和ConcurrentHashMap的区别?
- ConcurrentHashMap是线程安全的。读操作不需要加锁,写操作需要加锁。JDK7,ConcurrentHashMap采用数组 + 链表,并发控制用分段锁;JDK8,ConcurrentHashMap采用数组 + 链表 + 红黑树,并发控制用CAS + synchronized
- HashTable采用的是数组 + 链表,所有的方法都加synchronized锁
Set
Set集合的特点?
Set集合是唯一的,不会有重复的元素。
向Set集合中插入元素时,先通过hashCode确定元素的存储位置,再通过equals判断是否存在相同的元素,如果存在不会再次插入。
有序Set是什么?
有序的Set是TreeSet和LinkedHashSet
- TreeSet是基于红黑树来保证元素的自然顺序
- LinkedHashSet是基于双向链表和哈希表的结合来保证元素添加的自然顺序(可以记录插入顺序的集合,不仅保证元素的唯一性,还可以保证元素的插入顺序)