本文是按照JDK1.8编写的。
我们使用的HashMap作为共享资源被多线程操作时是会出现线程安全问题的。
注 : 1.8中HashTable和ConcurrentHashMap的key和value值都是不可以为NULL的,NPE
例如:map扩容重哈希死循环问题。具体不做解释了。
可以用HashTable&ConcurrentHashMap代替HashMap来规避这些error。而这个HashTable和ConcurrentHashMap全都是Map的实现类。你之前咋用的现在咋用就行,不同实现类的内部数据结构和算法变了而已,我们就用最好用的就OK了。
我认为ConcurrentHashMap可以用来代替HashTable。因为都是多线程安全的,ConcurrentHashMap要比HashTable效率高。
HashTable的简单吐槽
concurrentHashMap和HashTable都是给map加锁来保证多线程下安全的。但是这两个相比之下concurrentHashMap效率会更高一些。如果你看过HashTable源码你就会发现HashTable其实就是HashMap的synchronized版。
下面是HashTable的put,get方法。可以看到方法前面有个synchronized。
这个要是在多线程环境下就会阻塞住后面的线程。一个一个线程地访问这个方法。
下面看一下ConcurrentHashMap的做法。
ConcurrentHashMap
concurrentHashMap使用锁分段技术来使多线程环境下效率更高。而JDK1.8这个大版本把这个类改的和之前有较大的区别。
1。JDK1.7的代码如下
final Segment<K,V>[] segments;
//这个segment继承自可重入锁ReentrantLock。所以它也相当于一个锁。
static final class Segment<K,V> extends ReentrantLock implements Serializable {
transient volatile HashEntry<K,V>[] table;
transient int count;
}
// 存储Key,Value值
static final class HashEntry<K,V> {
final int hash;
final K key;
volatile V value;
volatile HashEntry<K,V> next;
}
简单介绍一下这个是1.7版本的concurrentHashMap这个使用的是segment来拆分ConcurrentHashMap为n段,每一段相当于一个小的HashMap。差不多是下面这个丑图这样式的。
我们在使用的时候往concurrentHashMap中插入是散列到不同的segment中了。get时候是给一个segment锁中取。意味着与其他segment中的数据是解耦合的。意味着别的线程是可以进入其他segment中操作的。而HashTable是不允许这样做的。
不做过多介绍。下面展示一下JDK1.8这个版本改成啥样了。
2.JDK1.8版本主要做出了两个修改。
(1)在分析HashMap源码时介绍过,JDK1.8引入了红黑树的概念,就是在由于哈希冲突而产生的链在到达一定的长度时采用红黑树的存储方法,o(n)---o(log n) 这样效率会高一些。
(2)搁置了segment,采用锁node[]的方式来编码。
改版之后的ConcurrentHashMap就是线程安全的HashMap
下面为JDK1.8时候的源码。
nextTable是扩容的数组。
我们先看看concurrentHashMap的get方法。它采用CAS这个无锁的模式来写的。concurrentHashMap有好多地方用到CAS
啥是CAS。就是一款无锁的模式。叫compare and swap 对比and交换值。算是乐观锁、像synchronized,lock这种都是悲观锁。
CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。 在原子类中就使用了这种CAS操作。所以一般情况下像atomic才比synchronized这种锁快的。
悲观锁假设最坏的情况,并且只有在确保其它线程不会造成干扰的情况下执行,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。
下面是get的源码。
在concurrentHashMap中有三个CAS方法。
下面看一下put方法。这个put方法也使用到了CAS。并在一条链的第一个节点上了锁。类似segment.但是写法不同。
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
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 {
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;
}
综上:JDK1.5版本及以上就用ConcurrentHashMap来取代HashTable 吧。
注意JDK版本很重要。背了滚瓜烂熟的东西结果用的不是那个JDK版本的。然后人家问你JDK几的代码你不知道岂不尴尬。
关于面试题的HashMap与HashTable的区别可以看这篇博客:https://blog.csdn.net/yu849893679/article/details/81530298
至于一些ConcurrentHashMap细节暂时没有研究,只学了个大概原理。感兴趣可以自行百度。
mark一下一位博主的关于ConcurrentHashMap的部分源码介绍:https://blog.csdn.net/qq296398300/article/details/79074239
参考CAS:https://www.cnblogs.com/Mainz/p/3546347.html
参考ConcurrentHashMap:https://blog.csdn.net/u012403290/article/details/68488562