-
(1) 串行操作将降低可伸缩性
(2) 上下文切换会降低性能
(3) 锁上发生竞争时, 将同时导致这两个问题
–> 尽量减少锁的竞争
-
影响锁竞争发生可能性的因素
(1) 锁的请求频率
(2) 每次持有锁的时间
-
对应的降低锁的竞争程度的解决方案
(1) 降低锁的请求频率
(2) 减少锁的持有时间
(3) 使用__带有协调机制__的锁
接下来会介绍具体的解决办法
-
缩小锁的范围——快进快出
(1) 将与锁无关的代码移除同步代码块, 特别是开销大、有阻塞的操作
(2) 示例
不好的示例
@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); } } }好的示例
@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); } } }需要同步的只是location = attributes.get(key);这一句,其他部分不应该套上锁
(3) 由于示例中只有一个状态变量, 所以可以考虑使用__委托__的方式, 直接不在客户代码中加锁, 使用ConcurrentHashMap
(4) 同步代码块过大时, 会造成串行部分过多, 影响性能;
但是, 当多个变量需要原子更新时, 必须放在同一个同步代码块中; 并且本应该同一个锁保护的同步代码块拆成多个, 反而影响性能;
因此, 原则是根据需求, 如果同步代码块中出现了阻塞操作或大量计算操作, 并且与状态变量无关, 一定要移除同步代码块
-
减少锁的粒度——锁分解
(1) 一个同步方法, 可以更改为在方法中使用多个同步代码块, 每个同步代码块被不同的锁保护, 这样可以有效降低竞争
(2) 但是, 引入多个锁以后, 出现死锁的风险增加
(3) 示例
不好的示例
@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); } }好的示例
@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); } } }第一个示例中所有的方法都由this一个锁保护, 而由于两个状态变量各自独立, 所以第二个示例采取__锁分解__的方式, 针对不同的函数使用不同的锁保护, 可以减少持有锁的时间, 降低竞争
-
减少锁的粒度——锁分段
(1) 在某些情况下, 可以将锁分解技术进一步扩展, 扩展为对一组独立对象上的锁进行分解, 称为__锁分段__
例如: ConcurrentHashMap中使用了一个包含了16个锁的数组, 每个锁保护所有散列桶的约1/16
(2) 缺点: 实现独占访问比较困难
(3) 示例
@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; } } } }这个示例中使用了一个包含锁的数组, 这样在get或clear时, 每次只获得一部分锁进行同步, 而不是一个锁对整体进行同步
-
避免热点域
(1) 例如统计一个HashMap的size(), 优化的方法就是加一个计数器。 但是对ConcurrentHashMap来说, 当并发的对其进行操作时,每次put和remove都需要改变这个计数器__进行同步__, 所以在这个类里__这个计数器就被叫做热点域__,是可伸缩性的瓶颈
(2) 解决办法是分段统计, 每个锁保护的范围维持一个单独的计数器, 调用时再集中加和
-
替代独占锁
(1) 上面的方法都是在synchronized的机制下进行优化, 如果干脆不使用synchronized独占锁, 而是使用其他友好的并发方法, 也可以减少锁的竞争
(2) ReadWriteLock(chapter13)
读操作可同时访问, 写操作独占
(3) 原子变量AtomicXXX(chapter15)
可以降级更新__热点域__的开销, 因为粒度更细且使用底层并发原语
-
向对象池说不
(1) 对象池的来历
早期JVM的垃圾回收和对象分配速度慢, 出现一种解决方案是维持一组对象组成对象池, 这些对象池使用不被回收, 需要对象时从对象池中取得
(2) 但是,现在应该放弃对象池
1° 并发程序中, 需要同步机制协调对对象池数据结构的访问, 引发某个线程阻塞
2° 对象池的大小设置是一个难题: 设小了没用; 设大了当真的需要回收对象池时, 垃圾回收器压力太大
3° JVM进化了, 对象分配操作和垃圾回收操作的开销比同步开销小
-
CPU没有充分利用的原因
(1) 负载不充足
(2) IO密集
(3) 外部限制(例如数据库或web)
(4) 锁竞争
-
日志操作中减少上下文开销的思路
(1) 传统的方式是每个线程需要记录消息时, 都调用日志方法写入日志文件
带来的问题是不同线程同时写入时, 就需要同步机制, IO造成阻塞,就会引起线程切换, 就要引发上下文切换开销
(2) 新思路是用一个专门的后台线程写日志, 需要记录日志的线程只是将要写的内容传给这个后台线程, 这样跟日志相关的IO操作只在一个线程中进行
chapter11_性能与可伸缩性_4_减少锁的竞争
最新推荐文章于 2022-10-06 21:54:57 发布
本文讨论了如何减少锁的竞争以提高并发性能和系统的可伸缩性。主要内容包括:缩小锁的范围,通过移除无关代码和使用委托避免不必要的同步;减少锁的粒度,通过锁分解降低锁的持有时间;锁分段,例如在ConcurrentHashMap中使用锁数组来保护数据;避免热点域,通过分段统计减少同步开销;替代独占锁,如使用ReadWriteLock和Atomic变量;以及在日志操作中减少上下文切换。

408

被折叠的 条评论
为什么被折叠?



