Loading

Java 容器

date: 2020-12-09 14:22:23
updated: 2020-12-12 17:47:23

Java 容器

  • Collection
    • List:可以重复
      • CopyOnWriteArrayList
      • Vector
      • ArrayList
      • LinkedList
    • Set:不可重复
      • HashSet & LinkedHashSet:HashSet无序,哈希表;LinkedHashSet 通过链表可以实现有序
      • SortedSet & TreeSet:有序,红黑树
      • EnumSet
      • CopyOnWriteArraySet
      • ConcurrentSkipListSet
    • Queue:主要是针对多线程 JUC
      • Deque:双端队列
        • ArrayDeque 实现类
        • LinkedList 实现类
        • BlockingDeque 接口 JUC重点
      • BlockingQueue:阻塞队列
        • ArrayBlockingQueue
        • PriorityBlockingQueue
        • LinkedBlockingQueue
        • TransferQueue & LinkedTransferQueue:有一定容量,但是一个生产者放进去一个值,如果没消费者来消费,就会一直在等待
        • SynchronousQueue:容量为空,必须放进去一个值才能拿,也必须拿走了才能继续生产。和LinkedTransferQueue的区别就是SynchronousQueue没有容量
        • Delayqueue
      • PriorityQueue:小顶堆或大顶堆(具体是什么堆,通过自定义comparator),只拿最上面的那一个。由于底层实现是堆(即二叉树)实现的,所以并不能保证线程同步。如果要支持并发,需要用PriorityBlockingQueue。
      • ConcurrentLinkedQueue
      • DelayQueue
  • Map
    • HashMap & LinkedHashMap
    • TreeMap
    • WeakHashMap
    • IdentityHashMap
    • ConcurrentHashMap
    • ConcurrentSkipListMap

为什么没有 ConcurrentTreeMap?因为树上锁的话一般就需要锁一整棵树,影响效率;同时对树进行修改,如果底层是红黑树,修改数据可能导致整棵树的转换。
ConcurrentHashMap 无法保证顺序,如果要高并发+顺序,可以使用 ConcurrentSkipListMap

CAS和Synchronized的效率比较:

  1. Synchronized的过程:无锁 -> 偏向锁 -> 自旋锁 -> 重量锁
  2. 如果线程不是很多,或者执行操作的时间不是很长的时候,cas可能会好一点

vector 和 arraylist 相似,都是动态数组,但是两者有不同

  1. vector 里面方法全是 synchronized 的
  2. vector 包含了很多传统的方法,这些方法不属于集合框架

ArrayList 不带锁,如果要带锁的话,Collections.SynchronizedList(list) 返回的就是加了锁的list,这个集合类方法会在所有操作上加一个 synchronized
LinkedList 不带锁,如果要带锁的话,Collections.SynchronizedList(list) 返回的就是加了锁的list
ArrayList 和 LinkedList 的区别:

  1. ArrayList 底层是数组实现,LinkedList 底层是双向链表实现
  2. ArrayList 查询快;LinkedList 增删快 => 原因是数组增删要移动数据,链表查询要移动指针

存储形式:
顺序存储 数组 查询快,增删慢
链式存储 链表 查询慢,增删快
散列存储 哈希 数组+链表 查询和增删很快

HashMap、LinkedHashMap 和 Treemap 的区别:

  1. HashMap 无序;LinkedHashMap 通过链表维护Key的插入顺序;TreeMap 通过红黑树来维护Key的自然顺序或者自定义顺序,但不是插入顺序。

    HashTable 不允许key或value为空;put get 方法都加上了synchronized,线程同步,性能较低。如果要用线程同步的HashMap可以用 ConcurrentHashMap

  2. HashMap 底层是 数组+链表,链表长度大于8的时候,转成红黑树;LinkedHashMap 比HashMap 多维护一个双向链表,可以按照插入顺序从头部或尾部迭代;

    LinkedHashMap 在HashMap 的基础上进行拓展,数组不变,链表增加了before和after,也就是将所有的kv(不论在数组的哪一个槽位)都通过前后指针连了起来。
    LinkedHashMap 通过accessOrder 来决定是按照插入顺序还是访问顺序,accessOrder = true,会调用 afterNodeAccess() 方法,HashMap也调用了不过是空方法,LinkedHashMap 通过这个方法将访问的节点也就是kv放到链表的最后。另外在accessOrder = true的模式下,迭代LinkedHashMap的同时去查询数据,会导致fail-fast,因为afterNodeAccess()会修改modCount,迭代的顺序已经改变。
    LinkedHashMap 重写了 containsValue(),HashMap 是双循环去遍历,而因为kv都通过双链表关联起来了,LinkedHashMap 可以通过直接通过head遍历链表;containsKey() 还是用的 HashMap 的,直接通过hashcode来遍历。

  3. HashMap 和 LinkedHashMap 的操作时间复杂度在O(1),TreeMap 是红黑树,时间复杂度在O(logn)

