StampedLock详解

StampedLock简介

StampedLock是比ReentrantReadWriteLock更快的一种锁,支持乐观读、悲观读锁和写锁。和ReentrantReadWriteLock不同的是,StampedLock支持多个线程申请乐观读的同时,还允许一个线程申请写锁。

乐观读并不加锁

StampedLock的底层并不是基于AQS的。

StampedLock示例

 class Point {
    private double x, y;
    private final StampedLock sl = new StampedLock();
 
    void move(double deltaX, double deltaY) { // an exclusively locked method
      long stamp = sl.writeLock();
      try {
        x += deltaX;
        y += deltaY;
      } finally {
        sl.unlockWrite(stamp);
      }
    }
 
    double distanceFromOrigin() { // A read-only method
      long stamp = sl.tryOptimisticRead();
      double currentX = x, currentY = y;
      if (!sl.validate(stamp)) {
         stamp = sl.readLock();
         try {
           currentX = x;
           currentY = y;
         } finally {
            sl.unlockRead(stamp);
        }
      }
      return Math.sqrt(currentX * currentX + currentY  currentY);
    }
 
    void moveIfAtOrigin(double newX, double newY) { // upgrade
      // Could instead start with optimistic, not read mode
      long stamp = sl.readLock();
      try {
        while (x == 0.0 && y == 0.0) {
          long ws = sl.tryConvertToWriteLock(stamp);
          if (ws != 0L) {
            stamp = ws;
            x = newX;
            y = newY;
            break;
          }
          else {
            sl.unlockRead(stamp);
            stamp = sl.writeLock();
          }
        }
      } finally {
        sl.unlock(stamp);
      }
    }
  }}

StampedLock源码分析

StampedLock也是通过一个int变量state、一个队列来实现的
state
state的默认值是256 1 0000 0000
state是一个int变量,总共有32位
前24位表示版本号、低8位表示锁。
低8位的第1位表示是否为写锁,1表示写锁、0表示没有写锁
剩下7位表示悲观读锁的个数

WNode
队列中的节点对应的是WNode

static final class WNode {
    //前继节点
    volatile WNode prev;
    //后继节点
    volatile WNode next;
    //悲观读锁对应的栈
    volatile WNode cowait;   
    //对应的线程
    volatile Thread thread;  
    //节点的状态:取消CANCEL 1、等待WATING -1。
    volatile int status;     
    //节点类型:读锁、写锁
    final int mode;   
    WNode(int m, WNode p) { mode = m; prev = p; }
}

具体的组织方式如下图所示,值得注意的是,队列的第一个节点是哨兵节点,不代表具体的请求锁的节点。
在这里插入图片描述

1、writeLock

 public long writeLock() {
     long s, next;  // bypass acquireWrite in fully unlocked case only
     return ((((s = state) & ABITS) == 0L &&
              U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) ?
             next : acquireWrite(false, 0L));
 }

ABITS = 128 ,二进制为1111 1111
只有当state的低八位为 0000 0000 的时候才能申请读锁,这个时候按照我们之前说的规则,就是没有写锁,并且读锁的个数为0.

然后利用CAS来申请锁,如果申请成功就将state+=WBIT
WBIT为128
state的低8位就变成 1000 0000,就代表着上了写锁,返回state的值。

如果申请锁失败就进入acquireWrite,自旋申请锁,如果申请不到就挂起

