赞
踩
Vector类的所有方法都是Synchronized同步的,线程安全的,但是单线程访问Vector的话就要在同步操作上耗费大量时间
ArrayList不是同步的,所以不需要保证线程安全时建议使用ArrayList。
如果是无参构造,就初始化一个长度为0的空数组,调用add方法时,先size+1,然后将元素加入数组的size位置处,如果添加时数组为空,就初始化长度为10的数组。如果需要的长度大于原来的长度时就需要扩容,扩容每次变为原来容量的1.5倍,把老数组copy到新数组中,再进行添加元素操作。
补充:为什么HashTable和ConcurrentHashMap不可以是null?
答:Map不是线程安全的,一般用于单线程环境,而Table用于多线程环境。
map.get(key)返回null,无法判断key对应的值是null,还是根本不存在key。单线程可以用map.contains(key)来判断,而多线程解决这个问题很复杂,必须保证contains与get操作的原子性。因此线程安全的map不允许value为null(也是这个原因,HashTable没有contains方法,错了,HashTable有containsValue和containsKey方法)
为什么key不能是null呢?
答:null不是对象,无法调用equals和hashCode方法,因此无法计算哈希值用以做键。但HashMap对此做了特殊处理。
不指定初始值:HashTable默认初始大小为11,每次扩充为原来的2*n+1;HashMap默认初始大小为16,每次扩充容量变为原来的2倍
创建时指定初始值:HashTable会直接使用指定大小,而HashMap会扩充为2的幂次方大小(向下取整)(后面介绍原因)
后者的底层就是基于前者实现的(key就是存入的对象,value是一个Object类型的对象,无意义),HashSet自己实现的方法有clone(), writeObject(), readObject(),其他都是调用HashMap中的方法。
把对象加入Set时,HashSet会先计算对象的hashcode值来进行下标运算(也就是hash&(n-1)),判断对象加入的位置是否为空,如果为空就直接插入,如果不为空,就与已存在对象的hashcode比较同时进行equals方法比较(&&),如果相同就插入失败。否则,再对已存在对象进行节点判断(是树节点还是普通节点),如果是普通节点,就从头遍历并equals比较,若与要插入的对象与某个已存在对象相同,就插入失败,否则在尾部插入新节点。如果是树节点,它的插入方法没有细看。
Hashcode与equals的相关规定:
关于hashCode和equals的重写
为什么需要重写?
答:把自定义对象放入Map中做key时就需要重写这两个方法。
当把自定义对象放入某些集合中时,比如让自定义对象作为Map的key。给定一个对象,Map会调用对象的hashCode方法获得该对象的哈希值,放入hash值所指引的内存位置(这里用指引二字,因为hash值并不完全等同于内存地址)。我们希望内容相同的两个对象拥有相同的哈希值,然而如果不重写hashCode方法,就会使用从Object类继承的hashCode方法,得到的是该对象的内存地址(也就是对应的引用类型变量的值),这样就导致即使内容相同的对象也会有不同的hashCode,这是我们不希望的。
为什么重写equals方法?
即使是不同内容的对象也有可能具有相同的hash值,因此HashMap不能只靠hashCode就取出value。当查询k1对应的value时,首先要通过计算k1的hash值得到HashMap中key的存储地址,然后再进一步比较该地址中放入的k0是否equals(k1),所以即使k0和k1的hashCode相同是不够的。如果不重写自定义对象的equals,那么默认使用的是继承自Object类的equals方法,判断依据还是两个对象的内存地址,即使内容相同的两个对象也是不equals的,因此需要重写,只要两个对象的内容相同就返回true。
1) ==可以用于基本类型和引用类型的比较,进行的是值比较(也就是说当比较引用类型时,实际比较是否指向同一个对象)
2)equals只能比较引用类型,一般情况下也是进行值比较(即比较是否指向同一对象,或者说比较引用类型变量存放的内存地址)
3) 但是File String Date 以及包装类,equals比较的是类型和内容(原因在于这些类重写了equals方法)
4)需要注意的是,使用字面量创建对象时(就是不使用new关键字,比如String a=“a”; int b=2;),这些对象只在常量池中保存一份,所以两个字面量创建的对象只要内容相同就是指向同一对象,但是如果是用new创建对象,new一个就在堆内存中保存一个对象,不管内容是否相同,同时也会在常量池保存一份。(比如 Integer i = new Interger(1); Integer j = new Interger(1); i和j分别指向堆内存中的创建的两个Integer对象,但常量池中也会保存一个1)
1.8之前
数组+链表(链表散列)。HashMap经过key的hashcode经过扰动函数处理得到hash值,然后通过(n-1)&hash判断当前元素的存放位置(n是数组长度)。如果当前位置已经存放元素,就判断该元素与要存入元素的hash值以及key是否相同,相同则覆盖,不同则拉链法解决。(拉链法就是数组加链表,数组每一格都是一个链表,哈希冲突值放到对应位置的链表中)
1.8之后(包括1.8)
链表长度大于8时转化为红黑树以减少搜索时间。TreeMap、TreeSet以及1.8之后的HashMap都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为某些情况下查找树会退化为线性结构。
Hash值的范围是int型的,-2147483648到2147483647,前后加起来大概40亿的映射空间。但一个40亿长度的数组内存是放不下的,(也就是散列值范围很大而数组不可能这么大,因此无法直接拿来使用)所以这个散列值不能直接使用,在使用之前要先对数组的长度做取模运算,得到的余数才能用来代表存放位置,也就是数组下标。这个数组下标的计算方法是(n-1)&hash,n为数组长度,这也就解释了为什么HashMap长度为2的幂次方。。当然,计算下标的(n-1)&hash中,hash并不是key的hashCode()直接拿来使用,而是经过扰动函数(高16位与低16位相与)。
取余(%)操作如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说,如果length是2的n次方,那么hash % length == hash&(length - 1)),采用二进制与运算比取余效率高,这就解释了为什么HashMap的长度为2的幂次方。
主要原因在于并发下rehash会造成元素之间形成一个循环链表,不过1.8之后解决了这个问题,但还是不建议多线程下使用HashMap,因为还是可能会存在数据丢失问题。并发环境建议使用ConcurrentHashMap。
详见:https://coolshell.cn/articles/9606.html
主要区别在实现线程安全的方式上不同
对比图:
HashTable
1.7 的ConcurrentHashMap
1.8 的ConcurrentHashMap(TreeBin 红黑树节点,Node:链表节点)
1.7
将数据分段存储,每一段数据分配一把锁,当一个线程访问其中一个数据段时,其他段的数据可以被其他线程访问。
ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。
一个ConcurrentHashMap包含一个Segment数组。数组中每个Segment的结构和HashMap类似,是数组+链表的结构。一个Segment包含一个HashEntry的数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组中所有元素。对HashEntry数组中的数据修改时要先获得对应的Segment的锁。
1.8
取消了分段锁,采用CAS和synchronized保证并发安全。数据结构跟HashMap1.8类似(数组+链表/红黑树)。1.8中当链表达到长度阈值(默认8)时转为红黑树。
Synchronized只锁定当前链表或红黑树节点的首节点,只要hash不冲突,就不会产生并发,效率提升。
CAS(Compare and Swap)操作包含三个操作数(内存当前值,预期的原值,新值)。如果内存的值和预期的原值是一致,那就转化为新值。CAS是通过不断的循环来获取新值的,如果线程中的值被另一个线程修改了,那么就需要自旋,到下次循环才有可能执行。
Node保存的是key,value,next指针和key的hash值。其中value和next都用volatile修饰,保证并发的可见性。
使用方法:
如果在进行排序时才确定以何种方式对对象排序,那么可以使用第二种sort方法(具有两个参数,第二个参数为new Comparator<待排序对象的类>)(使用如下图)
有点绕:我们可以认为sort方法一直是按升序排序的,只是我们重写了以上两个方法来重新规定两个需要比较的对象之间的“大小”关系。如果返回1,说明第一个对象比第二个对象“大”,如果返回-1,说明第一个对象比第二个对象“小”,二者“大小”相同则返回0.
String类和包装类已经实现了Comparable接口重写了compareTo方法(默认升序),所以List集合里面如果是这两种类型的对象就不用再重写了。倒序可以用Collections.reverse()方法。
1.8使用数组+链表/红黑树
判断数组是否为空,为空则进行初始化:
不为空则计算k的hash值(拿到k的hashCode()再进行扰动函数),通过(n-1)&hash计算应存放的数组下标index;查看table[index]是否存在数据,不存在就构造一个Node节点存放在该位置,如果存在数据说明发生了hash冲突,(存在两个节点的key的hash值相同),继续判断key是否相等(使用key的equals方法),相等则替换,不相等则进行插入操作,插入时先判断当前节点是不是树形节点,如果是树形节点就创建树形节点插入,否则创建普通Node加入链表(需要遍历判断是否equals链表中某个已存在的key,没有则插入链表尾部),判断链表长度是否大于8,大于则转换为红黑树。插入完成后判断当前节点数是否大于阈值,如果大于就扩容为原数组的二倍。
如果 new HashMap<>()不传值,默认大小是16,负载因子是0.75,如果传入初始大小k,则初始化大小为大于k的2的整数次方。(HashMap当前size大于等于负载因子乘总容量时进行扩容,扩容时进行rehash来确定各元素在新数组中的位置)
先拿到key的hashCode,(通过调用key的hashCode方法),得到32位的int值,然后让hashcode的高16位与低16位进行异或操作。
这个顺序到底是怎么样的?拿到key的hashcode,再经过扰动函数得到新的hash,再(n-1)&hash得到下标值,是这样吗?是的!
这个也叫扰动函数,这么设计原因有二:
一定要尽可能减少hash碰撞,越分散越好;算法一定要尽可能高效,因为这是高频操作,所以采用位运算。
这时扰动函数的价值就体现出来了。高位与低位异或就是为了混合原始哈希码的高位和低位,以此来加大低位的随机性,而且混合后的低位也掺杂了高位的部分特征,这样高位信息也变相的保留下来。(另外1.7做了4次移位和异或,1.8只做了一次,可能是考虑到效率和实际效果)
不是。Java中有HashTable,Collections.synchronizedMap方法以及ConcurrentHashMap可以实现线程安全的Map。
HashTable是直接在操作方法上加synchronized关键字,锁住整个数组,粒度大,Collections.synchronizedMap是使用Collections集合工具的内部类,通过传入Map封装出一个SynchronizedMap对象,内部定义了一个对象锁,方法内通过对象锁实现;ConcurrentHashMap使用分段锁,降低了锁粒度,使并发度大大提高。
ConcurrentHashMap的成员变量用volatile修饰,免除了指令重排序,同时保证内存可见性,另外使用CAS操作和synchronized结合实现赋值操作,多线程操作只会锁住当前操作索引的节点。
8,红黑树转链表阈值是6
在hash函数设计合理的情况下,发生hash碰撞8次(也就是有8个hash相同的情况)的概率为百万分之6,所以8够用了。如果hash碰撞次数在8附近徘徊,会一直发生链表和红黑树的转化。为了避免这种反复转化的情况所以二者的阈值不同。
是无序的,会根据hash值随机插入。
LinkedHashMap和TreeMap是有序的。
前者(继承了HashMap)内部维护了一个双向链表,定义了头尾结点,节点名为Entry,继承了HashMap的Node类,Entry维护了before和After保存前置节点和后置节点。
后者是按照key的自然顺序或Comparator的顺序排序,内部是通过红黑树实现。所以要么key所属的类实现了Comparable接口(重写了compareTo方法),要么自定义一个实现了Comparator接口的比较器传给TreeMap用于key的比较。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。