一.HashTable和HashMap
Hashtable是JAVA中依靠哈希表来实现的,学习数据结构的时候我们已经知道哈希表是基于数组加链表去实现的。哈希表最重要的是扩容机制和哈希冲突,当负载因子大于0.75时会触发扩容机制。哈希冲突是多个数通过哈希函数映射到同一位置。为了解决哈希冲突会在数组上挂一个链表,往链表上去插入元素。HashMap底层是依靠这种原理来实现的,是数组加链表加红黑树。而TreeMap底层是一颗二叉搜索树,HashMap增删查改的时间复杂度都为o(1),但HashMap和TreeMap是线程不安全的,在多线程的场景下会产生线程安全问题,java中的HashTable是线程安全的。
二.HashTable
Vector和HashTable属于java很早之前的集合,HashTable给每个方法加了锁,加了锁不一定线程安全,不加锁也不一定线程不安全。
虽然HashTable给每个方法加了锁,但是如果添加元素的同时删除元素,这2个操作整体不是原子性的,在多线程的情况下执行结果可能发生错误。所以vector/ HashTable这样的集合类,虽然加了synchronized也不能保证一定线程安全,同时在单线程的情况下,可能因为加锁影响效率。
HashTable是给方法加了锁,就相当于给this加了锁,不管是删除,增加,判空等都会针对this加锁,此时如果很多线程对HashTable进行操作会产生激烈的锁竞争,这些线程只能一个一个执行效率很低。
#sd ConcurrentHashMap
HashMap和HashTable相比,一个线程不安全一个线程安全。ConcurrentHashMap对HashTable进行了优化,HashTable对每个方法加了锁,多线程执行的时候,有的逻辑没必要加锁,全部加锁会使锁竞争激烈,执行效率很低。 ConcurrentHashMap对此进行了优化。
实现哈希表最主要的是解决哈希冲突,哈希冲突是通过哈希函数映射到同一位置产生的。哈希函数是元素除以数组长度,假设数组长度为10,同时添加4和14到数组中,那么4%10=4,14%10=4,那么4和14都会放到数组4下标的位置,这样就产生了冲突,我们用哈希桶来解决哈希冲突。给数组每个下标挂上一个链表,虽然4和14都会放到数组4下标的位置,但是让4放到数组4下标链表第一个元素,14放到数组4下标链表第二个元素,这样避免了冲突。
如果多线程同时插入2个数会不会产生线程安全问题?
多线程插入到2个不同的链表不会产生线程安全问题。
如果是往同一个链表上插,并且这2个节点紧挨着那么会产生线程安全问题,如上图。ConcurrentHashMap为了防止这种问题产生给每个节点都加了一把锁,这样同时对同一个链表进行插入只能等第一个插入结束,第二个才能继续执行。
2个线程同时给不同链表添加元素是不会有线程安全问题,那么这样加锁有没有必要?因为2个线程同时操作同一个链表本身就是概率很低的事情,其实整体锁的开销也不是特别大。
三.ConcurrentHashMap的改进
1、减小的锁的粒度,每个链表有一把锁,大部分情况不会涉及锁冲突
2.广泛使用原子类,比如size++,这样的操作,利用CAS操作。这样的操作也不会有锁冲突。
3.写操作进行了加锁读操作没有,可以认为是一把读写锁。
4.针对扩容进行了优化
哈希表扩容是负载因子大于0.75。负载因子随着元素的逐渐增多会逐渐增大,当插入到一定数量的元素后需要扩容,开辟一个新的数组,重新哈希,把元素放到新的数组中,那么当多个线程同时执行的时候,触发扩容机制会阻塞一段时间去,放下当前的操作去扩容,这样会影响整个程序的效率。
5.ConcurrentHashMap化零为整,当要进行扩容时会创建出一个更大的数组,把旧的数据逐渐往新数组上哈希。
a.新增元素的同时,往新数组上插入。
b.删除元素把旧数组的元素删除。
c.查找元素新旧数组都查找
d.修改元素统一把元素搞到新数组上去。
与此同时,每个操作都会触发一定的搬运,每次搬运一点,就可以保证整体的时间不是很长,积少成多每次就完成搬运了。全部完成后在销毁旧的数组。
四. ConcurrentHashMap的锁分段技术
java8之前,ConcurrentHashMap是使用分段锁,从java8之后每个链表一把锁。
这样做能提高效率但是不如每个链表一把锁,代码实现起来复杂。