private long acquireWrite(boolean interruptible, long deadline) {
    WNode node = null, p;
    for (int spins = -1;;) {
        long m, s, ns;
        //如果当前没有锁,就申请锁
        if ((m = (s = state) & ABITS) == 0L) {
            if (U.compareAndSwapLong(this, STATE, s, ns = s + WBIT))
                return ns;
        }
        
        //如果当前是写锁,并且队列亿初始化,但是队列中没有其他节点,就尝试一直自旋获取锁。如果spins = 0.不满足((m = (s = state) & ABITS) == 0L)的话就要执行下面的入队操作了,因为spins  > 0 和 spins < 0 的分支都会直接跳过
        else if (spins < 0)
            spins = (m == WBIT && wtail == whead) ? SPINS : 0;
        else if (spins > 0) {
            //将自旋次数--,如果自旋次数==-1就会执行else if (spins < 0),再次判断是否自旋
            if (LockSupport.nextSecondarySeed() >= 0)
                --spins;
        }//如果wtail==null,也就是队列为空
        else if ((p = wtail) == null) { 
            //初始化queue
            WNode hd = new WNode(WMODE, null);
            //将tail==head
            if (U.compareAndSwapObject(this, WHEAD, null, hd))
                wtail = hd;
        }
        else if (node == null)//如果node还未初始化,就初始化node,
            node = new WNode(WMODE, p);//node为写锁模式,prev=p,p就是tail
        else if (node.prev != p)
            node.prev = p;//将node的前继节点设为tail
        //cas将node的设置为tail
        else if (U.compareAndSwapObject(this, WTAIL, p, node)) {
            p.next = node;//将原本的tail的next设置现在的tail node
            //打断自旋,现在已经入队了
            break;
        }
    }

    //已经入队了,开始挂起
    for (int spins = -1;;) {
        WNode h, np, pp; int ps;
        //p是node的前继节点
        if ((h = whead) == p) {//如果是队列中的第二个节点,就开始抢锁,第一个节点是哨兵节点
            if (spins < 0)
                spins = HEAD_SPINS;//1<<10
            else if (spins < MAX_HEAD_SPINS)//1<<16
                spins <<= 1;
            for (int k = spins;;) { 
                long s, ns;
                //开始申请锁
                if (((s = state) & ABITS) == 0L) {
                    if (U.compareAndSwapLong(this, STATE, s,
                                             ns = s + WBIT)) {
                        //申请锁成功,将node从queue中移除
                        whead = node;
                        node.prev = null;
                        return ns;
                    }
                }
                //抢锁失败,k--;
                else if (LockSupport.nextSecondarySeed() >= 0 &&
                         --k <= 0)
                    break;
            }
        }
        //如果head不为空
        else if (h != null) {
            WNode c; Thread w;
            //如果h为悲观读锁节点,则唤醒所有的悲观读锁节点,不理解的可以看上面WNode的图
            while ((c = h.cowait) != null) {
                //
                if (U.compareAndSwapObject(h, WCOWAIT, c, c.cowait) &&
                    (w = c.thread) != null)
                    //唤醒线程
                    U.unpark(w);
            }
        }
        //如果其他线程在上述代码执行期间没有释放锁,就尝试将线程挂起啊
        if (whead == h) {
            //如果node的前继节点不是p
            if ((np = node.prev) != p) {
                if (np != null)
                    //将p的next设置为node
                    (p = np).next = node;
            }
            else if ((ps = p.status) == 0)
                //将p的waitStatus设置为WATING
                U.compareAndSwapInt(p, WSTATUS, 0, WAITING);
            else if (ps == CANCELLED) {
                //如果p被取消了,将p踢出queue,并将p.prev和node连接起来
                if ((pp = p.prev) != null) {
                    node.prev = pp;
                    pp.next = node;
                }
            }
            else {
                long time; // 
                //deadline == 0说明抢锁没有时间限制
                if (deadline == 0L)
                    time = 0L;
                //如果有时间限制,并且超时的话,将node设置为cancel状态
                else if ((time = deadline - System.nanoTime()) <= 0L)
                    return cancelWaiter(node, node, false);
                //线程对象
                Thread wt = Thread.currentThread();
                //将线程加入挂起
                U.putObject(wt, PARKBLOCKER, this);
                node.thread = wt;
                //如果前继节点为WAITING,并且再一次抢锁失败就挂起
                if (p.status < 0 && (p != h || (state & ABITS) != 0L) &&
                    whead == h && node.prev == p)
                    //挂起
                    U.park(false, time);  // emulate LockSupport.park
                node.thread = null;
                U.putObject(wt, PARKBLOCKER, null);
                //如果被中断了,就将node设置为cancel
                if (interruptible && Thread.interrupted())
                    return cancelWaiter(node, node, true);
            }
        }
    }
}

