java面试总结--【2】集合

    xiaoxiao2023-11-09  141

    一.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可见的。

    最新回复(0)