ConcurrenHashmap
线程安全的集合
- Hashtable
- ConcurrentHashMap
- Vector
ConcurrentHashMap是HashMap的线程安全版本,ConcurrentSkipListMap是TreeMap的线程安全版本
Hashtable是JDK 5之前Map唯一线程安全的内置实现(Collections.synchronizedMap不算)。Hashtable继承的是Dictionary(Hashtable是其唯一公开的子类),并不继承AbstractMap或者HashMap。尽管Hashtable和HashMap的结构非常类似,但是他们之间并没有多大联系。
为什么要使用ConcurrentHashMap
因为在并发编程中使用HashMap可能导致程序死循环,而HashTable效率低下。
1)HashMap是非线程安全的
在多线程的操作下、若调用HashMap.put()能引起死循环、导致CPU资源消耗大。
多线程操作put()会使得Entry链表形成环形数据结构,使得Entry的next节点永远不为空,就会产生死循环获取Entry。
2)HashTable的效率低
hashTable是用Synchronized来保证线程安全的,所有的线程都必须竞争一把锁、效率太低。
如线程A访问HashTable同步方法put()、B线程也访问同步方法put()、但B没有锁进入阻塞或轮询状态,也不能访问其他的方法,导致线程越多效率越低。
ConcurrentHashMap分析 jdk1.7
属性
int DEFAULT_INITIAL_CAPACITY = 16; //初始容量的默认值-》hashentry的table属性
float DEFAULT_LOAD_FACTOR = 0.75f; //默认的加载因子 -》HashEntry
int DEFAULT_CONCURRENCY_LEVEL = 16; //默认的并发度-》segment数组大小
int MAXIMUM_CAPACITY = 1 << 30; //最大的容量 -》segment中数组的最大值 ->tab.length<MAXIMUM_CAPACITY
int MIN_SEGMENT_TABLE_CAPACITY = 2; //HashEntry数组的最小值
int MAX_SEGMENTS = 1 << 16; // slightly conservative //最大并发度=segment数组的最大值
int RETRIES_BEFORE_LOCK = 2; //锁重试次数
final int segmentMask; //主要作用于keyhash过程
final int segmentShift;
final Segment<K,V>[] segments; //存储数据节点的位置
//存放数据的锁:
static final class Segment<K,V> extends ReentrantLock {
//Segment是一种可重入锁(ReentrantLock),扮演锁的角色;
static final int MAX_SCAN_RETRIES =Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1; //segment获取锁重试次数上限
transient volatile HashEntry<K,V>[] table; //存放数据的
transient int count;
}
//HashEntry的属性
static final class HashEntry<K,V> {
final int hash;
final K key;
volatile V value;
volatile HashEntry<K,V> next;
}
对HashEntry的value、next属性加上volatile,这样保证了可见性、
默认的ConcurrentHashMap中是有16个类似HashMap的结构、每个HashMap拥有一个独占锁。也就是说最终的效果就是通过某种Hash算法,将任何一个元素均匀的映射到某个HashMap的Map.Entry上面,而对某个一个元素的操作就集中在其分布的HashMap上,与其它HashMap无关。这样就支持最多16个并发的写操作。
构造函数
public ConcurrentHashMap(int initialCapacity,float loadFactor, int concurrencyLevel) {
}
//数组大小 加载因子 并发度,默认16个
segments数组长度ssize和concurrentLevel有关,ssize>=concurrentLevel的最小2次幂。
如果觉得这么描述不好理解,那么举个例子就清楚了。比如concurrentLevel=17,2的几次幂刚好>=17呢?是32对吧!所以ssize=32。更重要的是,Segment的数组大小之所以一定是2的次幂,就是为了方便通过按位与的散列算法来定位Segment的index位置
put()
通过加锁机制插入数据
1、定位数据应该放在哪个Segment中
通过hash算法定位到对应的Segment,若获取到的Segment为空,则调用ensureSegment()方法;
否则,直接调用查询到的Segment的put方法插入值.
ensureSegment()方法,使用getObjectVolatile()读取对应Segment,如果还是为空,则以segments[0]为原型创建一个Segment对象,并将这个对象设置为对应的Segment值并返回。
if (value == null) throw new NullPointerException();
int hash