抢写锁步骤:
1、先请求一次抢锁,抢不到转2
2、如果当前队列为空,就开始自旋抢锁,如果队列不为空就挂起
3、如果当前队列不为空了,并且在自旋期间没抢到锁,就开始挂起
4、将当前线程包装成WNode节点,并加入到queue的末尾
5、如果当前线程处于queue中的第二个节点,head是个哨兵节点,说明就很有可能抢到锁,就会开始新一轮的自旋抢锁。
6、如果不是在queue中的第二个节点,就将prev节点的waitstatus设置为WAITING(-1),并将waitstatus=CANCEL(1)的节点从queue中去除
5、挂起,但其他线程释放锁的时候,如果将当前线程唤醒就转到步骤5继续执行

unlockWrite

public void unlockWrite(long stamp) {
    WNode h;                   1000 0000  1000 0000
    //如果state != stamp,并且 stamp & WABIT == 0 说明被Stamp的写锁标志变为0了,这就属于异常了
    if (state != stamp || (stamp & WBIT) == 0L)
        throw new IllegalMonitorStateException();
    
    // state = stamp + WBIT 会把写锁标志的1变为0,并向前进1,也就是版本号会加1
    state = (stamp += WBIT) == 0L ? ORIGIN : stamp;
    
    //队列不为空,唤醒后继节点
    if ((h = whead) != null && h.status != 0)
        release(h);
}


private void release(WNode h) {
    if (h != null) {//队列不为空
        WNode q; Thread w;
        //将h的waitstatus变为0,
        U.compareAndSwapInt(h, WSTATUS, WAITING, 0);
        //1、h的next节点被取消了
        //2、h的next节点为null,并不代表队列为空,因为是在入队的时候先设置的node.prev = p,最后才设置的p.next = node,所以要从尾到头遍历
        if ((q = h.next) == null || q.status == CANCELLED) {
            for (WNode t = wtail; t != null && t != h; t = t.prev)
                //如果t为WAITING
                if (t.status <= 0)
                    q = t;
        }
        if (q != null && (w = q.thread) != null)
           //唤醒线程,去抢锁锁
           //之前挂起线程是在一个死循环中挂起的,直到超时或者申请成功锁后,才会退出
            U.unpark(w);
    }
}

释放写锁步骤
1、首先根据抢锁的时候返回的stamp来检验是否发生异常
2、如果没发生异常,就通过state+=WBIT将写锁的标志置为0,并将版本号+1
3、唤醒后继的第一个节点来抢锁

readLock()

public long readLock() {
    long s = state, next; 
    //1、队列为空
    //2、s & 255 < 126  如果 (s & ABITS) >= RFULL 说明当前为写模式,或者超出了读锁的最大线程个数   
    //       1111 1111
    //           & 
    //       1000 0000  = 1000 0000 = 128 写锁模式
    //       1111 1111    
    //       0111 1111  = 111 111 = 127  读模式 最多容纳 127个线程  
    //3、state = s + runit(1),也就是悲观读线程数+1 
    return ((whead == wtail && (s & ABITS) < RFULL &&
             U.compareAndSwapLong(this, STATE, s, next = s + RUNIT)) ?
            next : acquireRead(false, 0L));
}

