juc——ConcurrentHashMap
ConcurrentHashMap
ConcurrentHashMap较HashTable效率高的原因
1、HashTable 是在方法上面加的synchronized,put(),get(Object)都是同步方法,所以针对同一个hashTable,那就是同一把对象锁,读、写操作,都需要同步执行,效率低下。
2、容器中有多把锁,每一把锁锁住的是容器中的一部分数据,当多个线程访问容器中的不同的数据段的时候,由于获取的是不同的锁,所以不存在竞争的问题,从而提高并发访问效率。采用锁分段技术:首先将数据分成一段一段的存储,然后给每一段数据配置一把锁,当一个线程占有锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。多线程对于同一个段数据的访问,是互斥的;但是对于不同片段的访问,却是可以同步进行的。
ConcurrentHashMap结构原理
1、ConcurrentHashMap是由Segment数组接口和HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap扮演锁的角色; HashEntry用于存储键值对数据。
2、Segment的结构和HashMap类似,是一组数组和链表结构。一个Segment包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护者一个HashEntry数组里面的元素,当对HashEntry的数组进行修改的时候,首先需要获得HashEntry数组对应的数据段的Segment锁。
3、读不加锁:定义成volatile的变量,能够在线程之间保持可见性,能够被多线程同时读,并且保证不会读到过期的值,但是只能被单线程写(有一种情况可以被多线程写,就是写入的值不依赖于原值),在get操作里只需要读写 transient volatile HashEntry<K,V>[] table;所以可以不用加锁。之所以不会读到过期的值,是根据java内存模型的happen before原则,对volatile字段的写入操作先于读操作,即使两个线程同时修改和获取volatile变量,get操作也能拿到最新的值,这是用volatile替换锁的经典应用场景。
4、ConcurrentHashMap内部分为很多个Segment,每一个Segment拥有一把锁,然后每个Segment(继承ReentrantLock)下面包含很多个HashEntry列表数组。对于一个key,需要经过三次(为什么要hash三次下文会详细讲解)hash操作,才能最终定位这个元素的位置,这三次hash分别为:
对于一个key,先进行一次hash操作,得到hash值h1,也即h1 = hash1(key);
将得到的h1的高几位进行第二次hash,得到hash值h2,也即h2 = hash2(h1高几位),通过h2能够确定该元素的放在哪个Segment;
将得到的h1进行第三次hash,得到hash值h3,也即h3 = hash3(h1),通过h3能够确定该元素放置在哪个HashEntry。
如上分析,我们可以知道,使用ConcurrentHashMap时,单就 put、get这样的单一操作而言,可以保证map在多线程并发情况下的安全性。
具体使用代码
ConcurrentHashMap put操作线程安全
比较HashMap产生线程不安全的原因在于,size方法是取得HashMap中的size属性,在put操作是,多线程并发下,size++是线程不安全的,所以出现错误;还有就是有可能hash冲突位置,多线程同时冲突一个点的话,可能出现数据丢失。如下
线程池使用注意事项 保证HashMap put操作线程安全解决办法