HashTable
单纯的使用一个synchronized进行加锁~,具体相当于针对整个 HashTable对象,但是坏处就是这样的话锁冲突的概率是非常高的 如果有多个线程,线程1操作的在第一个链表上,线程2操作的元素在别的链表上,这个时候不涉及到线程安全。
此时两个线程,修改不同的变量,实际上是没有线程安全的,但是HashTable直接一把锁,锁住了,线程1去操作的时候,线程2就会被阻塞等待,这样效率就会比较低。
此外,在扩容的时候,如果某个线程T正好触发了扩容,那么这个T就倒霉了,就要负责完成整个扩容过程~
ConcurrentHashMap
内部针对多线程做出了一定的优化,并不是针对整个对象加一把锁,而是分成了很多把锁~ 降低锁冲突的概率~
注意:java1.8之后才是这样分的,而java1.7里采用分段锁,好几个链表公用一把锁。
1.每个链表 / 红黑树 分配一把锁~ 只有当两个线程恰好修改的是同一个链表/红黑树的时候才会涉及到锁冲突 ~ 2.针对读操作,ConcurrentHashMap直接不加锁!!!这是一个比较激进的手段,但是也不至于产生大问题~~
但是如果是一个线程读,一个线程写,就会有一点问题? 如果读不加锁~ 读操作就可能会读到一个写之前的值,也可能读到一个写之后的值,还可能读到一个写中间的值… 但是在大部分的场景下,也没啥影响。
举个例子: 这个时候你就在想,正好在12点这个时间点上,你展示的是修改前的数据还是修改后的数据,那个合理呢?仔细想一想,这都无所谓,都能说得通,这个时候线程不安全,也就不是啥大问题,影响不大~
ConcurrentHashMap 敢这样做,就是因为大部分场景对于读的线程安全操作没有很高要求~ 如果要是想让读也更准确的话,优化手段应该是使用读写锁~
3.ConcurrentHashMap内部也使用了广泛的CAS操作,来提高效率: 比如,获取元素个数的时候,没加锁,直接CAS; 比如,修改元素,获取对应的链表下标的时候,也是使用CAS…
4.针对扩容进行了优化~
ConcurrentHashMap把扩容任务分散开了,类似于蚂蚁搬家一样,一次只扩容一点~
当ConcurrentHashMap触发扩容的时候,比如说线程T触发了扩容,T线程把ConcurrentHashMap中一部分元素顺便搬运到新的hash表上去,然后线程B再搬运一部分,然后A线程也搬运一点元素到新hash表,由N个线程分批完成一点,逐渐完成扩容。
这个就可以避免把压力全给1个线程,平滑的进行过渡,从而降低整体的开销~
总结
HashTable 是一个线程安全的类,它使用synchronized来锁住整张Hash表来实现线程安全,即每次锁 住整张表让线程独占,相当于所有线程进行读写时都去竞争一把锁,导致效率非常低下。
ConcurrentHashMap可以做到读取数据不加锁,并且其内部的结构可以让其在进行写操作的时候能够 将锁的粒度保持地尽量地小,允许多个修改操作并发进行,其关键在于使用了锁分离技术。它使用了多 个锁来控制对hash表的不同部分进行的修改。只要不争夺同一把锁,它们就可以并发进行。
|