//申请锁失败
private long acquireRead(boolean interruptible, long deadline) {
    WNode node = null, p;
    for (int spins = -1;;) {
        WNode h;
        //如果whead == wtail,队列为空
        if ((h = whead) == (p = wtail)) {
        
            for (long m, s, ns;;) {
                //开始抢锁
                if ((m = (s = state) & ABITS) < RFULL ?
                    U.compareAndSwapLong(this, STATE, s, ns = s + RUNIT) :
                    (m < WBIT && (ns = tryIncReaderOverflow(s)) != 0L))
                    //成功返回state
                    return ns;
                    
                //如果现在是写锁模式 m = state & ABITS
                else if (m >= WBIT) {
                    if (spins > 0) {
                        //自旋次数-1
                        if (LockSupport.nextSecondarySeed() >= 0)
                            --spins;
                    }
                    else {
                        //如果自旋次数用完,
                        if (spins == 0) {
                            //h为自旋前的whead,p为自旋前的wtail
                            WNode nh = whead, np = wtail;
                            //如果nh = h && np = p,说明在自旋期间根本没有释放锁,或者队列不为空,就挂起线程
                            if ((nh == h && np == p) || (h = nh) != (p = np))
                                break;
                        }
                        //spin = 64
                        spins = SPINS;
                    }
                }
            }
        }//第一阶段自旋代码到此结束


		//开始入队
        //如果tail = null,开始初始化queue,head只是一个哨兵节点
        if (p == null) { //初始化
            WNode hd = new WNode(WMODE, null);
            if (U.compareAndSwapObject(this, WHEAD, null, hd))
                wtail = hd;
        }
        else if (node == null)//初始化node,p是node的prev前继节点
            node = new WNode(RMODE, p);
        //如果队列为空,但是被初始化了,或者wtail节点对应的是写锁模式
        else if (h == p || p.mode != RMODE) {
            //设置node.prev = tail
            if (node.prev != p)
                node.prev = p;
            //将node插入到queue中
            else if (U.compareAndSwapObject(this, WTAIL, p, node)) {
                p.next = node;
                //停止自旋,开始挂起
                break;
            }
        }
        //p是node的前继节点,如果p的mode是读锁,就将node通过cowait指针来链接到p节点中。
        else if (!U.compareAndSwapObject(p, WCOWAIT,
                                         node.cowait = p.cowait, node))
            node.cowait = null;
        else {
        //插入前尾节点是读锁的情况,通过cowait加入,这时whead不是哨兵节点,而是读锁。当第一个读锁对象争夺锁的时候,会将whead=node的。
            for (;;) {
                WNode pp, c; Thread w;
                //如果whead为读锁的时候,将其cowait唤醒来抢锁,被唤醒的节点然后唤醒其cowait,速度很快。
                if ((h = whead) != null && (c = h.cowait) != null &&
                    U.compareAndSwapObject(h, WCOWAIT, c, c.cowait) &&
                    (w = c.thread) != null) 
                    U.unpark(w);

                //抢锁,当m<WBIT,此时无写锁
                if (h == (pp = p.prev) || h == p || pp == null) {
                    long m, s, ns;
                    do {
                        //自旋获取锁
                        if ((m = (s = state) & ABITS) < RFULL ?
                            U.compareAndSwapLong(this, STATE, s,
                                                 ns = s + RUNIT) :
                            (m < WBIT &&
                             (ns = tryIncReaderOverflow(s)) != 0L))
                            return ns;
                    } while (m < WBIT);
                }
                
                //如果之前自旋期间没有锁释放,就开始尝试挂起了
                if (whead == h && p.prev == pp) {
                    long time;
                    if (pp == null || h == p || p.status > 0) {
                        node = null; // throw away
                        break;//
                    }
                    if (deadline == 0L)
                        time = 0L;
                    //超时了,将node取消
                    else if ((time = deadline - System.nanoTime()) <= 0L)
                        return cancelWaiter(node, p, false);
					//后面就是将线程挂起
                    Thread wt = Thread.currentThread();
                    U.putObject(wt, PARKBLOCKER, this);
                    node.thread = wt;
                    if ((h != pp || (state & ABITS) == WBIT) &&
                        whead == h && p.prev == pp)
                        U.park(false, time);
                    node.thread = null;
                    U.putObject(wt, PARKBLOCKER, null);
                    if (interruptible && Thread.interrupted())
                        return cancelWaiter(node, p, true);
                }
            }
        }
    }

	//插入前的尾节点是写锁
    for (int spins = -1;;) {
        WNode h, np, pp; int ps;
        //如果node处于第二个节点,可以开始抢锁了,开始自旋吧
        if ((h = whead) == p) {
            if (spins < 0)//设置自旋次数
                spins = HEAD_SPINS;//1<<10
            else if (spins < MAX_HEAD_SPINS)//1<<16
                spins <<= 1;
            for (int k = spins;;) { // spin at head
                long m, s, ns;
                //开始抢锁,
                //如果m < 126 就直接将state+=1
                //如果m == 126,就会调用tryIncReaderOverflow(),代码如下
         //if ((s & ABITS) == RFULL) {
         // s 后8为 126 ,RBITS=127,s|RBITS的后八位 为 1,然后将readerOverflow++,state 的后八位 为 1.    出现异常就返回0
	     //     if (U.compareAndSwapLong(this, STATE, s, s | RBITS)) {
	     //         ++readerOverflow;
	     //         state = s;
	     //         return s;
	     //     }
	     // }
                if ((m = (s = state) & ABITS) < RFULL ?
                    U.compareAndSwapLong(this, STATE, s, ns = s + RUNIT) :
                    //如果m == RFULL(126),调用tryIncReaderOverflow()
                    (m < WBIT && (ns = tryIncReaderOverflow(s)) != 0L)) {//抢锁成功了
                    WNode c; Thread w;
                    //将whead设置为node
                    whead = node;
                    //断了node和whead的联系
                    node.prev = null;
                    //通过cowait依次将node的cowait唤醒
                    while ((c = node.cowait) != null) {
                        if (U.compareAndSwapObject(node, WCOWAIT,
                                                   c, c.cowait) &&
                            (w = c.thread) != null)
                            U.unpark(w);
                    }
                    return ns;
                }
                //如果被写锁抢占了,就break,将线程挂起
                else if (m >= WBIT &&
                         LockSupport.nextSecondarySeed() >= 0 && --k <= 0)
                    break;
            }
        }
        else if (h != null) {//如果将node设置为whead了
            WNode c; Thread w;
            while ((c = h.cowait) != null) {//唤醒node的cowait去抢锁
                if (U.compareAndSwapObject(h, WCOWAIT, c, c.cowait) &&
                    (w = c.thread) != null)
                    U.unpark(w);
            }
        }
        
        if (whead == h) {//如果没有将node设置为whead,就该挂起线程了
            //为node找到合适的p,并将p设置为WAITING
            if ((np = node.prev) != p) {
                if (np != null)
                    (p = np).next = node;   
            }
            else if ((ps = p.status) == 0)
                U.compareAndSwapInt(p, WSTATUS, 0, WAITING);
            else if (ps == CANCELLED) {
                if ((pp = p.prev) != null) {
                    node.prev = pp;
                    pp.next = node;
                }
            }
            else {
                long time;
                if (deadline == 0L)
                    time = 0L;
                else if ((time = deadline - System.nanoTime()) <= 0L)
                    return cancelWaiter(node, node, false);
                Thread wt = Thread.currentThread();
                U.putObject(wt, PARKBLOCKER, this);
                node.thread = wt;
                if (p.status < 0 &&
                    (p != h || (state & ABITS) == WBIT) &&
                    whead == h && node.prev == p)
                    U.park(false, time);
                node.thread = null;
                U.putObject(wt, PARKBLOCKER, null);
                if (interruptible && Thread.interrupted())
                    return cancelWaiter(node, node, true);
            }
        }
    }
}

