文章目录

  • HashMap
    • 1.8结构
    • HashMap初始化
    • HashMap的存储
  • ConcurrentHashMap
    • JDK1.7ConcurrentHashMap操作原理
    • JDK1.8ConcurrentHashMap操作原理
      • node元素初始化
      • node位置冲突时的解决办法
      • JDK1.8ConcurrentHashMap使用锁总结
  • ConcurrentSkipListMap跳表
    • 索引的创建机制
    • ConcurrentSkipListMap跳表的跳跃查找机制
  • List
    • ArrayList
    • CopyOnWriteArrayList
  • Set
  • Queue
    • 非阻塞队列实现原理
    • 阻塞式队列实现原理
    • schedule延时队列

HashMap

数据结构:数组{链表,链表,链表}
JDK1.8之后对于HashMap进行了优化

1.8结构

初始时:数组{链表,链表,链表}
当链表增长到8个元素时链表转化为红黑树:数组{链表(–>红黑树)链表(–>红黑树)}

HashMap初始化

hashMap初始化的时候会构建存储大小的阈值和扩容临界值,当hashmap大小接近阈值时,就会进行扩容,hashmap的大小是2的幂数,所以扩容也是按照当前大小×2进行扩容。如new HashMap大小是519时,那么分配给他的内存就是1024

HashMap的存储


当一个<k,v>键值对需要存储的时候,先通过hash计算取模(),根据取模结果存入数组的相应位置。

冲突:当有两个<k,v>取模结果相同时,就要存入相同的数组位置,而数组的这个位置上已经有一个<k,v>。

当数组位置冲突时会转化成一个链表,第二个<k,v>就会存在第一个<k,v>后。当链表达到一定长度时,数组就会进行扩容(链表的数据结构特性是易存不易查,长度越长查询越慢)。
红黑树实现不做描述了,java实现红黑树很繁琐这里不贴代码了。红黑树的转换原理是当单个链表达到8那么这个链表就转化成红黑树,而不会转化数组中其他链表。

ConcurrentHashMap

JDK1.7底层:数组{数组segment{entry,entry},数组segment{entry},…}
JDK1.8底层:数组{链表}

JDK1.7ConcurrentHashMap操作原理

在1.7时ConcurrentHashMap不会扩容,初始化时就已经确定了segement的数量,多线程并行操作ConcurrentHashMap时每个线程操作一个segment数组,当线程操作segment时会lock这个segment,所以ConcurrentHashMap是安全的。segment的作用相当于一个加锁(分段锁模式)的hashMap,这个hashMap不会又转化红黑树的操作,segment是可以扩容的。

JDK1.8ConcurrentHashMap操作原理

JDK1.8弃用了segment,使用node<k,v>来实现数组内部元素。

node元素初始化

node初始化的时候,会有一个位置信息的控制权(SIZECTL),当有线程的hash值=node元素位置,而这个数组的node节点刚好为空,那么需要所有要操作这个node元素的线程争抢SIZECTL,只有一个线程可以抢到,所以只有一个线程可以操作node,这个过程使用了CAS自旋操作,但是并不会消耗大量的性能,因为没有抢到SIZECTL的线程会使用Thread.yield()方法来释放资源,当线程1初始化node完成之后,其他线程则不会直接break跳出初始化。

node位置冲突时的解决办法

当两个key的hash值都对应一个node时,node只允许一个key进行put操作,此时会使用synchronized的锁住这个node链表的head节点,另一个key的线程则等待(链表是key1–next–>key2–next–>key3所以锁住head之后相当于锁住整个链表,因为无法从头遍历)。当链表达到一定数量之后还是会转化成红黑树,每次put元素后都会比对阈值,当达到阈值时就会执行扩容。扩容时会使用CAS机制来保证线程的安全性。

JDK1.8ConcurrentHashMap使用锁总结

当put时,如果key1、key2不发生冲突,则使用CAS去初始化node。如果key1、key2冲突,则使用synchronized同步代码块来锁定node的head节点。

ConcurrentSkipListMap跳表

ConcurrentSkipListMap的实现原理:有序链表;无锁;value不能为空;层级越高跳跃性越大,数据越少,查询理论变快。
查找数据时,按照从上到下,从左到右顺序查找,是空间换时间,类似数据库索引的概念,在一些开元组件中有使用(level DB、redis)
插入数据的底层实现原理:首先比对key(这个key不是Map里的key,而是节点)值,对于key进行排序,然后将key插入到指定位置如下图

