Hashtable 、ConcurrentHashMap源码学习
来源:互联网 发布:python oxc000007b 编辑:程序博客网 时间:2024/06/05 03:47
一、简介
HashTable是线程安全的HashMap,两个实现原理都是一样的,只是HashTable集合的所有方法都是synchronized方法,而ConcurrentHashMap就不一样了,他最底层的存储使用的也是和HashMap一样的但是,在线程安全处理上有很大区别,他使用了分段的概念,来减小锁的范围,而且在获取时就没有加锁,而是使用了volatile变量来修饰V和Next,总的来说ConcurrentHashMap的效率比HashTable的效率高很多。下面来一个个详细看一下!
看源码之前先啰嗦两句,就是说我们分析源码的时候就按照类初始化以及调用put、get方法这个的一个思路来看!
二、HashTable 源码分析
1、HashTable类主要成员变量:
/** * The hash table data. *具体存放数据的数组,不能序列化 */ private transient Entry<K,V>[] table; /** * The total number of entries in the hash table. * table中的元素的个数 */ private transient int count; /** * The table is rehashed when its size exceeds this threshold. (The * value of this field is (int)(capacity * loadFactor).) *扩容阀值 threshold =capacity * loadFactor 在添加元素是会用到 * @serial */ private int threshold; /** * The load factor for the hashtable. *负载因子,也就是Entry<K,V>数组装满的程度 * @serial */ private float loadFactor;
2、Entry类型(HashTable类的静态内部类):
/** * Hashtable bucket collision list entry * 实现Map接口中的内部接口Entry接口,这个内部接口只有getKey getValue setValue equals 和hashCode方法 */ private static class Entry<K,V> implements Map.Entry<K,V> { int hash; //key的hash值 final K key; //key值 V value; //value值 Entry<K,V> next; //构造函数初始化成员变量 protected Entry(int hash, K key, V value, Entry<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } //对象的克隆 protected Object clone() { return new Entry<>(hash, key, value, (next==null ? null : (Entry<K,V>) next.clone())); } // Map.Entry Ops public K getKey() { return key; } public V getValue() { return value; } public V setValue(V value) { if (value == null) throw new NullPointerException(); V oldValue = this.value; this.value = value; return oldValue; } public boolean equals(Object o) { if (!(o instanceof Map.Entry)) return false; Map.Entry<?,?> e = (Map.Entry)o; return key.equals(e.getKey()) && value.equals(e.getValue()); } public int hashCode() { return hash ^ value.hashCode(); } public String toString() { return key.toString()+"="+value.toString(); } }
ok这样HashTable的重要成员变量有了
3、HashTable构造函数:
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); useAltHashing = sun.misc.VM.isBooted() && (initialCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD); }最终都调用的这个构造函数,如果没有参数则使用默认的值即16和0.75f
构造函数也没什么就创建了一个Entry<K,V>[] table的数组。
4、put方法:
//使用synchronized修饰方法,所以线程访问的时候需要有该方法对象的对象锁才能访问 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; //计算hash值 int hash = hash(key); //计算Entry[] table 数组的索引值。 int index = (hash & 0x7FFFFFFF) % tab.length; for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) { //如果key的hash值相等并且key值也相等则说明已经有这个值了,则直接使用新的Entry对象的value替换旧值就可以了! if ((e.hash == hash) && e.key.equals(key)) { V old = e.value; e.value = value; return old; } } modCount++; //这个是重点,就是判断是否要进行扩容 if (count >= threshold) { // Rehash the table if the threshold is exceeded // 扩容方法 rehash(); tab = table; hash = hash(key); index = (hash & 0x7FFFFFFF) % tab.length; } // Creates the new entry. //创建新Entry实例,并将它添加到链表头结点! Entry<K,V> e = tab[index]; tab[index] = new Entry<>(hash, key, value, e); count++; return null; }
5、rehash()在put时进行扩容:
protected void rehash() { int oldCapacity = table.length; Entry<K,V>[] oldMap = table; // overflow-conscious code //新容量为旧容量*2+1 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<K,V>[] newMap = new Entry[newCapacity]; modCount++; //计算新阀值 threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1); boolean currentAltHashing = useAltHashing; useAltHashing = sun.misc.VM.isBooted() && (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD); boolean rehash = currentAltHashing ^ useAltHashing; table = newMap; //计算新旧数组中的Entry在新数组中的索引位置,并存放到新数组中 for (int i = oldCapacity ; i-- > 0 ;) { for (Entry<K,V> old = oldMap[i] ; old != null ; ) { Entry<K,V> e = old; old = old.next; if (rehash) { e.hash = hash(e.key); } //计算在新数组中的索引位置 int index = (e.hash & 0x7FFFFFFF) % newCapacity; //作为头结点 e.next = newMap[index]; //元素添加到新数组中 newMap[index] = e; } } }
put的过程:
(1)、首先判断value的值是否为null如果为null则抛出异常
(2)、如果不为null则计算key的hash值和在HashEntry [] table 中的索引值
(3)、如果索引值中有值并且hash值和key都相等则直接用新的value替换旧的value值返回旧value值,put结束
(4)、如果索引值中没有值则判断HashEntry[] table 数组中元素的个数和阀值的大小来决定是否要进行扩容
(5)、如果要进行扩容则调用rehash()方法进行扩容,新HashEntry [ ] newTable 数组的容量是 HashEntry [ ] oldTable容量的两倍+1
(6)、判断这个新容量和最大容量的大小,如果新容量大于最大容量则则新容量就等于最大容量
(7)、按照最新容量来创建HashEntry[] newTable 数组
(8)、计算新旧数组中的Entry在新数组中的索引位置,并存放到新数组中
(9)、获取新放入的Entry的key在新数组中的索引位置上的Entry
(10)、按照传进来的K、V 以及新索引位置上获得的Entry等值创建一个新的Entry添加到新数组索引位置上
(11)、返回null 添加方法完毕!!!
6、get方法:
//同样使用了synchronized方法修饰所以是线程安全的(ConcurrentHashMap的get方法是没加锁的) public synchronized V get(Object key) { //获取引用 Entry tab[] = table; //计算hash值 int hash = hash(key); //计算索引值 int index = (hash & 0x7FFFFFFF) % tab.length; //获取索引处的元素,在遍历链表获取key相等的Entry实例 for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { return e.value; } } //没有则返回null return null; }
get方法的过程比较简单,也写了注释所以就不描述整个过程了!!!
三、ConcurrentHashMap 源码分析
ConcurrentHashMap的学习我们也使用上面的方式来学习,即构造函数的初始化,put方法和get方法,下面是ConcurrentHashMap的一个实例展示图:
从这个图中我们看到ConcurrentHashMap就是两个集合加一个链表组成的,相比HashMap/Hashtable他是多了一层数组,具体的细节看下面讲解!
1、ConcurrentHashMap类主要的成员变量:
/** * The default initial capacity for this table, * used when not otherwise specified in a constructor. * 默认初始容量16 */ static final int DEFAULT_INITIAL_CAPACITY = 16; /** * The default load factor for this table, used when not * otherwise specified in a constructor. * 默认负载因子 0.75f */ static final float DEFAULT_LOAD_FACTOR = 0.75f; /** * The default concurrency level for this table, used when not * otherwise specified in a constructor. * 默认的并发级别(并发级别也就是锁端的个数) */ static final int DEFAULT_CONCURRENCY_LEVEL = 16; /** * The maximum capacity, used if a higher value is implicitly * specified by either of the constructors with arguments. MUST * be a power of two <= 1<<30 to ensure that entries are indexable * using ints. * 最大容量2^30次方 */ static final int MAXIMUM_CAPACITY = 1 << 30; /** * The minimum capacity for per-segment tables. Must be a power * of two, at least two to avoid immediate resizing on next use * after lazy construction. * 最小段容量,也就是segment中HashEntry[] tab 数组的容量 */ static final int MIN_SEGMENT_TABLE_CAPACITY = 2; /** * The maximum number of segments to allow; used to bound * constructor arguments. Must be power of two less than 1 << 24. * 最大段数也就是Segments[] 数组的大小 */ static final int MAX_SEGMENTS = 1 << 16; // slightly conservative
/** * The segments, each of which is a specialized hash table. * 段,每一节都是专门的哈希表 */ final Segment<K,V>[] segments;这个所谓的段(segment)也就是ConcurrentHashMap和Hashtable的最大区别,这个在上面的图形中我们也看到
Segment 类:
static final class Segment<K,V> extends ReentrantLock implements Serializable { /** * The per-segment table. Elements are accessed via * entryAt/setEntryAt providing volatile semantics. * 真正存储数据的地方即一个segment类中包含一个HashEntry<k,v>[]table 数组,具体的元素是在这个集合中存储 */ transient volatile HashEntry<K,V>[] table; /** * The number of elements. Accessed only either within locks * or among other volatile reads that maintain visibility. */ transient int count; /** * The total number of mutative operations in this segment. * Even though this may overflows 32 bits, it provides * sufficient accuracy for stability checks in CHM isEmpty() * and size() methods. Accessed only either within locks or * among other volatile reads that maintain visibility. */ transient int modCount; /** * The table is rehashed when its size exceeds this threshold. * (The value of this field is always <tt>(int)(capacity * * loadFactor)</tt>.) * 段内HashEntry[]数组的阀值 */ transient int threshold; /** * The load factor for the hash table. Even though this value * is same for all segments, it is replicated to avoid needing * links to outer object. * @serial * 一个段内HashEntry[] 的负载因子 */ final float loadFactor; Segment(float lf, int threshold, HashEntry<K,V>[] tab) { this.loadFactor = lf; this.threshold = threshold; this.table = tab; }
这是segment(段)对象的一些重要成员变量和构造函数,好了这样ConcurrentHashMap的主要成员变量我们都知道了,下面来看看ConcurrentHashMap的构造函数:
public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) { if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0) throw new IllegalArgumentException(); if (concurrencyLevel > MAX_SEGMENTS) concurrencyLevel = MAX_SEGMENTS; // Find power-of-two sizes best matching arguments int sshift = 0; int ssize = 1; while (ssize < concurrencyLevel) { ++sshift; ssize <<= 1; } this.segmentShift = 32 - sshift; this.segmentMask = ssize - 1; if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; int c = initialCapacity / ssize; if (c * ssize < initialCapacity) ++c; int cap = MIN_SEGMENT_TABLE_CAPACITY; while (cap < c) cap <<= 1; // create segments and segments[0] Segment<K,V> s0 = new Segment<K,V>(loadFactor, (int)(cap * loadFactor), (HashEntry<K,V>[])new HashEntry[cap]); Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize]; UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0] this.segments = ss; }主要的就三个参数即容量,负载因子,多线程并发容量默认情况下是16,而按照传入的参数我们创建了Segment对象,segment类构造函数如下:
Segment(float lf, int threshold, HashEntry<K,V>[] tab) { this.loadFactor = lf; this.threshold = threshold; this.table = tab; }看构造函数就能明白其实一个Segment中主要就是一个HashEntry[] table 数组而数据的具体存储就是在HashEntry 这个对象中,下面是HashEntry对象的成员变量和构造方法:
static final class HashEntry<K,V> { final int hash; final K key; volatile V value; volatile HashEntry<K,V> next; HashEntry(int hash, K key, V value, HashEntry<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; }
其实HashMap和ConcurrentHashMap相比ConcurrentHashMap就多了一层数组,其他的么什么区别(不包括线程安全相关)。
2、ConcurrentHashMap 的put方法:
(1)、我们平时调用的就是这个put方法即ConcurrentHashMap对象的put方法:
public V put(K key, V value) { Segment<K,V> s; if (value == null) throw new NullPointerException(); //获取key的hash值 int hash = hash(key); //获取key在segment[]数组中的索引值 int j = (hash >>> segmentShift) & segmentMask; if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck (segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment s = ensureSegment(j); //如果segment[]数组的这个索引位置没有segment元素则创建一个,如果有则返回segment对象。 //也就是在这里吧put方法委托给了segment对象 return s.put(key, hash, value, false); }
// segment类的put方法,为何说是segment类的put方法因为ConcurrentHashMap也有put方法 //其实在我们调用put方法时ConcurrentHashMap类吧添加的操作委托给了segment的put方法即这里的put方法 final V put(K key, int hash, V value, boolean onlyIfAbsent) { //segment类是继承了ReentrantLock类的所以可以直接调用trylock()方法来获取锁,而对操作部分进行加锁 //而且注意它只是对某个segment对象进行加锁而不是对segment[] segments数组进行加锁 HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value); V oldValue; try { HashEntry<K,V>[] tab = table; //计算key在HashEntry[]tab中的索引位置 int index = (tab.length - 1) & hash; HashEntry<K,V> first = entryAt(tab, index); for (HashEntry<K,V> e = first;;) { //如果在原来的位置有元素,则判断key是否相等,如果相等则只替换value值如果不相等则遍历直到找到相等的那个元素。 if (e != null) { K k; if ((k = e.key) == key || (e.hash == hash && key.equals(k))) { oldValue = e.value; if (!onlyIfAbsent) { e.value = value; ++modCount; } break; } e = e.next; } else { if (node != null) node.setNext(first); else node = new HashEntry<K,V>(hash, key, value, first); //创建新节点first设置为他的next元素 int c = count + 1; //segment中的Hashentry[]table 数组中元素的个数 // 如果Hashentry[] 数组中的元素个数比阀值大并且表的长度比最大容量小,则进行扩容 if (c > threshold && tab.length < MAXIMUM_CAPACITY) rehash(node); else setEntryAt(tab, index, node); ++modCount; count = c; oldValue = null; break; } } } finally { //释放锁,这里加锁和释放锁都是使用了ReentrantLock对象的方法即tryLock()和unlock()方法 unlock(); } return oldValue; }
(3)、put时进行扩容:
在我们调用put方法添加元素的时候,如果我们添加元素的个数大于了阀值则要进行HashEntry[]table 数组的扩容:
private void rehash(HashEntry<K,V> node) { /* * Reclassify nodes in each list to new table. Because we * are using power-of-two expansion, the elements from * each bin must either stay at same index, or move with a * power of two offset. We eliminate unnecessary node * creation by catching cases where old nodes can be * reused because their next fields won't change. * Statistically, at the default threshold, only about * one-sixth of them need cloning when a table * doubles. The nodes they replace will be garbage * collectable as soon as they are no longer referenced by * any reader thread that may be in the midst of * concurrently traversing table. Entry accesses use plain * array indexing because they are followed by volatile * table write. */ HashEntry<K,V>[] oldTable = table; int oldCapacity = oldTable.length; //新容量是旧容量的两倍 int newCapacity = oldCapacity << 1; //新阀值 threshold = (int)(newCapacity * loadFactor); //新数组集合 HashEntry<K,V>[] newTable = (HashEntry<K,V>[]) new HashEntry[newCapacity]; int sizeMask = newCapacity - 1; //循环遍历吧旧数组中的元素都拿出来,在重新计算在新的数组中索引的值并进行添加。 for (int i = 0; i < oldCapacity ; i++) { HashEntry<K,V> e = oldTable[i]; if (e != null) { HashEntry<K,V> next = e.next; int idx = e.hash & sizeMask; //如果下一个元素为null则说明这个索引位置上只有一个元素即头结点,所以直接放到新数组中新的索引位置 if (next == null) // Single node on list newTable[idx] = e; else { // Reuse consecutive sequence at same slot //说明这个索引位置是有多个元素即链式的元素而非单个元素 HashEntry<K,V> lastRun = e; int lastIdx = idx; //循环获取这个链表的最后一个元素 for (HashEntry<K,V> last = next; last != null; last = last.next) { int k = last.hash & sizeMask; if (k != lastIdx) { lastIdx = k; lastRun = last; } } //将链表的最后一个元素放入到新数组中 newTable[lastIdx] = lastRun; // Clone remaining nodes //从头结点开始遍历这个链表中的元素直到最后一个元素 for (HashEntry<K,V> p = e; p != lastRun; p = p.next) { V v = p.value; int h = p.hash; int k = h & sizeMask; //获取新数组中的这个索引位置的元素 HashEntry<K,V> n = newTable[k]; //按照传递进行的hash K V 创建一个新元素添加到新数组的头结点。 newTable[k] = new HashEntry<K,V>(h, p.key, v, n); } } } } int nodeIndex = node.hash & sizeMask; // add the new node node.setNext(newTable[nodeIndex]); newTable[nodeIndex] = node; table = newTable; }
(1)、首先判断value值是否为null,如果为null则抛出异常
(2)、获取key的hash值并按照key的hash值计算segment的索引值index
(3)、按照index从segment [] segments 数组中获取segment对象
(4)、如果index处的segment为null则创建一个segment对象并添加到index位置并返回
(5)、如果已经有了就把那个segment对象返回
(6)、将ConcurrentHashMap 中的put委托给了segment对象的put方法
(7)、获取锁tryLock()如果获取不到则调用ScanAndLockForPut(key,hash,value)去判断当前的segment的HashEntry [] 数组中有没有这个HashEntry值以及锁的获取
(8)、拿着新增的key和hash值到HashEntry[] talbe 中查找,看这个位置有没有值,如果有并且key相等的话则用新的value替换旧的value值,结束!
(9)、如果有而key的值不相等则遍历这个链表找到key相等的元素然后替换结束!
(10)、如果没有则新创建一个HahEntry对象
(11)、判断HashEntry<K,V>[] table中元素的个数是否大于阀值 (这里说的还是某个segment中的HashEntry<K,V>[] table数组而不是segment[] segments)
(12)、如果不大于阀值则直接在索引处添加HashEntry<K,V> 实例,添加结束!!!
(13)、如果元素个数大于阀值则要进行扩容
(14)、获取新HashEntry<K,V>[] newTable 的容量就原来的旧数组的两倍
(15)、计算新的阀值
(16)、创建新的HashEntry<K,V>[] newTable数组
(17)、将旧数组中的元素从新计算在新数组中的索引位置并迁移到新数组中
(18)、将新HashEntry<K,V>实例添加到新的HashEntry<K,V>[] newTalbe数组中
(19)、扩容结束,添加方法结束
(20)、finally中释放锁即unlock();
注意:这里说的扩容的过程是对某个segment 中的HashEntry<K,V>[] 数组进行扩容。
ok put方法相关的到此就结束了,其实你也发现了和Hashtable相比除了在数据存储设计上的不同之外,代码上最大的区别就是在put方法里,也是因为设计的不同而不同。
3、ConcurrentHashMap的get方法:
public V get(Object key) { Segment<K,V> s; // manually integrate access methods to reduce overhead HashEntry<K,V>[] tab; //key的hash值 int h = hash(key); //获取segment的索引值 long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE; if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null && (tab = s.table) != null) { //如果索引位置能获取到segment对象则获取这个segment对象中的HashEntry[] table数组 for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE); e != null; e = e.next) { K k; //判断key值是否相等,返回相等的实例的value值。 if ((k = e.key) == key || (e.hash == h && key.equals(k))) return e.value; } } return null; }你发现没get方法是没有加锁的哦,这时大家会担心说会不会出现脏读的情况啊,不会的如果你看了segment类你会发现segment类中的HashEntry [] table 是volatile的即线程可见的,如果volatile修饰了一个变量则这个变量在是线程可见的,而且HashEntry 实例中的 K是final 的而value是volatile修饰的所以线程获取value值时是不会出现脏读的情况的。
ok这样concurrentHashMap类的初始化、添加、获取都走了一遍,下面我们用图展示出Hashtable和ConcurrentHashMap的结构在整理总结一下:
(1)、Hashtable 图形结构
看到这个图形你可能觉得很熟悉,确实是因为他和HashMap的图形结构是一样的,不同就在于Hashtable是线程安全的,也就是给put、get等方法添加了synchronized关键字,而这些关键字表现出来就是相当于给框1整个添加了一个锁,所以在多线程的情况下就要串行执行。
其中的EntryA、EntryI、EntryZ都是HashEntry[] table 数组中相应索引出的头结点元素,而EntryB、EntryC都是key的hash值相等所以形成的链表。
(2)、ConcurrentHashMap 图形结构:
这个图就是ConcurrentHashMap类的图形结构了你中我们看到有segment [ ] segments数组,其中存放的是一个个的segment(段)对象,而concurrentHashMap的加锁也是加在segment对象上而不是segment [] segments 数组上而且concurrenthashmap的get方法是不加锁的。
你发现没其实一个segment对象就是一个hashMap,上面的源代码中有列出来可以看看。所以说ConcurrentHashMap就是两个数组+链表。
第一次我看这个图的时候我也很懵逼,但看了源码在看图确实很清晰,所以如果你能看懂这个图,那说明你已经理解了ConcurrentHashMap的设计思路,也就明白了和Hashtable的区别。
四、Hashtable和ConcurrentHashMap的比较总结:
1、其实通过两张图就能大体上看到他们两个的区别,Hashtable是对整个hash表添加了锁而ConcurrentHashMap是对segment(段)进行加锁,每个段的锁不同,从而能提升并发度!
2、get方法的区别很大,Hashtable的get方法用synchronized 进行了修饰所以查询的时候也会有锁的竞争而ConcurrentHashMap就不一样了,因为他的get方法是没有加锁的
而是使用了volatile来修饰,volatile修饰的变量能保证变量在多线个线程中的可见性。
3、当然ConcurrentHashMap还有很多优秀的设计也CAS的使用,还没弄明白,明白了再来补充完善!
- Hashtable 、ConcurrentHashMap源码学习
- Hashtable、ConcurrentHashMap源码分析
- HashTable和ConcurrentHashMap的学习
- HashTable和ConcurrentHashMap学习笔记
- 深入Java集合源码学习系列:比较HashMap、Hashtable、TreeMap、ConcurrentHashMap、WeakHashMap性能
- memcached源码学习-hashtable
- memcached源码学习-hashtable
- Hashtable源码学习
- java 并发 ConcurrentHashMap 与 HashTable源码分析总结
- HashMap,Hashtable以及ConcurrentHashMap的比较(源码)
- 【Java数据结构】Hashmap、Hashtable、ConcurrentHashMap源码阅读笔记
- HashMap,Hashtable以及ConcurrentHashMap的比较(源码)
- java学习(3)concurrenthashmap hashtable hashmap copyonwritearraylist weakHashMap待续
- JDK源码学习(6)-ConcurrentHashMap代码学习
- Hashtable、synchronizedMap、ConcurrentHashMap 比较
- Hashtable、synchronizedMap、ConcurrentHashMap 比较
- Hashtable、synchronizedMap、ConcurrentHashMap 比较
- Hashtable、synchronizedMap、ConcurrentHashMap 比较
- 【解题报告】UVA10603 Fill BFS
- Leetcode学习(11)—— Island Perimeter
- vue2.0多条件搜索组件
- 【HDU 2438】 Turn the corner
- ubuntu16.04 切换 python 版本
- Hashtable 、ConcurrentHashMap源码学习
- java虚拟机--简单介绍
- BFS-1
- android电池剩余使用时间
- 539. Minimum Time Difference
- 二分图匹配——HDU 5943
- matlab面向对象教程【1】迷宫生成算法案例
- Mysql切换Oracle数据库
- Asp.net Core 打包发布 (Linux+Nginx)