读锁抢锁步骤
1、首先判断whead == wtail && (s & ABITS) < RFULL &&
U.compareAndSwapLong(this, STATE, s, next = s + RUNIT),这时就算申请成功

2、如果申请不成功,就尝试自旋申请锁,如果whead == wtail,就会自旋申请锁,如果whead!=wtail就退出自旋。

3、如果申请不成功,就将线程包装为WNode的node,然后这时分两种情况。

4、情况1:当前尾节点是写锁,就将node通过next和prev与尾节点联系起来,将node设置为新的尾节点。这个情况下,如果node满足是队列中的第二个节点,就开始自旋申请锁。申请成功后会将node设置为whead。

如果不满足队列中第二个节点的条件就将线程挂起

5、情况2:如果当前尾节点是读锁,就将node通过cowait指针与尾节点联系起来。如果这时h.cowait不为空,说明情况4已经获取到锁了,whead是一个读锁。如果h.cowait不为空,就会将其对应的线程唤醒来抢锁。唤醒的线程会继续将其cowait唤醒来抢锁。

如果h.cowait为空,说明情况4的锁还未申请成功,这个时候就会将线程挂起。

读锁的释放

public void unlockRead(long stamp) {
    long s, m; WNode h;
    for (;;) {
       //将stamp与state检验,如果不符合就说明出现异常了,抛出异常。
        if (((s = state) & SBITS) != (stamp & SBITS) ||
            (stamp & ABITS) == 0L || (m = s & ABITS) == 0L || m == WBIT)
            throw new IllegalMonitorStateException();
 
      
        if (m < RFULL) {

            if (U.compareAndSwapLong(this, STATE, s, s - RUNIT)) {
                if (m == RUNIT && (h = whead) != null && h.status != 0)
                    release(h);
                break;
            }
        }
        // m = 126 ,ABITS =  255 ,则state = 126
        //如果readerOverflow != 0,并且m = RFULL,将state设置为126
        else if (tryDecReaderOverflow(s) != 0L)
            break;
    }
}