每个node都有机会升级为索引,升级原则为随机生成。
新的node是否抽取出来作为index,随机决定;index对应的level也有随机数决定。每层元素headindex固定为所有node中最小的;
索引内部属性如下图node(当前节点)、right(右侧节点)、down(下层节点)

整个ConcurrentSkipListMap只有一个入口就是链表的head index入口
链表实现map

public class SkipMapDemo {Node head = null; // 队列的头部public void add(Node node) {if (head == null) {head = node;return;}// 追加到队列尾部Node temp = head;while (temp.next != null) { // 找到一个next为空的节点,这个节点就是最后一个temp = temp.next;}temp.next = node;}public void print() { // 打印链表内容Node temp = head;while (temp.next != null) {System.out.println(temp.key);temp = temp.next;}System.out.println(temp.key);}public static void main(String[] args) throws IOException {SkipMapDemo SkipMapDemo = new SkipMapDemo();Node node1 = new Node();node1.key = "1";SkipMapDemo.add(node1);Node node2 = new Node();node2.key = "4";SkipMapDemo.add(node2);Node node3 = new Node();node3.key = "3";SkipMapDemo.add(node3);SkipMapDemo.print();}
}class Node {public String key;public String value;public Node next;
}

索引的创建机制

当(headindex)索引A的下级左侧节点在插入值之后随即生成了与索引A同级的索引B时,此时索引B就会成为当前层级的headindex,索引A会连接在索引B的右侧;当(headindex)索引A的右侧对应node执行插入操作时,随即触发了索引升级,从而生成了索引B,此时索引B将成为headindex,索引A会连接在索引B的下级。

ConcurrentSkipListMap跳表的跳跃查找机制


想要获取Map中的值,先从headindex入口进入Map,然后从索引key-1开始比对key,如果大于key-1则向右侧同级索引查找,如果还是大于key-5,则继续向右查找,如果最右侧索引依旧小于key值,则在最右侧索引向下查找下层索引,下层索引也找到最右侧之后,再向下查找更下层索引,以此类推,直到最下级索引的最右侧,如果仍然小于key值则从此索引对应的node开始向右侧查询key值,知道找到对应的key。若找到的索引大于key值,则从前一个索引对应的node值进行查找key。

List

List可重复
CopyOnWriteArrayList即写时复制的容器,和ArrayList比较,有点是并发安全,缺点是:
1.多了一倍内存占用:写数据是copy一份完整数据,单独进行操作,占用内存是双倍。
2.数据一致性:数据写完之后,其他线程不一定是马上读取到最新内容。

ArrayList

存储的时候没有CAS也没锁,所以是线程不安全的。底层是数组实现,存数据的时候会判断扩容,扩容的操作是先新建一个数组,然后把新数组在赋值回来。

CopyOnWriteArrayList

存储时使用ReentrantLock可重入锁,所以是线程安全的。
CopyOnWriteArrayList可以一边遍历一边删除,而ArrayList不能这样操作。

Set

Set内部不重复,底层是hashMap,存储的时候把元素作为hashMap的key。当Set中存储一个对象时,此时Set会将这个Object求hash值,然后存储到hashMap的key里,而如果在取值的时候使用的是这个对象的一个新的引用(new一个新对象),那么此时的get方法会返回空,因为不同的引用hash值不同,所以取不到之前Object的key值。
代码实例

