Java 中的 ConcurrentHashMap
先决条件: ConcurrentMap
ConcurrentHashMap 类被引入到 JDK 1.5 中,属于 java.util.concurrent 包,它实现了 ConcurrentMap 接口和 Serializable 接口。ConcurrentHashMap 是一个 HashMap 的增强版,我们知道,在应用程序中处理线程时,HashMap 在性能方面并不是一个很好的选择,因为 HashMap 的表现不佳。
ConcurrentHashMap 是 Java 中 Map 接口的线程安全实现,这意味着多个线程可以同时访问它而不会出现任何同步问题。它是 java.util.concurrent 包的一部分,并在 Java 5 中作为传统的 HashMap 类的可扩展替代品被引入。
ConcurrentHashMap 的一个关键特征是它提供了细粒度锁定,这意味着它仅锁定正在修改的部分而不是整个映射。这使得它在并发操作方面具有高度的可扩展性和效率。此外,ConcurrentHashMap 还提供了各种方法,用于原子操作,如 putIfAbsent(),replace() 和 remove()。
这是一个使用 ConcurrentHashMap 的简单示例:
import java.util.concurrent.ConcurrentHashMap;
public class Main {
public static void main(String[] args) {
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
//向映射中添加元素
map.put("A", 1);
map.put("B", 2);
map.put("C", 3);
System.out.println("映射的大小:" + map.size());
//从映射中获取值
int valueA = map.get("A");
System.out.println("A 的值:" + valueA);
//从映射中移除元素
map.remove("B");
System.out.println("映射的大小:" + map.size());
}
}
输出结果:
映射的大小:3
A 的值:1
映射的大小:2
ConcurrentHashMap 的关键点:
- ConcurrentHashMap 的底层数据结构是 Hashtable。
- ConcurrentHashMap 类是线程安全的,即多个线程可以在单个对象上操作而没有任何复杂性。
- 同时可以有任意数量的线程适用于读操作,而不锁定 ConcurrentHashMap 对象,在 HashMap 中不存在这种情况。
- 在 ConcurrentHashMap 中,对象根据并发级别分成多个段。
- ConcurrentHashMap 的默认并发级别为 16。
- 在 ConcurrentHashMap 中,可以同时对任意数量的线程执行检索操作,但是要更新对象,则线程必须锁定要操作的特定段。这种锁定机制称为 段锁定或桶锁定 。因此,线程可以执行 16 个更新操作。
- 在 ConcurrentHashMap 中,不能插入空对象作为键或值。
声明:
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable
这里, K 是键 Object 类型, V 是值 Object 类型。
ConcurrentHashMap 的层次结构:

