chapter11_性能与可伸缩性_4_减少锁的竞争

本文讨论了如何减少锁的竞争以提高并发性能和系统的可伸缩性。主要内容包括:缩小锁的范围,通过移除无关代码和使用委托避免不必要的同步;减少锁的粒度,通过锁分解降低锁的持有时间;锁分段,例如在ConcurrentHashMap中使用锁数组来保护数据;避免热点域,通过分段统计减少同步开销;替代独占锁,如使用ReadWriteLock和Atomic变量;以及在日志操作中减少上下文切换。
  • (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操作只在一个线程中进行

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值