并发编程的艺术06-复合&层次自旋锁

3 月,跳不动了?>>> hot3.png

up-0b7cd4eb563763dc92b4c66193647962153.png

复合锁

考虑下面根据观察所得出的结论:在一个队列锁中,只有位于队列前面的线程需要进行锁切换。对于队列锁和后退锁的一种平衡方案就是进入临界区的过程中只允许在队列中保持少量的等待线程(使得队列变短,队列变短了争用锁的线程就少了),若剩下的线程企图进入这个队列,则采用指数后退的方法。线程通过后退来终止是很平常的。

CompositeLock 维护了一个短得固定大小的锁节点数组。每个试图获得锁的线程随机在数组中选择一个节点。如果被选中的这个节点正在被别的线程使用,那么当前线程就后退一段时间,并且之后再次在这个节点上尝试。一旦线程获得了一个节点,就将该节点添加到队列的尾部。线程在节点的前驱节点上自旋,如果该节点的所有者线程发出了已完成的信号,则线程进入临界区。当线程离开临界区时,完成或是超时,线程让出该节点的所有权,从而是另外的后退线程可以获得该节点

tail 域是一个 AtomicsStampedReference<QNode> , 它包含一个队尾节点的引用以及一个版本号,该版本号是为了避免在更新操作中存在的 ABA 问题。tail 域要么为 null,要么指向最近被插入队列的节点。

QNode 的4种状态:

WAITING :节点被链接在队列中,线程要么在临界区内要么正等待进入临界区。

RELEASED :当一个节点的拥有者线程准备离开临界区释放锁。

ABORTED :线程要放弃获取锁,节点已经进入队列。

FREE :线程要放弃获取锁,节点没有进入了队列。

acquireQNode 函数说明:

1. 线程随机的在QNode数组中选择了一个节点,并尝试进行 test-and-set 指令将该节点的状态由 FREE 改为 WAITING 状态。(FREE 状态说明该节点是没有被其他线程占有的,如果更改状态为 WAITING 成功,则表示当前线程成功占有了这个节点。)如果成功修改节点状态为 WAITING 会直接返回该节点以供后续操作使用。

2. 如果失败会判断该节点是否是 ABORTED 或 RELEASED 状态。如果是并且这个节点现在还处在链表的尾部而且还处于 ABORTED 状态,那么会尝试将该节点从链表中移除,让该节点的 pred 域指向的节点作为链表的尾部。当该节点此时没有后继节点的时候可以移除成功,并且修改该节点状态为 WAITING 然后直接返回该节点。如果是节点是 RELEASE 状态会直接将队列尾部设置为 null ,因为说明队列中所有的锁都释放了,最后一个队尾才会是 RELEASE 状态。如果节点是 ABORTED 状态就需要将,该节点的 pred 域所指的节点设置为队列尾部,因为 pred 域所指的节点状态目前不明确还需要让它在队列中继续存在。

3. 如果该节点状态不是 ABORTED 或 RELEASED 状态,那么说明该节点是 WAITING 状态正被其他线程占用,所以就会直接进行后退。

acquireQNode 函数会在上述三个步骤自旋,直到成功返回了一个 QNode 或者是超时。

spliceQNode 函数说明:

该函数做的事情就是将线程获取到的 QNode 节点尝试放到队列的尾部,成功后返回之前队列尾部节点。或者是在自旋执行过程中超时。

waitForPredecessor 函数说明:

waitForPredecessor 函数在前驱节点上自旋(也就是 pred),直到它的状态变为 RELEASE ,说明占有该节点的线程释放了锁。如果 pred 节点状态为 ABORTED 就会将它状态改为 FREE ,并且从队列中移除,将该节点的 pred 域指向的节点做为被选中的节点的前驱节点。重复上述这个过程直到超时或有一个前驱节点的状态变为 RELEASE 。前驱节点状态变为 RELEASE 后,该函数结束自旋并将前驱节点状态改为 FREE,然后线程可以进入临界区了。

Backoff

public class Backoff {
    final int minDelay , maxDelay;
    int limit;
    final Random random;
    
    public Backoff(int min , int max) {
        minDelay = min;
        maxDelay = max;
        limit = minDelay;
       random = new Random(); 
    }
    
    public void backoff() throw Exception {
        int delay = random.nextInt(limit);
        limit = Math.min(maxDelay , 2 * limit);
        Thread.sleep(delay);
    }
}

CompositeLock

enum State {FREE, WAITING, RELEASED, ABORTED};
class QNode {
    AtomicReference<State> state;
    QNode pred;
    public QNode() {
      state = new AtomicReference<State>(State.FREE);
    }
}

