序
本文介绍一下提升并发可伸缩性的一些方式:减少锁的持有时间,降低锁的粒度,锁分段、避免热点域以及采用非独占的锁或非阻塞锁来代替独占锁。
减少锁的持有时间
降低发生竞争可能性的一种有效方式就是尽可能缩短锁的持有时间。例如,可以将一些与锁无关的代码移出同步代码块,尤其是那些开销较大的操作,以及可能被阻塞的操作,例如I/O操作。
优化前
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
@ThreadSafe public class AttributeStore { @GuardedBy( "this" ) private final Map<String, String> attributes = new HashMap<String, String>(); public synchronized boolean userLocationMatches(String name, String regexp) { String key = "users." + name + ".location" ; String location = attributes.get(key); if (location == null ) return false ; else return Pattern.matches(regexp, location); } } |
优化后
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
@ThreadSafe public class BetterAttributeStore { @GuardedBy( "this" ) private final Map<String, String> attributes = new HashMap<String, String>(); public boolean userLocationMatches(String name, String regexp) { String key = "users." + name + ".location" ; String location; synchronized ( this ) { location = attributes.get(key); } if (location == null ) return false ; else return Pattern.matches(regexp, location); } } |
降低锁的粒度
另一种减小锁的持有时间的方式是降低线程请求锁的频率(从而减小发生竞争的可能性)。这可以通过锁分解和锁分段等技术来实现,在这些技术中将采用多个相互独立的锁来保护独立的状态变量,从而改变这些变量在之前由单个锁来保护的情况。这些技术能减小锁操作的粒度,并能实现更高的可伸缩性,然而,使用的锁越多,那么发生死锁的风险也就越高。
优化前
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
@ThreadSafe public class ServerStatusBeforeSplit { @GuardedBy( "this" ) public final Set<String> users; @GuardedBy( "this" ) public final Set<String> queries; public ServerStatusBeforeSplit() { users = new HashSet<String>(); queries = new HashSet<String>(); } public synchronized void addUser(String u) { users.add(u); } public synchronized void addQuery(String q) { queries.add(q); } public synchronized void removeUser(String u) { users.remove(u); } public synchronized void removeQuery(String q) { queries.remove(q); } } |
优化后
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
@ThreadSafe public class ServerStatusAfterSplit { @GuardedBy( "users" ) public final Set<String> users; @GuardedBy( "queries" ) public final Set<String> queries; public ServerStatusAfterSplit() { users = new HashSet<String>(); queries = new HashSet<String>(); } public void addUser(String u) { synchronized (users) { users.add(u); } } public void addQuery(String q) { synchronized (queries) { queries.add(q); } } public void removeUser(String u) { synchronized (users) { users.remove(u); } } public void removeQuery(String q) { synchronized (users) { queries.remove(q); } } } |
锁分段
在某些情况下,可以将锁分解技术进一步扩展为对一组独立对象上的锁进行分解,这种情况被称为锁分段。例如,在ConcurrentHashMap的实现中使用了一个包含16个锁的数组,每个锁保护所有散列桶的1/16,其中第N个散列桶由第(Nmod 16)个锁来保护。假设散列函数具有合理的分布性,并且关键字能够实现均匀分布,那么这大约能把对于锁的请求减少到原来的1/16。正是这项技术使得ConcurrentHashMap能够支持多达16个并发的写入器。(要使得拥有大量处理器的系统在高访问量的情况下实现更高的并发性,还可以进一步增加锁的数量,但仅当你能证明并发写入线程的竞争足够激烈并需要突破这个限制时,才能将锁分段的数量超过默认的16个。)
锁分段的一个劣势在于:与采用单个锁来实现独占访问相比,要获取多个锁来实现独占访问将更加困难并且开销更高。通常,在执行一个操作时最多只需获取一个锁,但在某些情况下需要加锁整个容器,例如当ConcurrentHashMap需要扩展映射范围,以及重新计算键值的散列值要分布到更大的桶集合中时,就需要获取分段所集合中所有的锁。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
@ThreadSafe public class StripedMap { // Synchronization policy: buckets[n] guarded by locks[n%N_LOCKS] private static final int N_LOCKS = 16; private final Node[] buckets; private final Object[] locks; private static class Node { Node next; Object key; Object value; } public StripedMap(int numBuckets) { buckets = new Node[numBuckets]; locks = new Object[N_LOCKS]; for (int i = 0; i < N_LOCKS; i++) locks[i] = new Object(); } private final int hash(Object key) { return Math.abs(key.hashCode() % buckets.length); } public Object get(Object key) { int hash = hash(key); synchronized (locks[hash % N_LOCKS]) { for (Node m = buckets[hash]; m != null ; m = m.next) if (m.key.equals(key)) return m.value; } return null ; } public void clear() { for (int i = 0; i < buckets.length; i++) { synchronized (locks[i % N_LOCKS]) { buckets[i] = null ; } } } } |
避免热点域
如果一个锁保护两个独立变量X和Y,并且线程A想要访问X,而线程B想要访问Y(这类似于在ServerStatus中,一个线程调用addUser,而另一个线程调用addQuery),那么这两个线程不会在任何数据上发生竞争,即使它们会在同一个锁上发生竞争。当每个操作都请求多个变量时,锁的粒度将很难降低。这是在性能与可伸缩性之间相互制衡的另一个方面,一些常见的优化措施,例如将一些反复计算的结果缓存起来,都会引入一些“热点域(HotField)”,而这些热点域往往会限制可伸缩性。当实现HashMap时,你需要考虑如何在size方法中计算Map中的元素数量。最简单的方法就是,在每次调用时都统计一次元素的数量。一种常见的优化措施是,在插入和移除元素时更新一个计数器,虽然这在put和remove等方法中略微增加了一些开销,以确保计数器是最新的值,但这将把size方法的开销从O(n)降低到O(l)。
在单线程或者采用完全同步的实现中,使用一个独立的计数能很好地提高类似size和isEmpty这些方法的执行速度,但却导致更难以提升实现的可伸缩性,因为每个修改map的操作都需要更新这个共享的计数器。即使使用锁分段技术来实现散列链,那么在对计数器的访问进行同步时,也会重新导致在使用独占锁时存在的可伸缩性问题。一个看似性能优化的措施—缓存size操作的结果,已经变成了一个可伸缩性问题。在这种情况下,计数器也被称为热点域,因为每个导致元素数量发生变化的操作都需要访问它。为了避免这个问题,ConcurrentHashMap中的size将对每个分段进行枚举并将每个分段中的元素数量相加,而不是维护一个全局计数。为了避免枚举每个元素,ConcurrentHashMap为每个分段都维护了一个独立的计数,并通过每个分段的锁来维护这个值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public int size() { long n = sumCount(); return ((n < 0L) ? 0 : (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int)n); } final long sumCount() { CounterCell[] as = counterCells; CounterCell a; long sum = baseCount; if (as != null ) { for (int i = 0; i < as.length; ++i) { if ((a = as[i]) != null ) sum += a.value; } } return sum; } |
代替独占锁
第三种降低竞争锁的影响的技术就是放弃使用独占锁,从而有助于使用一种友好并发的方式来管理共享状态。例如,使用并发容器、读-写锁、不可变对象以及原子变量。ReadWriteLock能提供比独占锁更高的并发性。而对于只读的数据结构,其中包含的不变性可以完全不需要加锁操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
|
public class ReadWriteMap <K,V> { private final Map<K, V> map; private final ReadWriteLock lock = new ReentrantReadWriteLock(); private final Lock r = lock.readLock(); private final Lock w = lock.writeLock(); public ReadWriteMap(Map<K, V> map) { this .map = map; } public V put(K key, V value) { w.lock(); try { return map.put(key, value); } finally { w.unlock(); } } public V remove(Object key) { w.lock(); try { return map.remove(key); } finally { w.unlock(); } } public void putAll(Map<? extends K, ? extends V> m) { w.lock(); try { map.putAll(m); } finally { w.unlock(); } } public void clear() { w.lock(); try { map.clear(); } finally { w.unlock(); } } public V get(Object key) { r.lock(); try { return map.get(key); } finally { r.unlock(); } } public int size() { r.lock(); try { return map.size(); } finally { r.unlock(); } } public boolean isEmpty() { r.lock(); try { return map.isEmpty(); } finally { r.unlock(); } } public boolean containsKey(Object key) { r.lock(); try { return map.containsKey(key); } finally { r.unlock(); } } public boolean containsValue(Object value) { r.lock(); try { return map.containsValue(value); } finally { r.unlock(); } } } |
https://www.jfh.com/jfperiodical/article/4670