前言
各位靓仔好呀,我们接着讲讲Map集合,
我们知道HashMap是线程不安全的及不安全在哪里
今天主要介绍线程安全的Map
上一篇文章地址:https://blog.youkuaiyun.com/u013351145/article/details/117450011
当然靓仔们可以看看我整理的系列:光头佳的求职之旅
https://blog.youkuaiyun.com/u013351145/category_11030408.html
在并发编程的现在,为了保持数据安全,HashMap线程不安全的问题注定不适合多线程并发。
因此也是在集合方面面试询问的高发区
HashTable
Hashtable是Map接口下的实现类之一,属于线程安全的集合。
下图为继承关系图
Hashtable是继承Dictionary类与HashMap继承AbstractMap不同,
初始化
Hashtable是默认长度11,加载因子0,75,支持数据结构数组和链表。
// 存放数组
private transient Entry<?,?>[] table;
// 当前行数
private transient int count;
// 阈值
private int threshold;
// 加载因子
private float loadFactor;
private transient int modCount = 0;
// 用指定的初始容量和负载因子构造一个新的空哈希表
public Hashtable(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal Load: "+loadFactor);
if (initialCapacity==0)
initialCapacity = 1;
this.loadFactor = loadFactor;
table = new Entry<?,?>[initialCapacity];
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}
// 指定长度和默认加载因子0.75
public Hashtable(int initialCapacity) {
this(initialCapacity, 0.75f);
}
// 默认11
public Hashtable() {
this(11, 0.75f);
}
新增元素
// key与value不允许为空
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();
// 判断下标位
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
addEntry(hash, key, value, index);
return null;
}
// 新增元素
private void addEntry(int hash, K key, V value, int index) {
modCount++;
Entry<?,?> tab[] = table;
if (count >= threshold) {
// Rehash the table if the threshold is exceeded
// 扩容
rehash();
tab = table;
hash = key.hashCode();
index = (hash & 0x7FFFFFFF) % tab.length;
}
// Creates the new entry.
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}
// threshold初始阈值是MAX_ARRAY_SIZE + 1,扩容大小原有的2倍+1
protected void rehash() {
int oldCapacity = table.length;
Entry<?,?>[] oldMap = table;
// overflow-conscious code
int newCapacity = (oldCapacity << 1) + 1;
if (newCapacity - MAX_ARRAY_SIZE > 0) {
if (oldCapacity == MAX_ARRAY_SIZE)
// Keep running with MAX_ARRAY_SIZE buckets
return;
newCapacity = MAX_ARRAY_SIZE;
}
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
modCount++;
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
table = newMap;
for (int i = oldCapacity ; i-- > 0 ;) {
for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
Entry<K,V> e = old;
old = old.next;
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
e.next = (Entry<K,V>)newMap[index];
newMap[index] = e;
}
}
}
新增元素问题
1.为啥说HashTable线程安全呢?
因为所有操作方法上有synchronized 关键字来保证线程安全
2.synchronized 是什么东东,凭这个关键字就可以保证?
简单来说synchronized 可以看成一个锁,A,B同时去添加新元素,
A先加synchronized锁,
这样B去添加的时候因为加了锁,无法添加
需要A添加完锁之后把锁打开,B去添加的时候也会加把锁,
假设又来了一个C就会陷入B之前的难题。
换成滑滑梯理解的话,熊猫A被熊猫B的萌物冲击顶到了大腰子,
下次再滑的时候在入口加了栏杆,只有自己滑到底去把栏杆拿掉,
熊猫B才能爬上滑滑梯,这样就很好的保护了自己的大腰子
挖坑
synchronized 作为并发编程锁之一,我准备在多线程的时候再讲,
先挖个小坑,以为我风一般的更新速度马上就到了。
3.HashTable这么好为啥不用
因为安全,成也萧何败也萧何,
安全的提前是牺牲了熊猫B的滑滑梯时间,
那没有更好的方法吗,接下来介绍下一位主角。
ConcurrentHashMap
ConcurrentHashMap是线程安全的map集合,初始化HashMap基本一致,我就不过多介绍,
直接说结论ConcurrentHashMap线程安全的实现,1.7和1.8方式不同,
1.8:CAS锁(算法)+synchronized
1.7:利用Segment,实现可重入式分段锁
1.8 新增元素put()
public V put(K key, V value) {
return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
// 看来key和value不允许为空
if (key == null || value == null) throw new NullPointerException();
// 计算hash值
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 初始化数组和判断下标位与hashMap一致的
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 多了一步需要先同步cas判断值有没发生改变
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
// 扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
// 最后的方式添加synchronized 然后走HashMap树化扩容treeifyBin
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
Node<K,V> c, Node<K,V> v) {
return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
// 扩容
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
Node<K,V>[] nextTab; int sc;
// 检验判断非空
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
// 根据resizeStamp拿到一个标识
int rs = resizeStamp(tab.length);
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab);
break;
}
}
return nextTab;
}
return table;
}
1.比HashTable多了一个CAS,什么是CAS?
CAS算法:(Compare And Swap)比较交换,而synchronized不管三七二十一先把锁加上,
CAS算法中包含三个参数(V,E,N),V表示要更新的变量、E表示预期的值、N表示新值。
我现在要改一个值,我知道原来要更新的V=1,那么E等于我所有知道的V=1
,现在我N=V+1把三个值去覆盖更新,重新获取一次V,发现V已经是2,那么说明有人抢在我前面更新了。
其实V和N很好理解的,E可能会混乱你的思路。
有一次A熊猫忘记了把栏杆拿下来,B熊猫被栏杆卡主无法爬上滑滑梯,哭着去找园长,园长就是把滑滑梯改造了一下。
滑滑梯上会显示号码,你去园长那边领一张票,如果票上的号码和滑滑梯的号码一样,你就可以去爬上去滑。
如果不对再去找园长那一张新的票,票就是E,号码对了才能使用。
1.7 新增元素put()
通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。
锁分段技术:首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
// Segment 数组,存放数据时首先需要定位到具体的 Segment 中。
final Segment<K,V>[] segments;
//
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
int hash = hash(key);
int j = (hash >>> segmentShift) & segmentMask;
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);
return s.put(key, hash, value, false);
}
1.为啥1.8里放弃使用Segment 数组?
还是我们老生常谈的问题,时间与空间,分成16端是很帅,变相的将写入操作加快16倍,但是显而易见的空间大大的被浪费。
第二个关键点:现实生活中 map 在放入时竞争同一个锁的概率非常小,分段锁反而会造成更新等操作的长时间等待。
小结
关于HashTable和ConcurrentHashMap,相比较HashMap我没有过多的介绍,
目前维持多线程安全问题解决方式就是锁,各种各样的锁,
从公平性出发:公平锁,非公平锁
从应用出发:轻量级,重量级,
及自旋锁,排它锁,可重入锁…
太多了,我更想在并发编程的时候介绍清楚。
我咋感觉你就是想偷懒?
TreeMap
我们知道HashMap是无序的,TreeMap与HashMap相比,TreeMap是一个能比较元素大小的Map集合,会对传入的key进行了大小排序。
看这个英文就知道TreeMap是红黑树结构
下图为继承关系
初始化
// 比较器
private final Comparator<? super K> comparator;
private transient Entry<K,V> root;
private transient int size = 0;
private transient int modCount = 0;
// 构造默认无比较器的treeMap
public TreeMap() {
comparator = null;
}
// 构造指定比较器的treeMap
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
新增元素
public V put(K key, V value) {
Entry<K,V> t = root;
if (t == null) {
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
Comparator<? super K> cpr = comparator;
// 如果比较器不为空
if (cpr != null) {
do {
// 从根节点开始
parent = t;
// 比较当前key和出入的key的大小
cmp = cpr.compare(key, t.key);
// 小的话,往左边移动
if (cmp < 0)
t = t.left;
// 大的话,往右边边移动
else if (cmp > 0)
t = t.right;
else
// 如果相等就覆盖,更新
return t.setValue(value);
} while (t != null);
}
// 在没有比较器的情况下,调用自然排序的Comparable比较
else {
// key不能空
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
小结
开发过程中较少使用到这个TreeMap因此也没啥较深的理解
到这里集合介绍的差不多,应该还会出一些面试题,
其中有别的面试官问我的,也有我觉得该掌握的点。