public class CompositeLock implements Lock{
  private static final int SIZE = 4;
  private static final int MIN_BACKOFF = 1024;
  private static final int MAX_BACKOFF = 256 * MIN_BACKOFF;
  AtomicStampedReference<QNode> tail;
  QNode[] waiting;
  Random random;
  ThreadLocal<QNode> myNode = new ThreadLocal<QNode>() {
    protected QNode initialValue() { return null; };
  };
  
  public CompositeLock() {
    tail = new AtomicStampedReference<QNode>(null, 0);
    random = new Random();
    waiting = new QNode[SIZE];
    for (int i = 0; i < waiting.length; i++) {
      waiting[i] = new QNode();
    }
  }
  
  public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
    long patience = TimeUnit.MILLISECONDS.convert(time, unit);
    long startTime = System.currentTimeMillis();
    Backoff backoff = new Backoff(MIN_BACKOFF, MAX_BACKOFF);
    try {
      QNode node = acquireQNode(backoff, startTime, patience);
      QNode pred = spliceQNode(node, startTime, patience);
      waitForPredecessor(pred, node, startTime, patience);
      return true;
    } catch (TimeoutException e) {
      return false;
    }
  }
  
  public void unlock() {
    QNode acqNode = myNode.get();
    acqNode.state.set(State.RELEASED);
  }
  
  private boolean timeout(long startTime, long patience) {
    return System.currentTimeMillis() - startTime > patience;
  }
  
  private QNode acquireQNode(Backoff backoff, long startTime, long patience)
      throws TimeoutException, InterruptedException {
    QNode node = waiting[random.nextInt(SIZE)];
    QNode currTail;
    int[] currStamp = {0};
    while (true) {
      if (node.state.compareAndSet(State.FREE, State.WAITING)) {
        return node;
      }
      currTail = tail.get(currStamp);
      State state = node.state.get();
      if (state == State.ABORTED || state == State.RELEASED) {
        if (node == currTail) {
          QNode myPred = null;
          if (state == State.ABORTED) {
            myPred = node.pred;
          }
          if (tail.compareAndSet(currTail, myPred,
              currStamp[0], currStamp[0]+1)) {
            node.state.set(State.WAITING);
            return node;
          }
        }
      }
      backoff.backoff();
      if (timeout(patience, startTime)) {
        throw new TimeoutException();
      }
    }
  }
  
  private QNode spliceQNode(QNode node, long startTime, long patience)
      throws TimeoutException {
    QNode currTail;
    int[] currStamp = {0};
    // splice node into queue
    do {
      currTail = tail.get(currStamp);
      if (timeout(startTime, patience)) {
        node.state.set(State.FREE);
        throw new TimeoutException();
      }
    } while (!tail.compareAndSet(currTail, node,
        currStamp[0], currStamp[0]+1));
    return currTail;
  }
  
  private void waitForPredecessor(QNode pred, QNode node, long startTime, long patience)
      throws TimeoutException {
    // wait for predecessor to release lock
    int[] stamp = {0};
    if (pred == null) {
      myNode.set(node);
      return;
    }
    State predState = pred.state.get();
    while (predState != State.RELEASED) {
      if (predState == State.ABORTED) {
        QNode temp = pred;
        pred = pred.pred;
        temp.state.set(State.FREE);
      }
      if (timeout(patience, startTime)) {
        node.pred = pred;
        node.state.set(State.ABORTED);
        throw new TimeoutException();
      }
      predState = pred.state.get();
    }
    pred.state.set(State.FREE);
    myNode.set(node);
    return;
  }

CompositeLock有一些让人感兴趣的特性。当多个线程退后时,它们访问不同的存储单元从而降低了争用。锁的切换速度很快。放弃一个锁请求对处于后退阶段的线程来说是平常的,而且对于已经获得队列节点的线程来说要更加简单直接。CompositeLock也存在一个缺点,那就是失去了先到先服务的公平性。

快速复合路径锁

尽管涉及 CompositeLock 的初衷是为了保证在争用时具有较好的性能,然而在无并发的情形下,性能也是非常重要的。理想情况下,对于一个单独运行的线程来说,获取一个锁应该和获取一个无争用的 TASLock 同样简单。然而在 CompositeLock 算法中,一个单独运行的线程必须重新设置 tail 域使之离开一个已经释放的锁,声明该节点然后把它插入到队列中。

快速路径是一条让单个线程执行复杂算法时快速完成的捷径。可以扩展 CompositeLock 算法使他包括一条快速路径,在该快速路径中单个线程不必获取一个节点并插入队列就可以获取一个空闲锁。

fastPathLock 函数说明:

1. 这个函数会检查 tail 是否有引用指向一个不为 null 的 QNode ,如果有则说明不满足单一线程获取锁的条件会直接返回 false。

2. 再检查 stamp 是否发生过改变,如果 stamp 的值大于等于 FASTPATH 的值 (FASTPATH 值的选取需要选择一个二进制高位为 1 其余位都为 0 的数字 , 例如 :1000-0000。) ,则 stamp & FASTPATH 必定不等于 0 ,说明快速路径标志量已经被占用,这时候直接返回 false 。(比 FASTPATH 小的数于 FASTPATH & 全都等于 0)(& 操作符是二进制操作,从高位开始如果两位都为 1 则结果为 1 。如果有一位为 0 ,则结果为 0 )。再通过上面两个检查后会根据 FASTPATH 的值计算一个新的 stamp ,并将队列尾部节点设置为 null 。

fastPathUnlock 函数说明:

该函数会将 stamp 的值还原到调用 fastPathLock() 函数之前 stamp 的值。举个例子:

1. stamp 为 11。

2. fastPathLock() 修改 stamp 之后, stamp 为 1073741824。

3. fastPathUnlock() 还原 stamp 之后 , stamp 为 11。

fastPathWait 函数说明:

函数的作用是确保从慢速路径返回之前,没有其他线程持有快速路径的锁,等待 FASTPATH 标志量被清空。

CompositeFastPathLock

public class CompositeFastPathLock extends CompositeLock {
  private static final int FASTPATH = 1 << 30;
  public int fastPathTaken = 0;
  
  public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
    if (fastPathLock()) {
      fastPathTaken++;
      return true;
    }
    if (super.tryLock(time, unit)) {
      fastPathWait();
      return true;
    }
    return false;
  }
  
  public void unlock() {
    if (!fastPathUnlock()) {
      super.unlock();
    };
  }
  
  private boolean fastPathLock() {
    int oldStamp, newStamp;
    int stamp[] = {0};
    QNode qnode;
    qnode = tail.get(stamp);
    oldStamp = stamp[0];
    if (qnode != null) {
      return false;
    }
    if ((oldStamp & FASTPATH) != 0) {
      return false;
    }
    newStamp = (oldStamp + 1) | FASTPATH;   // set flag
    return tail.compareAndSet(qnode, null, oldStamp, newStamp);
  }
  
  private boolean fastPathUnlock() {
    int oldStamp, newStamp;
    oldStamp = tail.getStamp();
    if ((oldStamp & FASTPATH) == 0) {
      return false;
    }
    int[] stamp = {0};
    QNode qnode;
    do {
      qnode = tail.get(stamp);
      oldStamp = stamp[0];
      newStamp = oldStamp & (~FASTPATH);   // unset flag
    } while (!tail.compareAndSet(qnode, qnode, oldStamp, newStamp));
    return true;
  }
  
  private void fastPathWait() {
    while ((tail.getStamp() & FASTPATH ) != 0) {} // spin while flag is set
  }
}

层次锁

目前大多数cache一致的系统结构都以集群方式组织处理器,在一个集群内的通信要比在集群间的通信快得多。例如,一个集群可以对应于通过快速互连来共享存储器的处理器,也可以对应于运行在一个多核系统结构中一个单核上的所有线程。这部分主要考虑这种对局部差异较敏感的锁。称这样的锁为层次锁,因为在设计中要考虑系统的层次存储结构以及访问开销。

系统结构的存储结构可以有多个层次,为简单起见,我们假设只有两个层次。下面考虑一种由多个处理器集群所组成的系统结构,同一集群中的处理器通过共享cache进行高效通信。集群之间的通信代价要比集群内通信代价大得多。假设每个集群都有一个唯一的集群ID,该ID对集群内每个线程都是已知的,可以通过 ThreadID.getCluster() 获得。线程不能在集群之间迁移。

层次后退锁

test-test-and-set 锁非常适用于集群开发。假定线程A持有锁。若A所在的集群中的线程具有较短的后退时间,那么当锁释放时,本地线程要比远程线程更有可能获得锁,从而降低了切换锁的拥有权所需的总时间。

HBOLock 缺点之一是它过度利用了局部性。这样可能存在同一集群中线程不断传递锁,而其他集群中的线程出现饥饿的现象。况且获取和释放锁的时候会使得远程cache副本失效,这将在保证cache一致性的系统结构中产生巨大开销。

HBOLock

public class HBOLock implements Lock {
  private static final int LOCAL_MIN_DELAY = 8;
  private static final int LOCAL_MAX_DELAY = 256;
  private static final int REMOTE_MIN_DELAY = 256;
  private static final int REMOTE_MAX_DELAY = 1024;
  private static final int FREE = -1;
  AtomicInteger state;
  
