Java容器相关问题

91 阅读6分钟

同步类容器的问题

  • 同步类容器都是线程安全的,但是在某些场景下可能需要加锁来保护复合操作
  • 复合类操作:迭代(反复访问元素,遍历容器中里面的所有的元素)、跳转(根据指定的顺序找到当前的元素的下一个元素)、以及条件运算
  • 这些复合操作在多线程并发地修改容器时,可能会表现出意外的行为,最经典的便是ConcurrentModificationException,原因是当容器并发的修改了内容

同步类容器的使用

  • 同步类容器

  • 如Vector、HashTable

  • 容器的同步功能其实都是JDK的Collections、synchronized等工厂的方法去实现的

  • 其底层的机制无非就使用synchronized关键字对每个公用的方法进行同步,或者使用Object mutex对象锁的机制使得每次只能有一个线程访问容器的状态

  • List<String> list = new ArrayList<>();
    Collections.synchronizedCollection(list);
    

     

并发类容器的概念

  • jdk提供了多种并发类容器来替代同步类容器从而改善性能
  • 同步类容器的状态是串行化的
  • 他们虽然实现了线程安全,但是严重降低了并发性,在多线程环境时,严重降低了应用程序的吞吐量

ConcurrentMap

  • ConcurrentMap接口有两个最重要的实现 ConcurrentHashMap和ConcurrentSkipListMap(支持并发排序的功能)
  • ConcurrentSkipListMap内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的HashTable,每个段都有自己的锁
  • 只要多个修改操作发生在不同的段上,他们就可以并发进行。把一个整体分成16个段,也就意味着最高支持16个线程的并发修改操作
  • 这也是在多线程场景时减小锁的粒度从而降低锁竞争的一种方案。并且代码中大多共享变量使用volatile关键字声明,目的是第一时间获取修改内容,性能非常好

Copy-On-Write容器

  • Copy-On-Write容器简称COW,是一种程序设计的优化策略
  • JDK容器里面COW容器有两种,CopyOnWriteArrayList
  • Copy-On-Write容器即写时复制的容器
  • 通俗的理解就是当我们往一个容器添加元素的时候,不直接往当前的容器添加,而是对当前的容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器
  • 这样的好处是我们可以对Copy-On-Write容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素
  • Copy-On-Write容器依据读写分离的思想,读和写基于不同的容器

并发Queue

  • 在并发队列上JDK的实现方式有两种(他们都继承于Queue接口)
  • ConcurrentLinkedQueue为代表的高性能队列
  • BlockingQueue为代表的阻塞队列

ConcurrentLinkedQueue

  • ConcurrentLinkedQueue:是一个适用于高并发场景下的队列,通过无锁的方式,实现了高并发状态下的高性能,通常ConcurrentLinkedQueue一般性能好于BlockQueue。ConcurrentLinkedQueue是一个基于链接节点的无界线程安全队列
  • ConcurrentLinkedQueue元素遵循先进先出的原则,头是最先加入的,尾是最后加入的,该队列不允许null元素
  • add()和offer()都是加入元素的方法(这两个方法没有任何区别)
  • poll()和peek()都是取头元素的节点,区别在于前者会删除元素,后者不会

BlockingQueue接口的重要方法

  • offer(anObject):表示如果可能的话,将按Object加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回True,否则返回False(本方法不阻塞当前执行方法的线程)
  • offer(E o,long timeout,TimeUnit unit):可以设定等待的时间,如果在指定的时间内,还不能往队列中加入BlockingQueue,则返回失败
  • put(anObject):把anObject加入到BlockingQueue里面,如果BlockingQueue没有空间,则调用此方法的线程被阻断,直到BlockingQueue里面有空间再继续
  • poll(long timeout,TimeUnit unit):从BlockingQueue取出一个队首的对象,如果在指定的时间内,队列一旦有数据可以取,则立即返回队列中的数据。否则直到时间超时还没有数据可以取出的话,返回失败
  • take():取出BlockingQueue里面排在首位的对象,如果BlockingQueue为空,阻断进入等待状态直到BlockingQueue有新的数据被加入
  • drainTo():一次性从BlockingQueue获取所有可用的数据对象(还可以指定获取数据的个数),通过该方法,可以提升获取数据的效率;不需要多次分批加锁或者释放锁

阻塞队列的模拟

  • 拥有固定长度承装元素的容器
  • 计数器统计容器容量的大小
  • 当队列里面没有元素的时候执行线程-》需要等待
  • 当队列元素已满的时候执行线程也需要等待

ArrayBlockingQueue

  • ArrayBlockingQueue:基于数组的阻塞队列的实现
  • 在ArrayBlockingQueue的内部,维护了一个定长的数组,用于缓存队列中的数据对象,其内部没有读写分离,也就意味着生产和消费不能完全并行,长度是需要定义的,可以指定先进先出或者先进后出,也叫有界队列,在很多时候非常适合使用

LinkedBlockingQueue

  • LinkedBlockingQueue:基于链表的阻塞队列

  • 和ArrayBlockingQueue类似,其内部维持着一个数据的缓冲队列(该队列由一个链表构成),

    LinkedBlockingQueue之所以能高效处理并发数据,是因为其内部实现采用分离锁(读写分离两个锁),从而实现生产者和消费者的完全并行运行。它是一个无界队列

SynchronousQueue

  • SynchronousQueue:一种没有缓冲的队列
  • 生产者产生的数据直接会被消费者获取并且消费

PriorityBlockingQueue

  • PriorityBlockingQueue:基于优先级的阻塞队列
  • 优先级的判断通过构造函数传入的Compator对象来决定,也就是说传入队列的对象必须实现Comparable接口,在实现PriorityBlockingQueue时,内部控制线程同步的锁采用得是公平锁,他也是一个无界的队列

DelayQueue

  • DelayQueue:带有延迟时间的Queue

  • 其中的元素只有当其指定的延迟时间到了,才能从队列中获取该元素。DelayQueue中的元素必须实现Delayed接口,

    DelayQueue是一个没有大小限制的队列,应用场景很多,比如对于缓存超时的数据进行移除、任务超时处理、空闲链接的关闭等等