public static void main(String[] args) {HashMap<User, String> map = new HashMap<>();User user = new User("1");map.put(user, "test");System.out.println(map.get(user)); // 1、 输出什么  testuser = new User("1");System.out.println(map.get(user));  // 2、 输出什么 null}

这段代码中第一个输出test,第二个输出null,原因就是因为两个new的对象hash值不同,所以当第map.put之后用第二个user当key时会取不到值,如果想要取到相同的值,需要重写Object的equels方法,代码如下

class User {public String name;public User(String name) {this.name = name;}@Overridepublic boolean equals(Object obj) {return name.equals(((User)obj).name);}@Overridepublic int hashCode() {return name.hashCode();}

这样就会两次都输出test了

Queue

非阻塞队列实现原理

底层是数组,当操作这个队列赋值时(offer)会上一把可重入锁ReentrantLock,只有一个线程可以操作队列,每条插入数据都对应数组的下标顺序存入,当队列满了之后,后续的线程会被舍弃。
JDK的offer源码

public boolean offer(E e) {checkNotNull(e);final ReentrantLock lock = this.lock;lock.lock();try {if (count == items.length)return false;else {enqueue(e);return true;}} finally {lock.unlock();}}

阻塞式队列实现原理

底层是数组,当操作这个队列赋值时(put)会上一把可重入锁ReentrantLock,只有一个线程可以操作队列,每条插入数据都对应数组的下标顺序存入,当队列满了之后,调用condition.await方法,让后续线程等待。
JDK的ArrayBlockingQueue源码

public void put(E e) throws InterruptedException {checkNotNull(e);final ReentrantLock lock = this.lock;lock.lockInterruptibly();try {while (count == items.length)notFull.await();enqueue(e);} finally {lock.unlock();}}

当队列取数据时,一样是先加Lock锁,然后直接从数组中取出数据,数组length-1,重新排列数组,之后需要notFULL.signal(数组不满的条件下通知外部等待线程)来通知之前执行await等待的线程。

schedule延时队列

schedule是基于PriorityQueue(优先级队列)来实现的,优先级的实现就是重写PriorityQueue的compare比较方法。在动态插入数据时会触发排序,它的底层数组是一个有序数组。这是排序的实现。
延时是基于DelayQueue来实现的,实现了Delayed接口,重写getDelayed(延时时间方法)、compareTo(时间比对方式)。

// (基于PriorityQueue来实现的)是一个存放Delayed 元素的无界阻塞队列,
// 只有在延迟期满时才能从中提取元素。该队列的头部是延迟期满后保存时间最长的 Delayed 元素。
// 如果延迟都还没有期满,则队列没有头部,并且poll将返回null。
// 当一个元素的 getDelay(TimeUnit.NANOSECONDS) 方法返回一个小于或等于零的值时,
// 则出现期满,poll就以移除这个元素了。此队列不允许使用 null 元素。
public class DelayQueueDemo {public static void main(String[] args) throws InterruptedException {DelayQueue<Message> delayQueue = new DelayQueue<Message>();// 这条消息5秒后发送Message message = new Message("message - 00001", new Date(System.currentTimeMillis() + 5000L));delayQueue.add(message);while (true) {System.out.println(delayQueue.poll());Thread.sleep(1000L);}// 线程池中的定时调度就是这样实现的}
}// 实现Delayed接口的元素才能存到DelayQueue
class Message implements Delayed {// 判断当前这个元素,是不是已经到了需要被拿出来的时间@Overridepublic long getDelay(TimeUnit unit) {// 默认纳秒long duration = sendTime.getTime() - System.currentTimeMillis();return TimeUnit.NANOSECONDS.convert(duration, TimeUnit.MILLISECONDS);}@Overridepublic int compareTo(Delayed o) {return o.getDelay(TimeUnit.NANOSECONDS) > this.getDelay(TimeUnit.NANOSECONDS) ? 1 : -1;}String content;Date sendTime;/*** @param content  消息内容* @param sendTime 定时发送*/public Message(String content, Date sendTime) {this.content = content;this.sendTime = sendTime;}@Overridepublic String toString() {return "Message{" +"content='" + content + '\'' +", sendTime=" + sendTime +'}';}
}

JDK的存放源码

public boolean add(E e) {return offer(e);}public boolean offer(E e) {final ReentrantLock lock = this.lock;lock.lock();try {q.offer(e);if (q.peek() == e) {leader = null;available.signal();}return true;} finally {lock.unlock();}}

JDK取值源码

public E poll() {final ReentrantLock lock = this.lock;lock.lock();try {E first = q.peek();//看第一个节点firstif (first == null || first.getDelay(NANOSECONDS) > 0)//比对当前是否到时间return null;elsereturn q.poll();} finally {lock.unlock();}}

并发容器类-Conconcurrent容器原理相关推荐

  1. java并发编程——并发容器类介绍

    2019独角兽企业重金招聘Python工程师标准>>> 并发容器的简单介绍 JDK5中添加了新的concurrent包,相对同步容器而言,并发容器通过一些机制改进了并发性能.因为同步 ...

  2. grpc通信原理_容器原理架构详解(全)

    目录 1 容器原理架构 1.1 容器与虚拟化 1.2 容器应用架构 1.3 容器引擎架构 1.4 Namespace与Cgroups 1.5 容器镜像原理 2 K8S原理架构 2.1 K8S主要功能 ...

  3. 走进Java中的持有对象(容器类)之一 容器分类

    转载自 https://www.cnblogs.com/ACFLOOD/p/5555555.html Java容器可以说是增强程序员编程能力的基本工具,本系列将带您深入理解容器类. 容器的用途 如果对 ...

  4. Day127.JUC:线程间通信(Conditon)、并发容器类(CopyOnWrite)、JUC强大辅助类、Callable

    . 目录 一.线程间通信 线程间通信改造成Lock版  Condition 定制化调用通信 Condition 二.并发容器类 (解决集合安全问题) CopyOnWrite 写时拷贝技术 三.JUC ...

  5. java容器类_走进Java中的持有对象(容器类)之一 容器分类

    Java容器可以说是增强程序员编程能力的基本工具,本系列将带您深入理解容器类. 容器的用途 如果对象的数量与生命周期都是固定的,自然我们也就不需要很复杂的数据结构. 我们可以通过创建引用来持有对象,如 ...

  6. Docker容器原理及相关知识

    Docker容器原理及相关知识 一.Docker容器介绍 1.容器概念 2.Docker介绍 3.Dcker的特点 二.Docker的体系架构 三.相关术语介绍 1.Docker 客户端 2.Dock ...

  7. 基于容器原理(docker、lxc、cells)的Android 双系统设计概要

    写在前面 最近一两年预研加开发android双系统:中途用过不少开源代码或者研读过大牛BLOG,现开放双系统设计原理来回报社区. 备注:我是在android6.0上实现的. 这个项目的原型来自于,哥伦 ...

  8. 转 Spring源码剖析——核心IOC容器原理

    Spring源码剖析--核心IOC容器原理 2016年08月05日 15:06:16 阅读数:8312 标签: spring 源码 ioc 编程 bean 更多 个人分类: Java https:// ...

  9. 并发编程五:java并发线程池底层原理详解和源码分析

    文章目录 java并发线程池底层原理详解和源码分析 线程和线程池性能对比 Executors创建的三种线程池分析 自定义线程池分析 线程池源码分析 继承关系 ThreadPoolExecutor源码分 ...

最新文章

  1. 单片机生成随机数的方法总结
  2. Laravel 事件侦听的几个方法 [Trait, Model boot(), Observer Class]
  3. Oracle 触发器的使用小结
  4. insert在python中的用法_python中insert用法是什么_后端开发
  5. android r 编译找不到头文件_kOS(1):编译
  6. centos 对已有卷扩容_centos7 逻辑卷扩容
  7. c语言全局变量和局部变量问题汇总
  8. python去重复功能_消除Python列表重复的几种方法,python,去,一些
  9. MySQL基础部分总结
  10. 移植mysql到嵌入式ARM平台
  11. STM32----TIM6和TIM7
  12. 通过特性动态获取属性及值
  13. ARM Cortex-M3与Cortex-M4中断相关寄存器
  14. Hive常见的存储格式的区别与应用场景
  15. linux怎么开启httpd服务公钥,在Apache httpd服务器上部署SSL证书
  16. 为什么建议要延迟macOS升级,小编为你全面分析!
  17. 【mybatisPlus】mybatis基本使用
  18. sublime text3找到定义_决策易aPaaS,一款非技术人员也能使用的自定义开发神器
  19. 知识图谱05:知识图谱构建涉及的技术
  20. 计算机一级中的高级筛选怎么做,详解Excel的高级筛选

热门文章

  1. c语言程序设计 第三版 哈工大,C语言程序设计_哈工大(3):字符串指针.pdf
  2. 服务器健康管理芯片设计,IC设计大厂看中健康领域,联发科发布首款六合一智能健康芯片-控制器/处理器-与非网...
  3. 重视对新能源问题的研究,将从新能源电气火灾的角度做进一步分析
  4. 虚拟机openGauss链接Navicat遇到SSL error:tlsv1 alert protocol version FATAL:Failed to Generate the fake salt
  5. 最新升级pip命令,查看pip版本命令,pycharm升级pip命令,推荐收藏关注不迷路
  6. windows xp 驱动开发(五) USB驱动程序、应用软件概述
  7. (sudo命令)linux中给普通用户添加root权限
  8. 星淘惠:文创产品拓展海外市场形成国际影响力
  9. pandas to_sql填坑
  10. pycharm英文设置成中文