HashMap概要
HashMap继承AbstractMap <K,V>,实现Map <K,V>,Cloneable,Serializable接口,提供put,get方法存取数据
HashMap数据结构
HashMap是由数组+链表构成,链表主要是为了解决哈希冲突问题即不同key的hash值可能一样(拉链法)。对于查找,如果定位到的数组不含链表,一次寻址即可,算法复杂度O(1),含有链表,需要用equals逐一比较,这个过程的算法复杂度O(n),jdk1.8以后做了优化,当链表长度大于阈值(TREEIFY_THRESHOLD = 8)且当前容量大于最小值(MIN_TREEIFY_CAPACITY = 64,小于则resize()),链表转化为红黑树,算法复杂度降为O(lgn)。
HashMap为什么线程不安全
内部方法未同步,哈希碰撞和哈希扩容的时候,出现线程不安全的情况。 哈希碰撞时候,线程A判断节点的next指针为null,这时候时间分片用完了,线程B进来了,也判断为null,然后线程A和B都建下一个节点,必然造成其中一个丢成。哈希扩容时候, 当多个线程同时检测到总数量超过门限值的时候就会同时调用resize操作,各自生成新的数组并rehash后赋给该map底层的数组table,结果最终只有最后一个线程生成的新数组被赋给table变量,其他线程的均会丢失。而且当某些线程已经完成赋值而其他线程刚开始的时候,就会用已经被赋值的table作为原始数组,这样也会有问题。
HashMap长度为2的幂次方
如果长度是2的幂次方,则hash%length = hash&(length -1)成立,与运算代替乘法运算,性能提升。
类比数据库的分库分表
如果我们把hashmap的过程和数据库分库分表水平拆分的hash法做类比,会发现很多思想上的共通性,我们完全可以把hashmap的这一套思想迁移过去,例如分成2的冥次方个表/库。
HashMap,HashTable的区别
HashMap线程不安全,HashTable线程安全,Hashtable内部方法基本都通过synchronized修饰,保证了可见性。但是Hashtable效率太低,当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态.
HashTable类比表锁
HashTable和数据库悲观锁中的表锁大体类似。锁的太多,性能就差。
线程安全且效率高的ConcurrentHashMap
JDK1.7的时候,对Table进行了分割分段,每个Segment(ReentrantLock)锁住一部分Table,默认分配16个Segment,效率也就提升16倍。到了JDK1.8的时候,直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。有几个node提升多少倍。
ConcurrentHashMap类比页锁,表锁
JDK1.7的时候,ConcurrentHashMap类似数据库悲观锁-页锁,JDK1.8则类似行锁,锁的数据越来越少,效率也就越来越高。
再说说ReentrantLock和Synchronized
两者都是可重入锁,即两者都是同一个线程没进入一次,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。区别的是ReentrantLock提供了创建公平锁的构造函数,ReentrantLock比较灵活,加锁解锁由用户自己控制,一般记得在finally里释放锁。
再说说Condition和Object
ReentrantLock获得锁后可以通过通过创建Condition对象来使线程wait,condition对象的signal方法可以唤醒wait线程,而notify(),notifyAll()是随机唤起一个。
到这里不得不提一嘴sleep和wait
sleep是Thread类的静态方法,不释放锁资源,休息指定时间后,恢复到就绪状态,wait是Object 类的方法,放弃锁进入等待池,notify(随机唤醒一个)/notifyAll唤起,才由等待池进入锁池,获得对象锁进入就绪状态。
再延伸一下到读写锁ReentrantReadWriteLock
readLock()获取读锁,writeLock获取写锁,读锁,此时多个线程可以或得读锁,写锁,此时只有一个线程能获得写锁,但读锁和写锁是互斥的,读锁释放后才执行写锁的方法,这个可以类比数据库中的读写锁,(in share mode/for update),这里不在赘述。
总结
很多知识点,类比之后我们能够理解的更加清晰,很多的思想其实都有共同之处。
文章出现谬误之处,希望大家指出,互相交流。