一.collection
1.java的集合继承于接口collection,collection继承于接口Iterable,如下图:
二、List
list是继承于collection的接口,主要特点是存取是有序的,并且可存取重复的元素。常用的有两个实现类,ArrayList和LinkedList。
1)ArrayList
ArrayList就是基于数组的一个线性表,只不过数组的长度可以动态改变而已。
ArrayList类似数据的存取结构,因此随机访问速度快,但是随机插入和删除由于需要整个数组的移动,因此插入和删除效率低。
- ArrayList的初始长度是10;
- ArrayList扩容每次长度是(已有长度*3/2) + 1;
- ArrayList是线程不安全的,如果要安全的则使用
2)LinkedList
LinkedList就是一种双向循环链表的链式线性表,只不过存储的结构使用的是链式表而已。
- 适合频繁插入和删除操作
- 随机访问速度很慢,因为需要需要从头开始一个一个查找
- 由于LinkedList实现了接口Dueue,所以LinkedList可以被当做堆栈来使用
问题:ArrayList和LinkedList怎么实现多线程安全?
可以使用Collections.synchronizedList(),如下:
List<Map<String,Object>> data=Collections.synchronizedList(new ArrayList<Map<String,Object>>());
LinkedList也类似。
三、Set
Set接口的实现类有HashSet和TreeSet,其中HashSet的子类又有LinkedHashSet。
Set是只能保存不重复的元素,并且是不能保证插入顺序的
1)HashSet
HashSet的底层实现其实就是基于HashMap,创建一个value为固定值的map,其他方法都是调用HashMap的方法。
2)TreeSet
TreeSet底层是使用二叉树实现,存储的元素是经过排序的,因此对象要实现compare方法。底层和HashSet类似,其实大部分方法使用TreeMap来实现的。
3)LinkedHashSet
LinkedHashSet可以保证存取顺序的。底层实现主要方法是基于LinkedHashMap的。
四、Map
1)HashMap
a. HashMap采用数组和链表的结果来保存数据。
b. put()方法是先对key使用hash()方法计算出hash值,根据hash值找到存储位置,如果存储位置已经被占,则发送hash冲突,此时会将新值以链表形式存储,即Entry要存储下一个元素的引用。
c. get()方法是先对key进行hash()计算得出存储位置,如果存储位置存在链表,则遍历链表,根据对象的equals方法来寻找需要的元素。
d. HashMap创建时初始大小为16,负载因子为0.75。即当元素个数达到当前数组大小*0.75时,开始扩容一倍。
e.扩容是很耗性能的操作,会把当前的所有元素存储地址计算一遍,所以如果提前知道整个map的大小,就创建时指定大小来提升性能。
2)TreeMap
TreeMap中的元素会自动根据key定义的排序方法进行排序,使用二叉树结构。
a.自然排序 :TreeMap中的key必须实现compareble接口
b.自定义排序:创建TreeMap时传入一个comparator对象
3)LinkedHahsMap
LinkedHahsMap可以保持两种顺序,分别是插入顺序和访问顺序,默认是按照插入顺序。
a. LinkedHahsMap是HashMap的亲儿子,增加了双向链表的结构(即增加了前后指针),其他处理逻辑和HashMap一样。
4)ConcurrentHashMap
1. 谈谈你理解的 HashMap,讲讲其中的 get put 过程。
2.1.8 做了什么优化?
答:1.7的结构采用位桶+链表结构,而1.8的结构采用位桶+链表+红黑树结构,即当链表超过指定的阀值时,自动转成红黑树
3. 是线程安全的嘛?
答:不是线程安全的。
4.不安全会导致哪些问题?
答:不是线程安全的,主要是以下两点:
a.hash碰撞,当多个线程同时在同一个桶的链表添加元素时,同时获取了head,会导致只有一个成功。
b.map扩容,当多个线程同时进行扩容时,会新建一个数组,重新计算hash值,这时就会
5.如何解决?有没有线程安全的并发容器?
6.ConcurrentHashMap 是如何实现的? 1.7、1.8 实现有何不同?为什么这么做?
HashTable容器在竞争激烈的并发环境下表现出效率低下的原因,是因为所有访问HashTable的线程都必须竞争同一把锁,那假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁。这里“按顺序”是很重要的,否则极有可能出现死锁。
JDK1.8的实现已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap,虽然在JDK1.8中还能看到Segment的数据结构,但是已经简化了属性,只是为了兼容旧版本。
get操作可以无锁是由于Node的元素val和指针next是用volatile修饰的,在多线程环境下线程A修改因为hash冲突修改结点的val或者新增节点的时候是对线程B可见的。