HashSet 、TreeSet 和 LinkedHashSet 的区别:

  1. HashSet 无序,基于HashMap来实现;LinkedHashSet 有序, 基于 LinkedHashMap 来实现的。

  2. TreeSet 是 SortedSet 接口的唯一实现类(NavigableSet 接口继承 SortedSet 接口,TreeSet 实现 NavigableSet 接口),通过comparator方法来实现有序。在底层实现上,是通过Treemap的结构,也就是红黑树来实现存储。TreeSet 不允许存放null值。放入的对象必须实现HashCode()方法,放入的对象,是以hashcode作为标识的,而具有相同内容的string对象,hashcode是一样的。

  3. Set 集合要求存储的元素比较重写hashcode和equals方法。比如HashSet的存放时,会先用hashcode来找位置,然后用equals来判断是否重复。

    == 判断的是内存中的地址,equals 比较内存中存放的值是否相等

CopyOnWriteArrayList:在JUC包下面,基于写时复制,并发的读,当需要修改的(add、set、remove)时候,加锁,先复制整个容器,修改元素,将原容器的引用指向新的容器。适合读多写少。

copyonwrite的思想应该和redis在持久化实现的过程是一致的,即并发读,当需要修改的时候,只把需要修改的那部分复制出来进行修改,然后把引用的指针指向新的内容。这个类在实现copyonwrite的时候是复制整个容器。

CopyOnWriteArraySet:底层用的也是 CopyOnWriteArrayList 类,在add、remove修改的时候需要加锁,复制整个容器

Queue 接口

引发异常 返回特殊值
插入 add(如果不能插入抛出异常) offer(不超过容量的情况下插入,如果无法插入的话返回false)。在容量受限的队列中,offer要优于add
移除头部元素 remove(如果队列为空,返回NoSuchElementException异常) poll(如果队列为空,返回false)
返回头部数据 element(如果队列为空,返回NoSuchElementException异常) peek(如果队列为空,返回null)

Deque:继承Queue接口,双端队列,允许头部或尾部进行操作

LinkedList 也实现了这个接口方法,add 是直接在链表的尾部加入,还可以从头部加入,addfirst或者offerfirst

ArrayDeque:实现Deque接口,是一个双向数组,或者叫循环数组。普通数组下标0是第一个元素,下标最大的是最后一个元素,在最后进行操作还可以,但是如果在头部进行操作,就需要移动所有的数据下标,而ArrayDeque通过循环数组就解决了这个问题。具体是通过移动head和tail的下标来实现。计算个数的时候通过位运算获得return (tail - head) & (elements.length - 1);每当add数据的时候,都会判断head是否等于tail,如果是的话就扩容,所以head=tail只会出现在初始化的时候。

BlockingQueue 同步队列中的堵塞队列

BlockingQueue:阻塞队列

注意事项:

  1. 不接受null元素
  2. 可以限定容量
  3. 主要用于实现 生产者消费者 队列,但它另外还支持 Collection 接口
  4. 线程安全

公平锁和非公平锁:公平锁即一个线程组里能保证每个线程都拿到锁,那么这个锁就是公平锁。如果保证不了每个线程都拿到锁,即存在有线程饿死,那么这个锁就是非公平锁。ReentrantLock 默认是非公平锁。
比如:t1获取锁,t2等待,t1释放,t2正准备获取t3插队,t3获取锁,t2继续等待。这就是非公平锁,可能会因为线程抢占问题而导致某个线程一直在等待。如果是公平锁的话,t3会发现等待队列有人,就不会随意抢占了。

ArrayBlockingQueue:基于数组,有界,初始化时限定容量,不会扩容。生产和消费公用一个锁,即两者不会真正并行。可以设置锁是否公平。
PriorityBlockingQueue:基于优先级(优先级的判断通过构造函数传入的Compator对象来决定),无界阻塞队列。生产和消费公用一个锁,即两者不会真正并行。不会阻塞生产者,统一使用的是offer(),由于队列是无界的,会一直放进去数据;但是消费的时候如果队列为空,会一直等待。如果生产者速度远大于消费者,某一刻会导致OOM。
LinkedBlockingQueue:基于链表,如果不指定容量,默认是Integer.MAX_VALUE,相当于无界,为了避免内存问题一般指定容量。内部有takeLock和putLock,保证了生产和消费可以并行执行,并且都是非公平锁。LinkedBlockingQueue

// Removes a node from head of queue.
private E dequeue() {
  // assert takeLock.isHeldByCurrentThread();
  // assert head.item == null;
  Node<E> h = head;
  Node<E> first = h.next;
  h.next = h; // help GC
  head = first;
  E x = first.item;
  first.item = null;
	return x;
}

TransferQueue & LinkedTransferQueue:无界。transfer() 方法将元素给消费者,如果没有消费者就会堵塞生产者;tryTransfer() 将元素立刻给一个消费者,如果没有消费者就返回false,不会将元素放入队列,还可以指定等待时间。LinkedTransferQueue
SynchronousQueue:容量为空,必须放进去一个值才能拿,也必须拿走了才能继续生产。和LinkedTransferQueue的区别就是SynchronousQueue没有容量
DelayQueue:无界。元素只有当指定的延迟时间到了,才能从里面获取到元素。即不会堵塞生产者,但是会堵塞消费者。使用场景比如用来管理一个超时未响应的连接队列。
ConcurrentLinkedQueue:通过CAS来实现

posted @ 2020-12-19 11:22  猫熊小才天  阅读(113)  评论(0编辑  收藏  举报