它实现了 **Serializable、ConcurrentMap <K, V> 、Map<K, V> ** 接口,并扩展了 **AbstractMap <K, V> ** 类。
ConcurrentHashMap 的构造函数:
- 并发级别(Concurrency-Level): 是指同时更新映射表的线程数。实现会进行内部大小调整,以尽力容纳这些线程。
- 负载因子(Load-Factor): 用于控制重分配的阈值。
- 初始容量(Initial Capacity): 实现最初提供给元素的一定数量设施。若此映射表的容量为10,即可存储10个条目。
1. ConcurrentHashMap() : 创建具有默认初始容量(16)、默认负载因子(0.75)和默认并发级别(16)的新空映射表。
ConcurrentHashMap<K, V> chm = new ConcurrentHashMap<>();
2. ConcurrentHashMap(int initialCapacity) : 创建具有指定初始容量和默认负载因子(0.75)和默认并发级别(16)的新空映射表。
ConcurrentHashMap<K, V> chm = new ConcurrentHashMap<>(int initialCapacity);
3. ConcurrentHashMap(int initialCapacity, float loadFactor) : 创建具有指定初始容量和负载因子以及默认并发级别(16)的新空映射表。
ConcurrentHashMap<K, V> chm = new ConcurrentHashMap<>(int initialCapacity, float loadFactor);
4. ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) : 创建具有指定初始容量、负载因子和并发级别的新空映射表。
ConcurrentHashMap<K, V> chm = new ConcurrentHashMap<>(int initialCapacity, float loadFactor, int concurrencyLevel);
5. ConcurrentHashMap(Map m) : 创建一个具有给定映射的相同映射表。
ConcurrentHashMap<K, V> chm = new ConcurrentHashMap<>(Map m);
示例:
// Java program to demonstrate working of ConcurrentHashMap
import java.util.concurrent.*;
class ConcurrentHashMapDemo {
public static void main(String[] args)
{
// create an instance of
// ConcurrentHashMap
ConcurrentHashMap<Integer, String> m
= new ConcurrentHashMap<>();
// Insert mappings using
// put method
m.put(100, "Hello");
m.put(101, "Geeks");
m.put(102, "Geeks");
// Here we cant add Hello because 101 key
// is already present in ConcurrentHashMap object
m.putIfAbsent(101, "Hello");
// We can remove entry because 101 key
// is associated with For value
m.remove(101, "Geeks");
// Now we can add Hello
m.putIfAbsent(103, "Hello");
// We cant replace Hello with For
m.replace(101, "Hello", "For");
System.out.println(m);
}
}
输出(Output)
{100=Hello, 102=Geeks, 103=Hello}
ConcurrentHashMap的基本操作
1. 添加元素
要向ConcurrentHashMap添加映射,可以使用put()或putAll()方法。下面的示例代码解释了这两种方法。 2. 查找元素
要在ConcurrentHashMap中查找元素,可以使用get()方法。如果该键不存在,则返回null值,如果存在,则返回映射值。
3. 删除元素
要删除ConcurrentHashMap中的元素,可以使用remove()方法。如果存在映射,则返回映射值。如果不存在映射,则返回null值。
4. 更新元素
要更新ConcurrentHashMap中的元素,可以使用replace()方法。如果键存在,则用新的值替换旧的值,并返回旧的值。如果键不存在,则返回null值。
5. 遍历元素
可以使用迭代器来遍历ConcurrentHashMap中的元素。迭代器可以通过使用keySet()、values()或entrySet()方法获得。
// Java程序,演示添加元素到ConcurrentHashMap
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class AddingElementsToConcuurentHashMap {
public static void main(String[] args)
{
// 创建ConcurrentHashMap
ConcurrentHashMap<String, String> my_cmmap
= new ConcurrentHashMap<String, String>();
// 使用put()方法将元素添加到映射中
my_cmmap.put("1", "1");
my_cmmap.put("2", "1");
my_cmmap.put("3", "1");
my_cmmap.put("4", "1");
my_cmmap.put("5", "1");
my_cmmap.put("6", "1");
// 打印映射
System.out.println("Mappings of my_cmmap : "
+ my_cmmap);
// 创建另一个ConcurrentHashMap
ConcurrentHashMap<String, String> new_chm
= new ConcurrentHashMap<>();
// 将my_cmmap中的映射复制到新映射中
new_chm.putAll(my_cmmap);
// 显示新映射
System.out.println("New mappings are: " + new_chm);
}
}
输出结果
Mappings of my_cmmap : {1=1, 2=1, 3=1, 4=1, 5=1, 6=1}
New mappings are: {1=1, 2=1, 3=1, 4=1, 5=1, 6=1}
2. 删除元素
要删除映射,我们可以使用ConcurrentHashmap类的remove(Object key)方法。如果key不存在于映射中,则此函数不执行任何操作。要清除整个映射,我们可以使用clear()方法。
// Java程序,演示从ConcurrentHashMap中删除元素
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class RemoveElementsFromConcurrentHashMap {
public static void main(String[] args)
{
// 创建ConcurrentHashMap
Map<String, String> my_cmmap
= new ConcurrentHashMap<String, String>();
// 使用put()方法将元素添加到映射中
my_cmmap.put("1", "1");
my_cmmap.put("2", "1");
my_cmmap.put("3", "1");
my_cmmap.put("4", "1");
my_cmmap.put("5", "1");
my_cmmap.put("6", "1");
// 打印映射
System.out.println("Map: " + my_cmmap);
System.out.println();
// 使用remove()方法删除具有现有键6的映射
String valueRemoved = my_cmmap.remove("6");
// 删除后打印映射
System.out.println(
"After removing mapping with key 6:");
System.out.println("Map: " + my_cmmap);
System.out.println("Value removed: "
+ valueRemoved);
System.out.println();
// 使用remove()方法删除具有不存在键10的映射
valueRemoved = my_cmmap.remove("10");
// 删除后打印映射
System.out.println(
"After removing mapping with key 10:");
System.out.println("Map: " + my_cmmap);
System.out.println("Value removed: "
+ valueRemoved);
System.out.println();
// 现在使用clear()方法清除映射
my_cmmap.clear();
// 打印清理后的映Map
System.out.println("Map after use of clear(): "
+ my_cmmap);
}
}
输出结果
Map: {1=1, 2=1, 3=1, 4=1, 5=1, 6=1}
After removing mapping with key 6:
Map: {1=1, 2=1, 3=1, 4=1, 5=1}
Value removed: 1
After removing mapping with key 10:
Map: {1=1, 2=1, 3=1, 4=1, 5=1}
Value removed: null
Map after use of clear(): {}
3. 访问元素
我们可以使用get()方法访问ConcurrentHashMap的元素,下面给出了一个例子。
// Java程序演示访问ConcurrentHashMap的元素
import java.util.concurrent.*;
class AccessingElementsOfConcurrentHashMap {
public static void main(String[] args)
{
// 创建ConcurrentHashMap实例
ConcurrentHashMap chm
= new ConcurrentHashMap();
// 使用put方法插入映射
chm.put(100, "Geeks");
chm.put(101, "for");
chm.put(102, "Geeks");
chm.put(103, "Contribute");
// 显示HashMap
System.out.println("Mappings are: ");
System.out.println(chm);
// 显示100的值
System.out.println("Value associated to "
+ "100 is : " + chm.get(100));
// 获取103的值
System.out.println("Value associated to "
+ "103 is : " + chm.get(103));
}
}
输出
The Mappings are:
{100=Geeks, 101=for, 102=Geeks, 103=Contribute}
The Value associated to 100 is : Geeks
The Value associated to 103 is : Contribute
4. 遍历
我们可以使用Iterator接口遍历Collection Framework的任何结构。由于迭代器使用一种类型的数据,我们使用Entry<?, ?>将两种分离的类型解决成兼容格式。然后使用next()方法打印ConcurrentHashMap的元素。
// Java程序遍历ConcurrentHashMap
import java.util.*;
import java.util.concurrent.*;
public class TraversingConcurrentHashMap {
public static void main(String[] args)
{
// 创建ConcurrentHashMap实例
ConcurrentHashMap chmap
= new ConcurrentHashMap();
// 使用put()添加元素
chmap.put(8, "Third");
chmap.put(6, "Second");
chmap.put(3, "First");
chmap.put(11, "Fourth");
// 创建ConcurrentHashMap的迭代器
Iterator>
itr = chmap.entrySet().iterator();
// 使用hasNext()方法检查是否有下一个元素
// 使用next()方法检索下一个元素
while (itr.hasNext()) {
ConcurrentHashMap.Entry entry
= itr.next();
System.out.println("Key = " + entry.getKey()
+ ", Value = "
+ entry.getValue());
}
}
}
输出
Key = 3, Value = First
Key = 6, Value = Second
Key = 8, Value = Third
Key = 11, Value = Fourth
方法 of ConcurrentHashMap
- K - map中键的类型。
- V - 映射在map中的值的类型。
METHOD | DESCRIPTION
—|—
clear() | 从 map 中移除所有的映射。
compute(K key, BiFunction<? super K,? super V,? extends V> remappingFunction) | 尝试计算指定键的映射及其当前映射值(如果当前没有映射值则为 null)。
computeIfAbsent(K key, Function<? super K,? extends V> mappingFunction) | 如果指定的键尚未与值关联,则尝试使用给定的映射函数计算其值并将其输入到此映射中(除非为 null)。
computeIfPresent(K key, BiFunction<? super K,? super V,? extends V> remappingFunction) | 如果存在指定键的值,则尝试根据键和其当前映射的值计算新的映射。
contains(Object value) | 在此表中测试是否有一些键映射到指定的值。
containsKey(Object key) | 测试指定的对象是否为此表中的键。
containsValue(Object value) | 如果此 map 将一个或多个键映射到指定的值,则返回 true。
elements() | 返回此表中值的枚举。
entrySet() | 返回此 map 中包含的映射的 Set 视图。
equals(Object o) | 将指定对象与此 map 进行比较以测试相等性。
forEach(long parallelismThreshold, BiConsumer<? super K,? super V> action) | 对于每一个(key,value)执行给定的操作。
forEach(long parallelismThreshold, BiFunction<? super K,? super V,? extends U> transformer, Consumer<? super U> action) | 对于每一个非 null 变换(key,value),执行给定的操作。
forEachEntry(long parallelismThreshold, Consumer<? super Map.Entry<K,V>> action) | 对于每一个 entry,执行给定的操作。
forEachEntry(long parallelismThreshold, Function<Map.Entry<K,V>,? extends U> transformer, Consumer<? super U> action) | 对于每一个非 null 变换的 entry,执行给定的操作。
forEachKey(long parallelismThreshold, Consumer<? super K> action) | 对每个键执行给定的操作。
forEachKey(long parallelismThreshold, Function<? super K,? extends U> transformer, Consumer<? super U> action) | 对于每一个非 null 变换后的 key,执行给定的操作。
forEachValue(long parallelismThreshold, Consumer<? super V> action) | 对每个 value执行给定的操作。
forEachValue(long parallelismThreshold, Function<? super V,? extends U> transformer, Consumer<? super U> action) | 对于每一个非 null 变换后的 value,执行给定的操作。
get(Object key) | 返回指定键所映射的值,如果此 map 中未映射该键,则返回 null。
getOrDefault(Object key, V defaultValue) | 返回指定键所映射的值,如果此 map 中未映射该键,则返回指定的默认值。
hashCode() | 返回此 map 的哈希码。
isEmpty() | 如果此 map 不包含任何映射,则返回 true。
keySet() | 返回此 map 中包含的键的 Set 视图。
merge(K key, V value, BiFunction<? super V,? super V,? extends V> remappingFunction) | 如果指定的键还没有与值相关联或已经与 null 关联,则将指定的键与给定的非 null 值关联。
put(K key, V value) | 将指定的值与此 map 中的指定键相关联。
putAll(Map<? extends K,? extends V> m) | 将指定 map 中的所有映射复制到此 map 中。
putIfAbsent(K key, V value) | 如果指定的键尚未与值关联(或映射为 null),则将其与指定的值相关联并返回 null,否则返回当前值。
reduce(long parallelismThreshold, BiFunction<? super K,? super V,V> transformer, BiFunction<V,V,? extends V> reducer) | 对于每一对(key, value)执行给定的变换,并在所有非 null 转换的值上执行给定的 reduce 操作。
reduceEntries(long parallelismThreshold, Function<Map.Entry<K,V>,? extends R> transformer, BiFunction<R,R,? extends R> reducer) | 对于每一个 entry 执行给定的变换,并在所有非 null 转换的结果上执行给定的 reduce 操作。
reduceKeys(long parallelismThreshold, Function<? super K,? extends U> transformer, BiFunction<U,U,? extends U> reducer) | 对于每个键执行给定的变换,并在所有非 null 转换的值上执行给定的 reduce 操作。
reduceValues(long parallelismThreshold, Function<? super V,? extends U> transformer, BiFunction<U,U,? extends U> reducer) | 对于每个 value 执行给定的变换,并在所有非 null 转换的值上执行给定的 reduce 操作。
remove(Object key) | 如果存在键的映射关系,则将其从此 map 中移除。
remove(Object key, Object value) | 仅当指定键的当前映射为指定值时,才从此 map 中移除该键的映射关系。
replace(K key, V value) | 只有在当前映射到指定值时,才替换指定键的当前映射。
replace(K key, V oldValue, V newValue) | 仅当当前映射到指定值时,才替换指定键的当前映射。
replaceAll(BiFunction<? super K,? super V,? extends V> function) | 将此 map 中的每个映射替换为通过给定映射函数计算的新值。
size() | 返回此 map 中的键-值映射关系数。
toString() | 返回此 map 的字符串表示形式。
values() | 返回此 map 中包含的值的 Collection 视图。
keys() | 返回此表中键的枚举。
keySet() | 返回包含此映射中所包含的键的Set视图。
keySet(V mappedValue) | 使用给定的常映射值返回此映射中的键的Set视图(即,对于任何添加,使用Collection.add(E)和Collection.addAll(Collection))。
mappingCount() | 返回映射数。
merge(K key, V value, BiFunction<? super V,? super V,? extends V> remappingFunction) | 如果指定的键没有与非空值关联,则将其与给定值相关联。
newKeySet() | 从给定类型到Boolean.TRUE创建一个ConcurrentHashMap支持的新Set。
newKeySet(int initialCapacity) | 从给定类型到Boolean.TRUE创建一个支持的新ConcurrentHashMap Set。
put(K key, V value) | 在此表中将指定键映射到指定值。
putAll(Map<? extends K,? extends V> m) | 将指定映射中的所有映射复制到此映射中。
putIfAbsent(K key, V value) | 如果指定的键还没有与值相关联,则将其与给定的值相关联。
reduce(long parallelismThreshold, BiFunction<? super K,? super V,? extends U> transformer, BiFunction<? super U,? super U,? extends U> reducer) | 使用给定的规约器组合所有(键,值)对的给定转换的结果的累积结果,如果没有,则返回null。
reduceEntries(long parallelismThreshold, BiFunction<Map.Entry<K,V>,Map.Entry<K,V>,? extends Map.Entry<K,V>> reducer) | 使用给定的规约器组合所有条目的结果的累积结果,如果没有,则返回null。
reduceEntries(long parallelismThreshold, Function<Map.Entry<K,V>,? extends U> transformer, BiFunction<? super U,? super U,? extends U> reducer) | 使用给定的规约器组合所有条目的给定转换的结果的累积结果,如果没有,则返回null。
reduceEntriesToDouble(long parallelismThreshold, ToDoubleFunction<Map.Entry<K,V>> transformer, double basis, DoubleBinaryOperator reducer) | 使用给定的规约器组合所有条目的给定转换的结果的累积结果,以及给定的基础值作为标识值。
reduceEntriesToInt(long parallelismThreshold, ToIntFunction<Map.Entry<K,V>> transformer, int basis, IntBinaryOperator reducer) | 使用给定的规约器组合所有条目的给定转换的结果的累积结果,以及给定的基础值作为标识值。
reduceEntriesToLong(long parallelismThreshold, ToLongFunction<Map.Entry<K,V>> transformer, long basis, LongBinaryOperator reducer) | 使用给定的规约器组合所有条目的给定转换的结果的累积结果,以及给定的基础值作为标识值。
reduceKeys(long parallelismThreshold, BiFunction<? super K,? super K,? extends K> reducer) | 使用给定的规约器组合所有键的结果的累积结果。
reduceValuesToLong(long parallelismThreshold, ToLongFunction<? super V> transformer, long basis, LongBinaryOperator reducer) 返回通过使用给定的reducer组合值来累积所有值的结果,给定的基值作为标识值,或者如果没有则返回null。
| 方法 | 描述 |
|---|---|
| reduceValuesToLong(long parallelismThreshold, ToLongFunction<? super V> transformer, long basis, LongBinaryOperator reducer) | 使用给定的归约器(reducer)和给定的基值(basis)对所有值进行给定的转换(transformation)的累计,并返回结果。 |
| remove(Object key) | 从此映射中删除键和其对应的值。 |
| remove(Object key, Object value) | 仅在当前映射到给定值时,删除该键的条目。 |
| replace(K key, V value) | 仅当当前映射到某个值时,替换键的条目。 |
| replace(K key, V oldValue, V newValue) | 仅在当前映射到给定值时,替换该键的条目。 |
| search(long parallelismThreshold, BiFunction<? super K,? super V,? extends U> searchFunction) | 在每个键值对上应用给定的搜索函数,并返回非空结果,否则返回null。 |
| searchEntries(long parallelismThreshold, Function<Map.Entry<K,V>,? extends U> searchFunction) | 在每个条目上应用给定的搜索函数,并返回非空结果,否则返回null。 |
| searchKeys(long parallelismThreshold, Function<? super K,? extends U> searchFunction) | 在每个键上应用给定的搜索函数,并返回非空结果,否则返回null。 |
| searchValues(long parallelismThreshold, Function<? super V,? extends U> searchFunction) | 在每个值上应用给定的搜索函数,并返回非空结果,否则返回null。 |
| toString() | 返回此映射的字符串表示形式。 |
| values() | 返回此映射中所包含的值的Collection视图。 |
在java.util.AbstractMap类中声明的方法
| 方法 | 描述 |
|---|---|
| clone() | 返回此AbstractMap实例的浅拷贝:键和值本身没有被克隆。 |
| isEmpty() | 如果此映射不包含键值映射,则返回true。 |
| size() | 返回此映射中的键值映射数。 |
在java.util.concurrent.ConcurrentMap接口中声明的方法
| 方法 | 描述 |
|---|---|
| forEach(BiConsumer<? super K,? super V> action) | 对此映射中的每个条目执行给定的操作,直到处理完所有条目或操作引发异常。 |
| replaceAll(BiFunction<? super K,? super V,? extends V> function) | 用调用该条目上给定函数的结果替换每个条目的值,直到处理完所有条目或函数引发异常。 |
ConcurrentHashMap vs Hashtable
HashTable
- Hashtable 是Map数据结构的一种实现
- 这是一个受遗留的类,在Hashtable实例上,它的所有方法都使用synchronized关键字进行同步。
- 线程安全,因为其方法是同步的
ConcurrentHashMap
- ConcurrentHashMap 实现Map数据结构,也像Hashtable一样提供线程安全。
- 它通过将完整的哈希表数组分成段或部分,并允许对这些段进行并行访问来工作。
- 在哈希图桶水平上的锁定控制更精细。
- 在应用程序中需要非常高的并发性时,请使用ConcurrentHashMap。
- 它是一种无需同步整个映射即可实现线程安全的数据结构。
- 读取可以非常快,而写入则使用对段级或桶级别进行锁定。
- 对象级别上没有锁定。
- 如果一个线程尝试在另一个线程迭代它时修改它,则ConcurrentHashMap不会引发ConcurrentModificationException。
- ConcurrentHashMap不允许NULL值,因此在ConcurrentHashMap中键不能为空。
- 如果一个线程尝试在另一个线程迭代它时修改它,则ConcurrentHashMap不会引发ConcurrentModificationException。
| 属性 | Hashtable(散列表) | ConcurrentHashMap(并发哈希表) |
|---|---|---|
| 创建方式 | Map ht = new Hashtable(); | Map chm = new ConcurrentHashMap(); |
| 是否允许 null 键值 | 否 | 否 |
| 是否允许 null 值 | 否 | 不允许(不允许 null 键和值) |
| 线程安全性 | 是 | 是。通过为不同的桶提供单独的锁以确保线程安全性,从而获得更好的性能。提供并发读取访问,无需任何阻塞,从而进一步提高性能。 |
| 性能 | 由于同步开销较大,性能较慢。 | 比Hashtable更快。当读取操作比写入操作更多时,ConcurrentHashMap是更好的选择。 |
| 迭代器 | Hashtable使用枚举器来迭代Hashtable对象的值。Hashtable方法keys和elements返回的枚举不具有快速失败特性。 | 安全故障迭代器:ConcurrentHashMap提供的迭代器是安全故障迭代器,这意味着它不会抛出ConcurrentModificationException异常。 |
结论:
如果需要线程安全的高并发实现,则建议使用ConcurrentHashMap,而不是Hashtable。
ConcurrentHashMap的优点:
- 线程安全:ConcurrentHashMap被设计为支持多个线程同时访问,使其成为需要处理并发访问数据的应用程序的理想选择。
- 细粒度锁:与其他同步机制锁定整个数据结构不同,ConcurrentHashMap使用细粒度锁仅锁定正在修改的映射的部分。这使得它在并发操作中高度可扩展和高效。
- 原子操作:ConcurrentHashMap提供了多种原子操作方法,如putIfAbsent()、replace()和remove(),可以用于实现复杂的并发算法。
- 高性能:由于其细粒度锁机制,即使在重度并发访问下,ConcurrentHashMap也能实现高性能。
ConcurrentHashMap的缺点:
- 内存开销更大:ConcurrentHashMap使用的细粒度锁机制需要比其他同步机制更多的内存开销。
- 复杂性:ConcurrentHashMap使用的细粒度锁机制可能会使代码更加复杂,特别是对于不熟悉并发编程的开发人员来说。
极客教程