   public HBOLock() {
    state = new AtomicInteger(FREE);
  }
  
  public void lock() {
    int myCluster = ThreadID.getCluster();
    Backoff localBackoff = new Backoff(LOCAL_MIN_DELAY, LOCAL_MAX_DELAY);
    Backoff remoteBackoff = new Backoff(REMOTE_MIN_DELAY, REMOTE_MAX_DELAY);
    while (true) {
      if (state.compareAndSet(FREE, myCluster)) {
        return;
      }
      int lockState = state.get();
      try {
        if (lockState == myCluster) {
          localBackoff.backoff();
        } else {
          remoteBackoff.backoff();
        }
      } catch (InterruptedException ex) {
      }
    }
  }
  
  public void unlock() {
    state.set(FREE);
  }
}

层次 CLH 队列锁

为了提供一种更平衡的集群开发方法,首先分析层次队列锁的设计。层次锁设计中,问题的关键就是要协调冲突时的公平性需求。既要保证在同一集群内进行锁的传递以避免较高的通信开销,同时也要保证某种程度的公平性,以使远程锁请求不至于比本地锁请求过于延迟。我们通过将相同集群的请求序列一起调度来平衡这些需求。

HCLHLock 由一组本地队列和一个全局队列组成,每个集群对应一个本地队列。所有队列都是由节点组成的链表,其中的链表都是隐式的,即链表被保持在线程的局部域 myQNode , myPred 中。

 

QNode 有三个虚拟域:

1. 当前或最近拥有者的 ClusterId。

2. successorMustWait : 在入队前被设为 true ,在释放锁的时候设为 false 。这样对于一个正在等待锁的线程来说,当其前驱节点的 successorMustWait 变为 false 时,该线程才可以结束自旋。

3. tailWhenSpliced : 说明该节点是否为当前被拼接到全局队列中的最后一个节点。

之所以称这些域为虚拟的,是因为对它们的更新要以原子方式进行。因此在 AtomicInteger 域中把它们描述为位-域(bit-field),采用简单的移位和屏蔽操作来取他们的值。

lock() 函数首先将线程节点加入到本地队列,然后等待直到该线程要么获得锁能够进入临界区,要么它的节点成为本地队列的队首。在后一种情况下,该线程成为集群主,由它负责把本地队列拼接到全局队列中。由于线程的节点已被初始化,所以 successorMustWait 为 true ,tailWhenSpliced 为 false。ClusterId 为调用者线程的集群ID。该线程将他的节点加入到本地队列的尾部。线程随后调用 waitForGrantOrClusterMaster() ,让线程自旋直到下面情况发生:

1. 前驱节点来自于同一个集群,且 successorMustWait ,tailWhenSpliced 都为 false 。

2. 前驱节点来自于不同集群或前驱节点的标志量 tailWhenSpliced 为 true。

在第一种情况下线程的节点是全局队列的队首,所以它进入临界区然后返回。

在第二种情况下,线程的节点为本地队列队首,所以该线程为集群主,从而由它来负责把本地队列拼接到全局队列中。

另外,要么该线程前驱的 tailWhenSpliced 标志量为 true , 要么其前驱的集群与它自己的集群不同。如果是集群不同,则前驱节点不可能在该线程的本地队列中。前驱节点必定已经被移动到全局队列中且被不同集群中的某个线程所回收。另一个方面,如果前驱节点的 tailWhenSpliced 为 true ,则前驱节点是进入了全局队列的最后一个节点,因此该线程的节点此时处于本地队列队首。它不可能被移动到全局队列中,因为只有其节点位于本地队列队首的集群主才能将节点移动到全局队列中。

作为集群主,其任务就是将本地队列拼接到全局队列当中。本地队列每个线程都在其前驱节点上自旋。一旦集群主进入全局队列它就和通常的 CLHLock 对列一样,在它前驱节点的 successorMustWait 为 false 时进入临界区。

HCLHLock

public class HCLHLock implements Lock {
    
  static final int MAX_CLUSTERS = 32;
  List<AtomicReference<QNode>> localQueues;
  AtomicReference<QNode> globalQueue;
 
  ThreadLocal<QNode> currNode = new ThreadLocal<QNode>() {
    protected QNode initialValue() { return new QNode(); };
  };
 
 ThreadLocal<QNode> predNode = new ThreadLocal<QNode>() {
    protected QNode initialValue() { return null; };
 };
  