private long tryDecReaderOverflow(long s) {
    //如果ABITS = 255 ,s的后八位为 126 ,ABITS & s = RFULL 
    if ((s & ABITS) == RFULL) {
               
        if (U.compareAndSwapLong(this, STATE, s, s | RBITS)) {
            int r; long next;
            //如果readerOverflow > 0, 将state = s|RBITS(127) ,state的后8位为1
            if ((r = readerOverflow) > 0) {
                readerOverflow = r - 1; 
                next = s;
            }
            else
                next = s - RUNIT;//state-1;
             
             state = next;
             return next;
        }
    }
    else if ((LockSupport.nextSecondarySeed() &
              OVERFLOW_YIELD_RATE) == 0)
        Thread.yield();
    return 0L;
}

tryOptimisticRead
StampedLock支持乐观读,乐观读并不会加锁,所以会提高读的效率。

public long tryOptimisticRead() {
    long s;
     //state&WBIT == 0,说明当前不存在写锁,返回 s& SBITS
     //WBIT = 128  SBITS = ~RBITS(127),SBITS的后7位为0,其他都为1
    //s & SBITS 就是我们前25位,版本号+锁标志(0)。
    
    //state&WBIT != 0,说明当前存在写锁,返回0
    
    return (((s = state) & WBIT) == 0L) ? (s & SBITS) : 0L;
}

//验证
public boolean validate(long stamp) {
    U.loadFence();//使用LoadLoad屏障,禁止将之前的读取成员属性的操作重排序到后面的验证逻辑之后 
     
    //将之前返回的stamp和当前state对比
	//SBITS = ~RBITS(127)
   //SBITS的后7位为0,其他都为1
   //一、stamp = 0,有写锁。 stamp & SBITS = 0, 
  state & SBITS
   1、当前有写锁,锁标志位肯定为1 ,那么 state & SBITS肯定不等0,最起码锁标志对应的那一位就肯定为1,state & SBITS != stamp & SBITS
   2、当前无锁,之前的写锁释放,版本号肯定+1,不为0。state & SBITS肯定也不等0,state & SBITS != stamp & SBITS
 
  //二、stamp,无写锁, stamp & SBITS就是版本号+锁标志0。
   1、当前有锁,锁标志位肯定为1 ,那么 state & SBITS的最后一位锁标志是1,不相等
   2、当前无锁,如果不存在写锁的申请和是释放,版本号不变,state & SBITS == stamp & SBITS ,版本号一样,锁标志还是0
               如果存在写锁的申请和释放,版本号会变, state & SBITS != stamp & SBITS,虽然锁标志还是0,但是版本号不一样。

综上所述,当在乐观读中间发生写操作的时候,会返回false
                      
    return (stamp & SBITS) == (state & SBITS);
}

总结

StampedLock也是基于state和队列实现的,只不过不能重入。通过引入了乐观读并不加锁,在读多写少的场景中性能会比ReentrantReadWriteLock要好!

StampedLock和ReentrantReadWriteLock的队列也有区别:
1、StampedLock中的连续申请的读锁节点通过cowaiter新起了一个维度链表来存放。而ReentrantReadWriteLock的读锁节点是一个一个的排列在队列中的。
在这里插入图片描述

在这里插入图片描述

posted @ 2021-08-25 08:42  张孟浩Jay  阅读(3737)  评论(1编辑  收藏  举报