concurrenthashmap介绍
concurrenthashmap是线程安全的哈希表。hashmap, hashtable, concurrenthashmap之间的关联如下:
hashmap是非线程安全的哈希表,常用于单线程程序中。
hashtable是线程安全的哈希表,它是通过synchronized来保证线程安全的;即,多线程通过同一个“对象的同步锁”来实现并发控制。hashtable在线程竞争激烈时,效率比较低(此时建议使用concurrenthashmap)!因为当一个线程访问hashtable的同步方法时,其它线程就访问hashtable的同步方法时,可能会进入阻塞状态。
concurrenthashmap是线程安全的哈希表,它是通过“锁分段”来保证线程安全的。concurrenthashmap将哈希表分成许多片段(segment),每一个片段除了保存哈希表之外,本质上也是一个“可重入的互斥锁”(reentrantlock)。多线程对同一个片段的访问,是互斥的;但是,对于不同片段的访问,却是可以同步进行的。
concurrenthashmap原理和数据结构
要想搞清concurrenthashmap,必须先弄清楚它的数据结构:
(01) concurrenthashmap继承于abstractmap抽象类。
(02) segment是concurrenthashmap中的内部类,它就是concurrenthashmap中的“锁分段”对应的存储结构。
concurrenthashmap与segment是组合关系,1个concurrenthashmap对象包含若干个segment对象。在代码中,这表现为concurrenthashmap类中存在“segment数组”成员。
(03) segment类继承于reentrantlock类,所以segment本质上是一个可重入的互斥锁。
(04) hashentry也是concurrenthashmap的内部类,是单向链表节点,存储着key-value键值对。segment与hashentry是组合关系,segment类中存在“hashentry数组”成员,“hashentry数组”中的每个hashentry就是一个单向链表。
对于多线程访问对一个“哈希表对象”竞争资源,hashtable是通过一把锁来控制并发;而concurrenthashmap则是将哈希表分成许多片段,对于每一个片段分别通过一个互斥锁来控制并发。concurrenthashmap对并发的控制更加细腻,它也更加适应于高并发场景!
concurrenthashmap函数列表
// 创建一个带有默认初始容量 (16)、加载因子 (0.75) 和 concurrencylevel (16) 的新的空映射。 concurrenthashmap() // 创建一个带有指定初始容量、默认加载因子 (0.75) 和 concurrencylevel (16) 的新的空映射。 concurrenthashmap(int initialcapacity) // 创建一个带有指定初始容量、加载因子和默认 concurrencylevel (16) 的新的空映射。 concurrenthashmap(int initialcapacity, float loadfactor) // 创建一个带有指定初始容量、加载因子和并发级别的新的空映射。 concurrenthashmap(int initialcapacity, float loadfactor, int concurrencylevel) // 构造一个与给定映射具有相同映射关系的新映射。 concurrenthashmap(map<? extends k,? extends v> m) // 从该映射中移除所有映射关系 void clear() // 一种遗留方法,测试此表中是否有一些与指定值存在映射关系的键。 boolean contains(object value) // 测试指定对象是否为此表中的键。 boolean containskey(object key) // 如果此映射将一个或多个键映射到指定值,则返回 true。 boolean containsvalue(object value) // 返回此表中值的枚举。 enumeration<v> elements() // 返回此映射所包含的映射关系的 set 视图。 set<map.entry<k,v>> entryset() // 返回指定键所映射到的值,如果此映射不包含该键的映射关系,则返回 null。 v get(object key) // 如果此映射不包含键-值映射关系,则返回 true。 boolean isempty() // 返回此表中键的枚举。 enumeration<k> keys() // 返回此映射中包含的键的 set 视图。 set<k> keyset() // 将指定键映射到此表中的指定值。 v put(k key, v value) // 将指定映射中所有映射关系复制到此映射中。 void putall(map<? extends k,? extends v> m) // 如果指定键已经不再与某个值相关联,则将它与给定值关联。 v putifabsent(k key, v value) // 从此映射中移除键(及其相应的值)。 v remove(object key) // 只有目前将键的条目映射到给定值时,才移除该键的条目。 boolean remove(object key, object value) // 只有目前将键的条目映射到某一值时,才替换该键的条目。 v replace(k key, v value) // 只有目前将键的条目映射到给定值时,才替换该键的条目。 boolean replace(k key, v oldvalue, v newvalue) // 返回此映射中的键-值映射关系数。 int size() // 返回此映射中包含的值的 collection 视图。 collection<v> values()
下面从concurrenthashmap的创建,获取,添加,删除这4个方面对concurrenthashmap进行分析。
1 创建
下面以concurrenthashmap(int initialcapacity,float loadfactor, int concurrencylevel)来进行说明。
@suppresswarnings("unchecked") public concurrenthashmap(int initialcapacity, float loadfactor, int concurrencylevel) { // 参数有效性判断 if (!(loadfactor > 0) || initialcapacity < 0 || concurrencylevel <= 0) throw new illegalargumentexception(); // concurrencylevel是“用来计算segments的容量” if (concurrencylevel > max_segments) concurrencylevel = max_segments; int sshift = 0; int ssize = 1; // ssize=“大于或等于concurrencylevel的最小的2的n次方值” while (ssize < concurrencylevel) { ++sshift; ssize <<= 1; } // 初始化segmentshift和segmentmask this.segmentshift = 32 - sshift; this.segmentmask = ssize - 1; // 哈希表的初始容量 // 哈希表的实际容量=“segments的容量” x “segments中数组的长度” if (initialcapacity > maximum_capacity) initialcapacity = maximum_capacity; // “哈希表的初始容量” / “segments的容量” int c = initialcapacity / ssize; if (c * ssize < initialcapacity) ++c; // cap就是“segments中的hashentry数组的长度” int cap = min_segment_table_capacity; while (cap < c) cap <<= 1; // segments 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; }
说明:
(01) 前面我们说过,concurrenthashmap采用了“锁分段”技术;在代码中,它通过“segments数组”对象来保存各个分段。segments的定义如下:
final segment<k,v>[] segments;
concurrencylevel的作用就是用来计算segments数组的容量大小。先计算出“大于或等于concurrencylevel的最小的2的n次方值”,然后将其保存为“segments的容量大小(ssize)”。
(02) initialcapacity是哈希表的初始容量。需要注意的是,哈希表的实际容量=“segments的容量” x “segments中数组的长度”。
(03) loadfactor是加载因子。它是哈希表在其容量自动增加之前可以达到多满的一种尺度。
concurrenthashmap的构造函数中涉及到的非常重要的一个结构体,它就是segment。下面看看segment的声明:
static final class segment<k,v> extends reentrantlock implements serializable { ... transient volatile hashentry<k,v>[] table; // threshold阈,是哈希表在其容量自动增加之前可以达到多满的一种尺度。 transient int threshold; // loadfactor是加载因子 final float loadfactor; segment(float lf, int threshold, hashentry<k,v>[] tab) { this.loadfactor = lf; this.threshold = threshold; this.table = tab; } ... }
说明:segment包含hashentry数组,hashentry保存了哈希表中的键值对。
此外,还需要说明的segment继承于reentrantlock。这意味着,segment本质上就是可重入的互斥锁。
hashentry的源码如下:
static final class hashentry<k,v> { final int hash; // 哈希值 final k key; // 键 volatile v value; // 值 volatile hashentry<k,v> next; // 下一个hashentry节点 hashentry(int hash, k key, v value, hashentry<k,v> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } .. }
说明:和hashmap的节点一样,hashentry也是链表。这就说明,concurrenthashmap是链式哈希表,它是通过“拉链法”来解决哈希冲突的。
2 获取
下面以get(object key)为例,对concurrenthashmap的获取方法进行说明。
public v get(object key) { segment<k,v> s; // manually integrate access methods to reduce overhead hashentry<k,v>[] tab; int h = hash(key); long u = (((h >>> segmentshift) & segmentmask) << sshift) + sbase; // 获取key对应的segment片段。 // 如果segment片段不为null,则在“segment片段的hashentry数组中”中找到key所对应的hashentry列表; // 接着遍历该hashentry链表,找到于key-value键值对对应的hashentry节点。 if ((s = (segment<k,v>)unsafe.getobjectvolatile(segments, u)) != null && (tab = s.table) != null) { 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; if ((k = e.key) == key || (e.hash == h && key.equals(k))) return e.value; } } return null; }
说明:get(object key)的作用是返回key在concurrenthashmap哈希表中对应的值。
它首先根据key计算出来的哈希值,获取key所对应的segment片段。
如果segment片段不为null,则在“segment片段的hashentry数组中”中找到key所对应的hashentry列表。segment包含“hashentry数组”对象,而每一个hashentry本质上是一个单向链表。
接着遍历该hashentry链表,找到于key-value键值对对应的hashentry节点。
下面是hash()的源码
private int hash(object k) { int h = hashseed; if ((0 != h) && (k instanceof string)) { return sun.misc.hashing.stringhash32((string) k); } h ^= k.hashcode(); // spread bits to regularize both segment and index locations, // using variant of single-word wang/jenkins hash. h += (h << 15) ^ 0xffffcd7d; h ^= (h >>> 10); h += (h << 3); h ^= (h >>> 6); h += (h << 2) + (h << 14); return h ^ (h >>> 16); }
3 增加
下面以put(k key, v value)来对concurrenthashmap中增加键值对来进行说明。
public v put(k key, v value) { segment<k,v> s; if (value == null) throw new nullpointerexception(); // 获取key对应的哈希值 int hash = hash(key); int j = (hash >>> segmentshift) & segmentmask; // 如果找不到该segment,则新建一个。 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); }
说明:
(01) put()根据key获取对应的哈希值,再根据哈希值找到对应的segment片段。如果segment片段不存在,则新增一个segment。
(02) 将key-value键值对添加到segment片段中。
final v put(k key, int hash, v value, boolean onlyifabsent) { // trylock()获取锁,成功返回true,失败返回false。 // 获取锁失败的话,则通过scanandlockforput()获取锁,并返回”要插入的key-value“对应的”hashentry链表“。 hashentry<k,v> node = trylock() ? null : scanandlockforput(key, hash, value); v oldvalue; try { // tab代表”当前segment中的hashentry数组“ hashentry<k,v>[] tab = table; // 根据”hash值“获取”hashentry数组中对应的hashentry链表“ int index = (tab.length - 1) & hash; hashentry<k,v> first = entryat(tab, index); for (hashentry<k,v> e = first;;) { // 如果”hashentry链表中的当前hashentry节点“不为null, if (e != null) { k k; // 当”要插入的key-value键值对“已经存在于”hashentry链表中“时,先保存原有的值。 // 若”onlyifabsent“为true,即”要插入的key不存在时才插入”,则直接退出; // 否则,用新的value值覆盖原有的原有的值。 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 { // 如果node非空,则将first设置为“node的下一个节点”。 // 否则,新建hashentry链表 if (node != null) node.setnext(first); else node = new hashentry<k,v>(hash, key, value, first); int c = count + 1; // 如果添加key-value键值对之后,segment中的元素超过阈值(并且,hashentry数组的长度没超过限制),则rehash; // 否则,直接添加key-value键值对。 if (c > threshold && tab.length < maximum_capacity) rehash(node); else setentryat(tab, index, node); ++modcount; count = c; oldvalue = null; break; } } } finally { // 释放锁 unlock(); } return oldvalue; }
说明:
put()的作用是将key-value键值对插入到“当前segment对应的hashentry中”,在插入前它会获取segment对应的互斥锁,插入后会释放锁。具体的插入过程如下:
(01) 首先根据“hash值”获取“当前segment的hashentry数组对象”中的“hashentry节点”,每个hashentry节点都是一个单向链表。
(02) 接着,遍历hashentry链表。
若在遍历hashentry链表时,找到与“要key-value键值对”对应的节点,即“要插入的key-value键值对”的key已经存在于hashentry链表中。则根据onlyifabsent进行判断,若onlyifabsent为true,即“当要插入的key不存在时才插入”,则不进行插入,直接返回;否则,用新的value值覆盖原始的value值,然后再返回。
若在遍历hashentry链表时,没有找到与“要key-value键值对”对应的节点。当node!=null时,即在scanandlockforput()获取锁时,已经新建了key-value对应的hashentry节点,则”将hashentry添加到segment中“;否则,新建key-value对应的hashentry节点,然后再“将hashentry添加到segment中”。 在”将hashentry添加到segment中“前,会判断是否需要rehash。如果在添加key-value键值之后,容量会超过阈值,并且hashentry数组的长度没有超过限制,则进行rehash;否则,直接通过setentryat()将key-value键值对添加到segment中。
在介绍rehash()和setentryat()之前,我们先看看自旋函数scanandlockforput()。下面是它的源码:
private hashentry<k,v> scanandlockforput(k key, int hash, v value) { // 第一个hashentry节点 hashentry<k,v> first = entryforhash(this, hash); // 当前的hashentry节点 hashentry<k,v> e = first; hashentry<k,v> node = null; // 重复计数(自旋计数器) int retries = -1; // negative while locating node // 查找”key-value键值对“在”hashentry链表上对应的节点“; // 若找到的话,则不断的自旋;在自旋期间,若通过trylock()获取锁成功则返回;否则自旋max_scan_retries次数之后,强制获取”锁“并退出。 // 若没有找到的话,则新建一个hashentry链表。然后不断的自旋。 // 此外,若在自旋期间,hashentry链表的表头发生变化;则重新进行查找和自旋工作! while (!trylock()) { hashentry<k,v> f; // to recheck first below // 1. retries<0的处理情况 if (retries < 0) { // 1.1 如果当前的hashentry节点为空(意味着,在该hashentry链表上上没有找到”要插入的键值对“对应的节点),而且node=null;则新建hashentry链表。 if (e == null) { if (node == null) // speculatively create node node = new hashentry<k,v>(hash, key, value, null); retries = 0; } // 1.2 如果当前的hashentry节点是”要插入的键值对在该hashentry上对应的节点“,则设置retries=0 else if (key.equals(e.key)) retries = 0; // 1.3 设置为下一个hashentry。 else e = e.next; } // 2. 如果自旋次数超过限制,则获取“锁”并退出 else if (++retries > max_scan_retries) { lock(); break; } // 3. 当“尝试了偶数次”时,就获取“当前segment的第一个hashentry”,即f。 // 然后,通过f!=first来判断“当前segment的第一个hashentry是否发生了改变”。 // 若是的话,则重置e,first和retries的值,并重新遍历。 else if ((retries & 1) == 0 && (f = entryforhash(this, hash)) != first) { e = first = f; // re-traverse if entry changed retries = -1; } } return node; }
说明:
scanandlockforput()的目标是获取锁。流程如下:
它首先会调用entryforhash(),根据hash值获取”当前segment中对应的hashentry节点(first),即找到对应的hashentry链表“。
紧接着进入while循环。在while循环中,它会遍历”hashentry链表(e)“,查找”要插入的key-value键值对“在”该hashentry链表上对应的节点“。
若找到的话,则不断的自旋,即不断的执行while循环。在自旋期间,若通过trylock()获取锁成功则返回;否则,在自旋max_scan_retries次数之后,强制获取锁并退出。
若没有找到的话,则新建一个hashentry链表,然后不断的自旋。在自旋期间,若通过trylock()获取锁成功则返回;否则,在自旋max_scan_retries次数之后,强制获取锁并退出。
此外,若在自旋期间,hashentry链表的表头发生变化;则重新进行查找和自旋工作!
理解scanandlockforput()时,务必要联系”哈希表“的数据结构。一个segment本身就是一个哈希表,segment中包含了”hashentry数组“对象,而每一个hashentry对象本身是一个”单向链表“。
下面看看rehash()的实现代码。
private void rehash(hashentry<k,v> node) { hashentry<k,v>[] oldtable = table; // ”segment中原始的hashentry数组的长度“ int oldcapacity = oldtable.length; // ”segment中新hashentry数组的长度“ int newcapacity = oldcapacity << 1; // 新的阈值 threshold = (int)(newcapacity * loadfactor); // 新的hashentry数组 hashentry<k,v>[] newtable = (hashentry<k,v>[]) new hashentry[newcapacity]; int sizemask = newcapacity - 1; // 遍历”原始的hashentry数组“, // 将”原始的hashentry数组“中的每个”hashentry链表“的值,都复制到”新的hashentry数组的hashentry元素“中。 for (int i = 0; i < oldcapacity ; i++) { // 获取”原始的hashentry数组“中的”第i个hashentry链表“ hashentry<k,v> e = oldtable[i]; if (e != null) { hashentry<k,v> next = e.next; int idx = e.hash & sizemask; 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; // 将”原始的hashentry数组“中的”hashentry链表(e)“的值,都复制到”新的hashentry数组的hashentry“中。 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]; newtable[k] = new hashentry<k,v>(h, p.key, v, n); } } } } // 将新的node节点添加到“segment的新hashentry数组(newtable)“中。 int nodeindex = node.hash & sizemask; // add the new node node.setnext(newtable[nodeindex]); newtable[nodeindex] = node; table = newtable; }
说明:rehash()的作用是将”segment的容量“变为”原始的segment容量的2倍“。
在将原始的数据拷贝到“新的segment”中后,会将新增加的key-value键值对添加到“新的segment”中。
setentryat()的源码如下:
static final <k,v> void setentryat(hashentry<k,v>[] tab, int i, hashentry<k,v> e) { unsafe.putorderedobject(tab, ((long)i << tshift) + tbase, e); }
unsafe是segment类中定义的“静态sun.misc.unsafe”对象。源码如下:
static final sun.misc.unsafe unsafe;
unsafe.java在openjdk6中的路径是:openjdk6/jdk/src/share/classes/sun/misc/unsafe.java。其中,putorderedobject()的源码下:
public native void putorderedobject(object o, long offset, object x);
说明:putorderedobject()是一个本地方法。
它会设置obj对象中offset偏移地址对应的object型field的值为指定值。它是一个有序或者有延迟的putobjectvolatile()方法,并且不保证值的改变被其他线程立即看到。只有在field被volatile修饰并且期望被意外修改的时候,使用putorderedobject()才有用。
总之,setentryat()的目的是设置tab中第i位置元素的值为e,且该设置会有延迟。
4 删除
下面以remove(object key)来对concurrenthashmap中的删除操作来进行说明。
public v remove(object key) { int hash = hash(key); // 根据hash值,找到key对应的segment片段。 segment<k,v> s = segmentforhash(hash); return s == null ? null : s.remove(key, hash, null); }
说明:remove()首先根据“key的计算出来的哈希值”找到对应的segment片段,然后再从该segment片段中删除对应的“key-value键值对”。
remove()的方法如下:
final v remove(object key, int hash, object value) { // 尝试获取segment对应的锁。 // 尝试失败的话,则通过scanandlock()来获取锁。 if (!trylock()) scanandlock(key, hash); v oldvalue = null; try { // 根据“hash值”找到“segment的hashentry数组”中对应的“hashentry节点(e)”,该hashentry节点是一hashentry个链表。 hashentry<k,v>[] tab = table; int index = (tab.length - 1) & hash; hashentry<k,v> e = entryat(tab, index); hashentry<k,v> pred = null; // 遍历“hashentry链表”,删除key-value键值对 while (e != null) { k k; hashentry<k,v> next = e.next; if ((k = e.key) == key || (e.hash == hash && key.equals(k))) { v v = e.value; if (value == null || value == v || value.equals(v)) { if (pred == null) setentryat(tab, index, next); else pred.setnext(next); ++modcount; --count; oldvalue = v; } break; } pred = e; e = next; } } finally { // 释放锁 unlock(); } return oldvalue; }
说明:remove()的目的就是删除key-value键值对。在删除之前,它会获取到segment的互斥锁,在删除之后,再释放锁。
它的删除过程也比较简单,它会先根据hash值,找到“segment的hashentry数组”中对应的“hashentry”节点。根据segment的数据结构,我们知道segment中包含一个hashentry数组对象,而每一个hashentry本质上是一个单向链表。 在找到“hashentry”节点之后,就遍历该“hashentry”节点对应的链表,找到key-value键值对对应的节点,然后删除。
下面对scanandlock()进行说明。它的源码如下:
private void scanandlock(object key, int hash) { // 第一个hashentry节点 hashentry<k,v> first = entryforhash(this, hash); hashentry<k,v> e = first; int retries = -1;
// 查找”key-value键值对“在”hashentry链表上对应的节点“; // 无论找没找到,最后都会不断的自旋;在自旋期间,若通过trylock()获取锁成功则返回;否则自旋max_scan_retries次数之后,强制获取”锁“并退出。 // 若在自旋期间,hashentry链表的表头发生变化;则重新进行查找和自旋! while (!trylock()) { hashentry<k,v> f; if (retries < 0) { // 如果“遍历完该hashentry链表,仍然没找到”要删除的键值对“对应的节点” // 或者“在该hashentry链表上找到”要删除的键值对“对应的节点”,则设置retries=0 // 否则,设置e为下一个hashentry节点。 if (e == null || key.equals(e.key)) retries = 0; else e = e.next; } // 自旋超过限制次数之后,获取锁并退出。 else if (++retries > max_scan_retries) { lock(); break; } // 当“尝试了偶数次”时,就获取“当前segment的第一个hashentry”,即f。 // 然后,通过f!=first来判断“当前segment的第一个hashentry是否发生了改变”。 // 若是的话,则重置e,first和retries的值,并重新遍历。 else if ((retries & 1) == 0 && (f = entryforhash(this, hash)) != first) { e = first = f; retries = -1; } } }
说明:scanandlock()的目标是获取锁。它的实现与scanandlockforput()类似,这里就不再过多说明。
总结:concurrenthashmap是线程安全的哈希表,它是通过“锁分段”来实现的。concurrenthashmap中包括了“segment(锁分段)数组”,每个segment就是一个哈希表,而且也是可重入的互斥锁。第一,segment是哈希表表现在,segment包含了“hashentry数组”,而“hashentry数组”中的每一个hashentry元素是一个单向链表。即segment是通过链式哈希表。第二,segment是可重入的互斥锁表现在,segment继承于reentrantlock,而reentrantlock就是可重入的互斥锁。
对于concurrenthashmap的添加,删除操作,在操作开始前,线程都会获取segment的互斥锁;操作完毕之后,才会释放。而对于读取操作,它是通过volatile去实现的,hashentry数组是volatile类型的,而volatile能保证“即对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入”,即我们总能读到其它线程写入hashentry之后的值。 以上这些方式,就是concurrenthashmap线程安全的实现原理。
concurrenthashmap示例
下面,我们通过一个例子去对比hashmap和concurrenthashmap。
import java.util.*; import java.util.concurrent.*; /* * concurrenthashmap是“线程安全”的哈希表,而hashmap是非线程安全的。 * * 下面是“多个线程同时操作并且遍历map”的示例 * () 当map是concurrenthashmap对象时,程序能正常运行。 * (02) 当map是hashmap对象时,程序会产生concurrentmodificationexception异常。 * * */ public class concurrenthashmapdemo1 { // todo: map是hashmap对象时,程序会出错。 //private static map<string, string> map = new hashmap<string, string>(); private static map<string, string> map = new concurrenthashmap<string, string>(); public static void main(string[] args) { // 同时启动两个线程对map进行操作! new mythread("ta").start(); new mythread("tb").start(); } private static void printall() { string key, value; iterator iter = map.entryset().iterator(); while(iter.hasnext()) { map.entry entry = (map.entry)iter.next(); key = (string)entry.getkey(); value = (string)entry.getvalue(); system.out.print(key+" - "+value+", "); } system.out.println(); } private static class mythread extends thread { mythread(string name) { super(name); } @override public void run() { int i = 0; while (i++ < 6) { // “线程名” + "-" + "序号" string val = thread.currentthread().getname()+i; map.put(string.valueof(i), val); // 通过“iterator”遍历map。 printall(); } } } }
(某一次)运行结果:
1 - tb1, 1 - tb1, 1 - tb1, 1 - tb1, 2 - tb2, 2 - tb2, 1 - tb1, 3 - ta3, 1 - tb1, 2 - tb2, 3 - tb3, 1 - tb1, 2 - tb2, 3 - tb3, 1 - tb1, 4 - tb4, 3 - tb3, 2 - tb2, 4 - tb4, 1 - tb1, 2 - tb2, 5 - ta5, 1 - tb1, 3 - tb3, 5 - tb5, 4 - tb4, 3 - tb3, 2 - tb2, 4 - tb4, 1 - tb1, 2 - tb2, 5 - tb5, 1 - tb1, 6 - tb6, 5 - tb5, 3 - tb3, 6 - tb6, 4 - tb4, 3 - tb3, 2 - tb2, 4 - tb4, 2 - tb2,
结果说明:如果将源码中的map改成hashmap对象时,程序会产生concurrentmodificationexception异常。
以上所述是小编给大家介绍的java concurrency集合之concurrenthashmap,希望对大家有所帮助
如对本文有疑问, 点击进行留言回复!!
CONMISANMA的正确答案——关于微信支付的退款问题【java】
网友评论