  public HCLHLock() {
    localQueues = new ArrayList<AtomicReference<QNode>>(MAX_CLUSTERS);
    for (int i = 0; i < MAX_CLUSTERS; i++) {
      localQueues.add(new AtomicReference <QNode>());
    }
    QNode head = new QNode();
    globalQueue = new AtomicReference<QNode>(head);
    
  }
  
  public void lock() {
    QNode myNode = currNode.get();
    AtomicReference<QNode> localQueue = localQueues.get(ThreadID.getCluster());
    // splice my QNode into local queue
    QNode myPred = null;
    do {
      myPred = localQueue.get();
    } while (!localQueue.compareAndSet(myPred, myNode));
    if (myPred != null) {
      boolean iOwnLock = myPred.waitForGrantOrClusterMaster();
      if (iOwnLock) {
        // I have the lock. Save QNode just released by previous leader
        predNode.set(myPred);
        return;
      }
    }
    // At this point I am the cluster master.
    // Splice local queue into global queue.
    QNode localTail = null;
    do {
      myPred = globalQueue.get();
      localTail = localQueue.get();
    } while(!globalQueue.compareAndSet(myPred, localTail));
    // inform successor it is the new master
    localTail.setTailWhenSpliced(true);
    // wait for predecessor to release lock
    while (myPred.isSuccessorMustWait()) {};
    // I have the lock. Save QNode just released by previous leader
    predNode.set(myPred);
    return;
  }
  
  public void unlock() {
    QNode myNode = currNode.get();
    myNode.setSuccessorMustWait(false);
    // promote pred node to current
    QNode node = predNode.get();
    node.unlock();
    currNode.set(node);
  }
  
  static class QNode {
    // private boolean tailWhenSpliced;
    private static final int TWS_MASK = 0x80000000;
    // private boolean successorMustWait= false;
    private static final int SMW_MASK = 0x40000000;
    // private int clusterID;
    private static final int CLUSTER_MASK = 0x3FFFFFFF;
    AtomicInteger state;
    public QNode() {
      state = new AtomicInteger(0);
    }
    boolean waitForGrantOrClusterMaster() {
      int myCluster = ThreadID.getCluster();
      while(true) {
        if (getClusterID() == myCluster &&
            !isTailWhenSpliced() &&
            !isSuccessorMustWait()) {
          return true;
        } else if (getClusterID() != myCluster || isTailWhenSpliced()) {
          return false;
        }
      }
    }
    public void unlock() {
      int oldState = 0;
      int newState = ThreadID.getCluster();
      // successorMustWait = true;
      newState |= SMW_MASK;
      // tailWhenSpliced = false;
      newState &= (~TWS_MASK);
      do {
        oldState = state.get();
      } while (! state.compareAndSet(oldState, newState));
    }
    
    public int getClusterID() {
      return state.get() & CLUSTER_MASK;
    }
    
    public void setClusterID(int clusterID) {
      int oldState, newState;
      do {
        oldState = state.get();
        newState = (oldState & ~CLUSTER_MASK) | clusterID;
      } while (! state.compareAndSet(oldState, newState));
    }
    
    public boolean isSuccessorMustWait() {
      return (state.get() & SMW_MASK) != 0;
    }
    
    public void setSuccessorMustWait(boolean successorMustWait) {
      int oldState, newState;
      do {
        oldState = state.get();
        if (successorMustWait) {
          newState = oldState | SMW_MASK;
        } else {
          newState = oldState & ~SMW_MASK;
        }
      } while (! state.compareAndSet(oldState, newState));
    }
    
    public boolean isTailWhenSpliced() {
      return (state.get() & TWS_MASK) != 0;
    }
    
    public void setTailWhenSpliced(boolean tailWhenSpliced) {
      int oldState, newState;
      do {
        oldState = state.get();
        if (tailWhenSpliced) {
          newState = oldState | TWS_MASK;
        } else {
          newState = oldState & ~TWS_MASK;
        }
      } while (! state.compareAndSet(oldState, newState));
    }
    
  }
}

往期精彩回顾

并发编程的艺术01-面试中被问到并发基础知识打不上来?

并发编程的艺术02-过滤锁算法

并发编程的艺术03-Bakery互斥算法

并发编程的艺术04-TAS自旋锁

并发编程的艺术05-队列自旋锁

每日阅读之计算机总线系统

内存屏障究竟是个什么鬼?

带你了解缓存一致性协议MESI

关注微信公众号「黑帽子技术」

第一时间浏览技术干活文章

up-63f0e4ab13ec0ece69b8844f65375ffaed7.png

发布了132 篇原创文章 · 获赞 2 · 访问量 532

猜你喜欢

转载自blog.csdn.net/weixin_45839894/article/details/105198539