• 数据结构汇总


    1、ConcurrentHashMap原理和技术,size方法的实现?

      java1.7中采用Segment +HashEntry +ReentrantLock实现,是用的分段锁

      Java1.8中采用Node + CAS+Synchronized来保证并发安全实现,降低锁的粒度,采用链表

    线程安全

    Concurrenthashmap是hashmap的实现

    Concurrenthashmap结构:主干是一个segment数组,segment(继承ReentrantLock)里面是一个hashEntry数组,segment跟hashmap差不多

    Concurrenthashmap的put方法:(1)定位segment并确保segment已初始化,(2)调用segment的put方法

    concurrentHashmap的get方法:

    get时未加锁,因为用了volatile对Map中的Node修饰。  这也是比HashTable、HashMap安全效率高的原因

      volatile修饰数组主要是保证在数组扩容时保证可见性

    对于一个key,需要经过三次(为什么要hash三次下文会详细讲解)hash操作,才能最终定位这个元素的位置,这三次hash分别为:

    1. 对于一个key,先进行一次hash操作,得到hash值h1,也即h1 = hash1(key);
    2. 将得到的h1的高几位进行第二次hash,得到hash值h2,也即h2 = hash2(h1高几位),通过h2能够确定该元素的放在哪个Segment;
    3. 将得到的h1进行第三次hash,得到hash值h3,也即h3 = hash3(h1),通过h3能够确定该元素放置在哪个HashEntry

    concurrentHashmap的size方法:size方法需要遍历所有的segment才能算出整个map的大小,而在遍历的过程中,之前遍历的segment可能会改变,所以最后size出来的可能并不是map真正的大小

    2、HashMap  :   数组 + 链表

      (1)线程不安全的原因是,在扩容的时候,会将原数组内容重新hash到新的扩容数组,多线程时,同时put就会出问题,get会出现死循环。

      (2)HashMap的优势是查询和操作的时间复杂度都是O(1)

      (3)在新建HashMap的时候,大部分时候不会给初始化容量,这在后续Map中元素越来越多的时候,会有一个扩容问题。阿里巴巴Java开发手册建议新建HashMap时要设置初始化容量。

      这个初始化容量的值为: 你需要的大小 / 0.75+1,这个逻辑在Maps的方法newHashMapWithExpectedSize()中已经实现,可以直接用:

      Map<String,String> map = Maps.newHashMapWithExpectedSize(7);

    HashMap的原理?(数组+单向链表、put、get、size方法)

    非线程安全:(1)hash冲突:多线程某一时刻同时操作hashmap并执行put操作时,可能会产两个key的hash值相同,两线程在插入时,肯定会有一个丢失值(put方法不是同步的,put内部调用的addEntry方法也是不同步的)

    (2)hashmap的扩容方法resize(不是同步的):当hashmap大小不够,两个线程同时进行扩容时,最后一个线程生成的新数组赋予了resize方法中的table,其他线程的均丢失。

    HashMap采用链地址法,即数组+单向链表。主干是数组,长度是2的次幂,链表是为了解决哈希冲突(对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了)而存在的。

    HashMap的主干是一个Entry数组。Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对

    static class Entry<K,V> implements Map.Entry<K,V> {
            final K key;
            V value;
            Entry<K,V> next;//存储指向下一个Entry的引用,单链表结构
            int hash;//对key的hashcode值进行hash运算后得到的值,存储在Entry,避免重复计算
         +构造方法
    }

    如果定位到的数组位置不含链表(当前entry的next指向null),那么对于查找,添加等操作很快,仅需一次寻址即可;

    如果定位到的数组包含链表,对于添加操作,其时间复杂度依然为O(1),先根据key的hashCode重新计算hash值,再根据hash值得到元素在数组中的位置,若数组该位置上已存放其他元素,则该位置上的元素以链表形式存放,新加入的元素放在链头Entry中存放的next指向原先的元素,因为最新的Entry会插入链表头部,即需要简单改变引用链即可,

    而对于查找操作来讲,此时就需要通过计算key的hashCode,找到数组中对应位置的元素,然后通过key对象的equals方法逐一比对查找,遍历链表。

    所以,性能考虑,HashMap中的链表出现越少,性能才会越好。

    注:hashMap数组扩容,当数组中元素个数超过数组大小(默认16)*负载因子(默认0.75)时,数组大小要扩大一倍,在重新计算元素在数组中的位置(非常消耗性能,预设元素个数能够提高HashMap的性能)

    高效遍历HashMap的方式

    Map m = new HashMap();

    Iterator it = map.entrySet().iterator();

    while(it.hasNext()){

      Map.Entry en = (Map.Entry) it.next();

      Object key = en.getKey();

      Object value = en.getValue();

    }

    3、

    HashMap和双向链表合二为一即是LinkedHashMap,它是一个将所有Entry节点链入一个双向链表的HashMap,LinkedHashMap是HashMap的子类

    LinkedHashMap的Entry结构,在hashmap的基础上,增加了before和after:

    4、hashTable

    线程安全使用synchronized来锁住整张hash表来实现线程安全

    通过“拉链法”实现的哈希表,成员变量table是一个Entry[ ] 数组类型,而 Entry(在 HashMap 中有讲解过)实际上就是一个单向链表。哈希表的"key-value键值对"都是存储在Entry数组中的

    hashTable与hashMap的结构是类似的,只不过它俩继承、实现的类不同。

    hashMap与hashTable的比较:线程安全性、同步、速度

    (1)hashMap的key和value都允许为null(遇到key为空的时候调用putForNullKey方法进行处理,存储在table数组的第一个节点上),而hashTable的key和value都不允许为null(遇到null直接返回nullPointerException)

    (2)hashTable是线程安全的,方法是同步的,几乎所有的public方法都是synchronized的。hashMap是非线程安全的。涉及到多线程同步就用hashTable,反之用hashMap

    (3)hashTable基于Dictionary类,hashmap继承了AbstractMap(它们都实现了Map接口)

    (4)hashtable在单线程环境下比hashmap慢

    (5)hashMap扩容是当前容量翻倍,而hashTable是当前容量翻倍+1

    (6)计算hash的方法不同:hashTable是直接使用key的hashCode对table数组的长度直接进行取模;hashMap对key的hashCode进行了二次hash,已获得更好的散列值,然后对table数组长度取模;

    5、Arraylist和linkedlist原理结构,以及linkedhashmap底层结构?

     数组在内存中都是以一段连续的地址空间来存储元素的,由于它直接操作内存,所以性能要比集合类更好一些

    ArrayList基于数组实现,所有方法都没有进行同步,是线程不安全的,查找修改快(根据索引直接查询或修改,不涉及数组复制)而插入删除慢(插入、删除都涉及到数组元素的移位、以及复制)。源码中有成员变量final int default_CAPACITY初始容量,一个空对象数组Object[] enpty_elementdata = {},一个对象数组Object[] elementdata,一个集合元素个数size

    增加add():单纯的添加元素,直接插入数组末尾,快速

    增加add(index,value):指定位置的添加,需要将index后的元素后移并复制数组,操作慢

    删除remove(index):需要将删除位置后的元素前移,也会涉及到数组复制,操作慢

    修改set(index,value):直接对指定位置的元素进行修改,不涉及元素移动和数组复制,操作快

    查询get(index):直接返回指定下标的数组元素,操作快

    arraylist的扩容,就是增加原来数组长度的一半(实际上是新建一个容量更大的数组,将原先数组的元素全部复制到新的数组上)

    linkedlist底层是基于双向链表实现的,不管是增删改查方法还是队列和栈的实现,都是可以通过操作结点实现的,所有方法没有进行同步,是线程不安全的,插入删除快(时间复杂度O(1))而查找修改慢(要遍历链表进行元素定位,时间复杂度O(n/2))

    增加add(e):在链表尾部添加

    增加add(index,e):index等于集合元素个数时,在链表尾部添加;否则在链表中部插入

    删除remove(index):将要删除的元素的上一个结点的后继结点引用指向要删除的结点的下一个结点的前继结点引用

    删除remove(object):同上,删除元素后集合占用的内存自动缩小

    修改set(index,element):获取指定下标结点的引用,获取指定下标结点的值,将结点元素设置为新的值

    查找get(index):先检查下标是否合法,再返回指定下标的结点的值

    6、Arraylist的elementdata属性为什么用了transient修饰后,依然可以序列化,这样的好处?

    transient是类型修饰符,只能用来修饰字段。在对象序列化的过程中,标记为transient的变量不会被序列化

    好处:Arraylist的两个方法writeObject和readObject,

    ArrayList在序列化的时候会调用writeObject,直接将size和element写入ObjectOutputStream;反序列化时调用readObject,从ObjectInputStream获取size和element,再恢复到elementData。
           为什么不直接用elementData来序列化,而采用上诉的方式来实现序列化呢?原因在于elementData是一个缓存数组,它通常会预留一些容量,等容量不足时再扩充容量,那么有些空间可能就没有实际存储元素,采用上诉的方式来实现序列化时,就可以保证只序列化实际存储的那些元素,而不是整个数组,从而节省空间和时间。

  • 相关阅读:
    Java中的Set List HashSet互转
    Java数组如何转为List集合
    Map
    Jave中的日期
    mybatis plus 条件构造器queryWrapper学习
    Error running 'JeecgSystemApplication': Command line is too long. Shorten command line for JeecgSystemApplication or also for Spring Boot default configuration.
    RBAC权限系统分析、设计与实现
    html拼接时onclick事件传递json对象
    PostgreSQL 大小写问题 一键修改表名、字段名为小写 阅读模式
    openssl创建的自签名证书,使用自签发证书--指定使用多域名、泛域名及直接使用IP地址
  • 原文地址:https://www.cnblogs.com/blackdd/p/12089201.html
Copyright © 2020-2023  润新知