这些天一直在看关于多线程和高并发的书籍,也对jdk中的并发措施了解了些许,看到concurrentHashMap的时候感受知识点很乱,有必要写篇博客整理记录一下。java
当资源在多线程下共享时会产生一些逻辑问题,这个时候类或者方法会产生不符合正常逻辑的结果,则不是线程安全的。纵观jdk的版本更新,能够看到jdk的开发人员在高并发和多线程下了很大的功夫,尽量的经过jdk原生API来给开发人员带来最方便最轻松的高并发数据模型,甚至想彻底为开发人员解决并发问题,能够看得出来jdk的开发人员确实很用心。可是在大量业务数据的逻辑代码的状况下高并发仍是不可避免,也不可能彻底经过jdk原生的并发API去解决这些并发问题,开发人员不得不本身去空值在高并发环境下的数据高可用性和一致性。node
前面说了jdk原生的API已经有了不少的高并发产品,在java.util.concurrent包下有不少解决高并发,高吞吐量,多线程问题的API。好比线程池ThreadPoolExecutor,线程池工厂Executors,Future模式下的接口Future,阻塞队列BlockingQueue等等。c++
直接进入正题,concurrentHashMap相信用的人也不少,由于在数据安全性上确实比HashMap好用,在性能上比hashtable也好用。你们都知道线程在操做一个变量的时候,好比i++,jvm执行的时候须要通过两个内存,主内存和工做内存。那么在线程A对i进行加1的时候,它须要去主内存拿到变量值,这个时候工做内存中便有了一个变量数据的副本,执行完这些以后,再去对变量真正的加1,可是此时线程B也要操做变量,而且逻辑上也是没有维护多线程访问的限制,则颇有可能在线程A在从主内存获取数据并在修改的时候线程B去主内存拿数据,可是这个时候主内存的数据尚未更新,A线程尚未来得及讲加1后的变量回填到主内存,这个时候变量在这两个线程操做的状况下就会发生逻辑错误。数组
原子性就是当某一个线程A修改i的值的时候,从取出i到将新的i的值写给i之间线程B不能对i进行任何操做。也就是说保证某个线程对i的操做是原子性的,这样就能够避免数据脏读。安全
Volatile保证了数据在多线程之间的可见性,每一个线程在获取volatile修饰的变量时候都回去主内存获取,因此当线程A修改了被volatile修饰的数据后其余线程看到的必定是修改事后最新的数据,也是由于volatile修饰的变量数据每次都要去主内存获取,在性能上会有些牺牲。数据结构
HashMap在多线程的场景下是不安全的,hashtable虽然是在数据表上加锁,纵然数据安全了,可是性能方面确实不如HashMap。那么来看看concurrentHashMap是如何解决这些问题的。多线程
concurrentHashMap由多个segment组成,每个segment都包含了一个HashEntry数组的hashtable, 每个segment包含了对本身的hashtable的操做,好比get,put,replace等操做(这些操做与HashMap逻辑都是同样的,不一样的是concurrentHashMap在执行这些操做的时候加入了重入锁ReentrantLock),这些操做发生的时候,对本身的hashtable进行锁定。因为每个segment写操做只锁定本身的hashtable,因此可能存在多个线程同时写的状况,性能无疑好于只有一个hashtable锁定的状况。通俗的讲就是concurrentHashMap由多个hashtable组成。并发
看下concurrentHashMap的remove操做jvm
V remove(Object key, int hash, Object value) { lock();//重入锁
try { int c = count - 1; HashEntry<K,V>[] tab = table; int index = hash & (tab.length - 1); HashEntry<K,V> first = tab[index]; HashEntry<K,V> e = first; while (e != null && (e.hash != hash || !key.equals(e.key))) e = e.next; V oldValue = null; if (e != null) { V v = e.value; if (value == null || value.equals(v)) { oldValue = v; // All entries following removed node can stay // in list, but all preceding ones need to be // cloned.
++modCount; HashEntry<K,V> newFirst = e.next; for (HashEntry<K,V> p = first; p != e; p = p.next) newFirst = new HashEntry<K,V>(p.key, p.hash, newFirst, p.value); tab[index] = newFirst; count = c; // write-volatile
} } return oldValue; } finally { unlock();//释放锁
} }
Count是被volatile所修饰,保证了count的可见性,避免操做数据的时候产生逻辑错误。segment中的remove操做和HashMap大体同样,HashMap没有lock()和unlock()操做。高并发
看下concurrentHashMap的get源码
V get(Object key, int hash) { if (count != 0) { // read-volatile
HashEntry<K,V> e = getFirst(hash); //若是没有找到则直接返回null
while (e != null) { if (e.hash == hash && key.equals(e.key)) { //因为没有加锁,在get的过程当中,可能会有更新,拿到的key对应的value可能为null,须要单独判断一遍
V v = e.value; //若是value为不为null,则返回获取到的value
if (v != null) return v; return readValueUnderLock(e); // recheck
} e = e.next; } } return null; }
关于concurrentHashMap的get的相关说明已经在上面代码中给出了注释,这里就很少说了。
看下concurrentHashMap中的put
public V put(K key, V value) { if (value == null) throw new NullPointerException(); int hash = hash(key.hashCode()); return segmentFor(hash).put(key, hash, value, false); }
能够看到concurrentHashMap不容许key或者value为null
接下来看下segment的put
V put(K key, int hash, V value, boolean onlyIfAbsent) { lock(); try { int c = count; if (c++ > threshold) // ensure capacity
rehash(); HashEntry<K,V>[] tab = table; int index = hash & (tab.length - 1); HashEntry<K,V> first = tab[index]; HashEntry<K,V> e = first; while (e != null && (e.hash != hash || !key.equals(e.key))) e = e.next; V oldValue; if (e != null) { oldValue = e.value; if (!onlyIfAbsent) e.value = value; } else { oldValue = null; ++modCount; tab[index] = new HashEntry<K,V>(key, hash, first, value); count = c; // write-volatile
} return oldValue; } finally { unlock(); } }
一样也是加入了重入锁,其余的基本和HashMap逻辑差很少。值得一提的是jdk8中添加的中的putval,这里就很少说了。
ConcurrentHashmap将数据结构分为了多个Segment,也是使用重入锁来解决高并发,讲他分为多个segment是为了减少锁的力度,添加的时候加了锁,索引的时候没有加锁,使用volatile修饰count是为了保持count的可见性,都是jdk为了解决并发和多线程操做的经常使用手段。