大厂之路一由浅入深、并行基础、源码分析一 “J.U.C.L”之StampedLock源码分析

StampedLock的由来

  • `StampedLock类:在JDK1.8时引入,是对读写锁ReentrantReadWriteLock的增强,该类提供了一些功能,优化了读锁、写锁的访问,同时使读写锁之间可以互相转换,更细粒度控制并发
    • 之所以说是对读写锁ReentrantReadWriteLock的增强,是因为其会产生写饥饿问题,而`StampedLock就解决了这个问题;

StampedLock的特点

  • StampedLock: 这是一个什么东西呢?英文单词Stamp,意思是邮戳
  • 所有获取锁的方法,都返回一个邮戳:Stamp为0表示获取失败,其余都表示成功
  • 所有释放锁的方法,都需要一个邮戳:这个Stamp必须是和成功获取锁时得到的Stamp一致
  • StampedLock是 不可重入 的:
    • 注意: 如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁;
  • `StampedLock有三种访问模式:
    • `Reading(读模式):功能和ReentrantReadWriteLock的读锁类似;
    • `Writing(写模式):功能和ReentrantReadWriteLock的写锁类似;
    • `Optimistic reading(乐观读模式):这是一种优化的读模式
      • 这是最主要的特点,乐观读模式,即使读线程获取到了读锁,写线程尝试获取写锁也不会阻塞,这相当于对读模式的优化,但是可能会导致数据不一致的问题。所以,当使用Optimistic reading获取到读锁时,必须对获取结果进行校验
  • `StampedLock:支持读锁和写锁的相互转换
  • 在`ReentrantReadWriteLock中,当线程获取到写锁后,可以降级为读锁,但是读锁是不能直接升级为写锁的;
    • 而`StampedLock提供了读锁和写锁相互转换的功能,使得该类支持更多的应用场景;
  • 不过无论写锁还是读锁,都不支持`Conditon等待

StampedLock的基本使用

  • 在`StampedLock的Oracle官方文档中就提供了一个非常好的例子,让我们可以很快的理解StampedLock的使用,如下:
class Point {
    private double x, y;
    private final StampedLock sl = new StampedLock();
	// 第一种模式:Writing(写模式),这个也是独占锁,没有什么改动,注意stamp的运用即可
	//第二种模式同理
    void move(double deltaX, double deltaY) {
        long stamp = sl.writeLock();    //涉及对共享资源的修改,使用写锁-独占操作
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            sl.unlockWrite(stamp);
        }
    }
    /**
     * 第三种模式:`Optimistic reading`(乐观读模式),使用乐观读锁访问共享资源
     * 注意:乐观读锁在保证数据一致性上需要拷贝一份要操作的变量到 方法栈🧡,并且在操作数据时候可能其他写线程已经修改了数据,
     * 因此需要校验
     */
    🧡注意点1double distanceFromOrigin() {
        long stamp = sl.tryOptimisticRead();    // 获取`Optimistic reading`(乐观读模式)
        double currentX = x, currentY = y;      // 拷贝共享资源到本地方法栈中
   🧡注意点2if (!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
        // 这里是悲观锁策略readLock,同样也可以用乐观锁策略来举例
        long stamp = sl.readLock();
        try {
        //相当于自旋,满足条件则不停尝试
            while (x == 0.0 && y == 0.0) {
                long ws = sl.tryConvertToWriteLock(stamp);  //读锁升级为写锁,ws=0表示失败,反之升级成功
                if (ws != 0L) {								//升级成功,则修改数据并返回
                    stamp = ws;
                    x = newX;
                    y = newY;
                    break;
                } else {
                //升级写锁失败,则显示释放读锁并且显示获取写锁
                    sl.unlockRead(stamp);
                    stamp = sl.writeLock();
                }
            }
          } finally {				//因为不知道是否能升级成功,则通过通用式解锁
🧡注意点3:      	 sl.unlock(stamp);
       	 }
      }
 }
  • 看完官方提供的例子,我们要注意几个地方:
  • 注意点2:
    public boolean validate(long stamp) {
        U.loadFence();
        return (stamp & SBITS) == (state & SBITS);
    }
  • 参数是上次锁操作返回的邮戳:
    • 如果在调用validate()之前,如果没有写锁被占用,那就返回true:表示锁保护的共享数据并没有被修改,因此之前的读取操作是肯定能保证数据完整性和一致性的,直接返回即可;
    • 如果在调用validate()之前,如果有写锁被占用,那就返回false:表示之前的数据读取和写操作冲突了,程序需要进行重试(多自旋几次),或者升级为悲观读锁。
  • 注意点1:
  • “Optimistic reading”的使用模板:
long stamp = lock.tryOptimisticRead();  	// 乐观读模式返回的邮戳
copyVaraibale2ThreadMemory();         	    // 拷贝变量到线程本地堆栈
	if(!lock.validate(stamp)){              // 校验是否有写锁被占用
	    long stamp = lock.readLock();       // 如果有写锁被占用则获取读锁
	    try {
	        copyVaraibale2ThreadMemory();   // 拷贝变量到线程本地堆栈
	        //---相关操作
	     } finally {
	       lock.unlockRead(stamp);              // 释放悲观读锁
	    }
	}
useThreadMemoryVarables();                  // 使用线程本地堆栈里面的数据进行操作
  • 注意点3:
    public void unlock(long stamp) {
        if ((stamp & WBIT) != 0L)
            unlockWrite(stamp);
        else
            unlockRead(stamp);
    }
  • 用于 写锁➡读锁 的释放锁用,因为不知道转没转成功,则通过判断释放读锁或写锁。

和重入锁的比较

  • 通过上面的例子,我们不难看出,StampedLock的编程复杂度要比重入锁复杂的多,代码也不是很简介,那为什么还要用呢?
    • 最本质的原因就是性能的提升,一般来说,这种乐观锁的性能要比普通的重入锁快几倍,而且随着线程数量的不断增加,性能的差距会越来越大。
    • 简而言之,在大量并发的场景中StampedLock的性能是碾压重入锁和读写锁的
    • 不过它也有缺点,我们前面多少也提到一些,总结如下:
      • 如果使用乐观读,那么冲突的场景要应用自己处理;
      • 不可重入,如果连续调用两次写锁请求,那就会死锁!!!!!
      • 它不支持wait/notify机制
  • 不过如果能很好的理解StampedLock,那么它是首选!!!!

StampedLock的原理分析

  • StampedLock虽然不像其它锁一样定义了内部类来实现AQS框架,但是StampedLock的基本实现思路还是利用CLH队列进行线程的管理,通过同步状态值来表示锁的状态和类型

StampedLock的常用属性

  • StampedLock内部定义了很多常量,定义这些常量的根本目的还是和ReentrantReadWriteLock一样,对同步状态值按位切分,以通过位运算对State进行操作:
  • state:
    • 这是一个64位的整数,long,StampedLock对它的使用是非常巧妙的;
    • 如何巧妙呢?写锁被占用的标志是第8位为1,读锁使用低7位,正常情况下读锁数目为1-1260有特殊含义,127用来表示读状态标识RBITS),超过126时,使用int整型的readerOverflow属性保存超出的读锁计数(图来自上面所写参考的博客)(这里的写锁释放次数是不是应该表示为写锁释放次数➕1呢? 因为1表示初始状态);
      在这里插入图片描述
  • state的相关位运算:🧡🧡🧡
    private static final int LG_READERS = 7; 			//位数,这7位一般用来表示读锁的那7位	
    private static final long RUNIT = 1L;				//单位读锁 		...	0000 0001
    private static final long WBIT  = 1L << LG_READERS;	//写锁标志位			...	1000 0000
    private static final long RBITS = WBIT - 1L;		//读状态标识			...	0111 1111
    private static final long RFULL = RBITS - 1L;		//读锁的最大数量		...	0111 1110(126)
    private static final long ABITS = RBITS | WBIT;		//用于获取读写状态	...	1111 1111
    private static final long SBITS = ~RBITS; 			//			  111111... 1000 0000

    private static final long ORIGIN = WBIT << 1;       //初始化state值    ...1 0000 0000  
       
    private transient volatile long state;				//同步状态
    
    private transient int readerOverflow;				//读锁数量超过126,用readerOverflow记录
  • 那具体怎么记录读锁次数,写锁次数呢?
    • 首先state同步状态初始值是ORIGIN:…0001 0000 0000;(不能是0,因为有特殊含义)
    • 当有写锁占用时,即state=state➕WBIT : … 0001 1000 0000;
    • 释放写锁,即再加wait,state=state➕WBIT : … 0010 0000 0000; (如果再加锁,就变成了 0010 1000 0000)
    • 如果继续读锁占用,就直接加1即: 0010 0000 0001
  • 为什么要记录写锁释放的次数呢?
    • 因为state的同步状态判断都是基于CAS操作的,而普通的CAS操作可能会遇到 “ABA”问题,如果不记录次数,那么当写锁释放,申请,再释放时,我们将无法判断数据是否被写过;而这里记录了释放的次数,因此出现"释放->申请->释放"的时候,CAS操作就可以检查到数据的变化,从而判断写操作已经有发生,作为一个乐观锁来说,就可以准确判断冲突已经产生,剩下的就是交给应用来解决冲突即可。因此,这里记录释放锁的次数,是为了精确地监控线程冲突。

StampedLock的内部数据结构

  • 这里简单介绍一下它的数据结构:
    • StampedLock中,有一个队列,里面存放着等待在锁上的线程。该队列是一个链表,链表中的元素是一个叫做Node的对象;
    • 我们先看其Node类结构图和源码
      在这里插入图片描述
    abstract static class Node {   //JDK16
        volatile Node prev;       // initially attached via casTail
        volatile Node next;       // visibly nonnull when signallable
        Thread waiter;            // visibly nonnull when enqueued
        volatile int status;      // written by owner, atomic bit ops by others
		//---省略本质是一些native方法操作
    }
    static final class WriterNode extends Node { // node for writers}  

    static final class ReaderNode extends Node { // node for readers
        volatile ReaderNode cowaiters;           // 等待的读模式节点       jdk8中用的cowait 
						
 		//---省略本质是一些native方法操作
    }
        // Bits for Node.status
    static final int WAITING   = 1;       //JDK8  为 -1    
    static final int CANCELLED = 0x80000000; // must be negative  
    /** Head of CLH queue */
    private transient volatile Node head;																																																																																																																										
    /** Tail (last) of CLH queue */
    private transient volatile Node tail;
    /*JDK8中不同的地方
      static final int WAITING   = -1      static final int CANCELLED = 1
      在Node中还会有一个属性 final int mode   : RMODE =0、WMODE =1分别来表示ReaderNode、WriterNode
      并多了一个构造器
      WNode(int m ,WNode p){  WNode就是JDK16中的Node
      	mode = m;
      	prev = p;
      }
    */
  • 可知:

    • Node是一个抽象类,具体由 WriterNode,ReaderNode实例类来实现;
    • JDK8中其结点是这样的(均网图,来自参考文献):
      在这里插入图片描述
  • 当队列中有若干个线程等待时,整个队列可能看起来像这样的:
    在这里插入图片描述

StampedLock对多核CPU优化

  • StampedLock相比ReentrantReadWriteLock,对多核CPU进行了优化,可以看到,当CPU核数超过1时,会有一些自旋操作(转自别人博客):
    在这里插入图片描述

StampedLock对象的创建

  • StampedLock构造器,构造时设置下同步状态值为ORIGIN
    public StampedLock() {
        state = ORIGIN;
    }
  • StamedLock提供了三类视图:
   // views 视图
    transient ReadLockView readLockView;
    transient WriteLockView writeLockView;
    transient ReadWriteLockView readWriteLockView;
  • 这些视图其实是对StamedLock方法的封装,便于习惯了ReentrantReadWriteLock的用户使用:
  • 比如,ReadWriteLockView 的两个方法:
    final class ReadWriteLockView implements ReadWriteLock {
        public Lock readLock() { return asReadLock(); }
        public Lock writeLock() { return asWriteLock(); }
    }

StampedLock读锁的申请和释放

  • 让我们先看一下StampedLock对读锁的申请(读锁比写锁复杂):
/*
* 获取读锁时,如果写锁被占用,线程阻塞,且方法不相应中断,返回非0表示成功
*/
    public long readLock() {
        long s = state, next;  
  //条件1:(whead == wtail 表示:表示队列为空,或者只有一个结点
  //条件2:(s & ABITS) < RFULL 表示:写锁没有被占用,并且读锁数量没有超过界限126
  //同时满足条件1、2再去修改同步状态,让其读锁+1, 成功返回next,失败则自旋请求读锁,求不到则入队,入队还自旋,实在不行挂起
        return ((whead == wtail && (s & ABITS) < RFULL &&   
                 U.compareAndSwapLong(this, STATE, s, next = s + RUNIT)) ? next : acquireRead(false, 0L));
    }
  • 接下来我们看一下acquireRead(boolean interruptible, long deadline) 源码
  • 这个方法非常复杂,用到了大量自旋操作,我就按别人的分析➕自己的理解来写:
/**
 * 尝试自旋的获取读锁, 获取不到则加入等待队列, 并阻塞线程
 *
 * @param interruptible true 表示检测中断, 如果线程被中断过, 则最终返回INTERRUPTED
 * @param deadline      如果非0, 则表示限时获取
 * @return 非0表示获取成功, INTERRUPTED表示中途被中断过
 */
private long acquireRead(boolean interruptible, long deadline) {
    WNode node = null, p;   // node:指向入队结点, p:指向入队前的队尾结点
    /**
     * 自旋入队操作:
     * 如果写锁未被占用, 则立即尝试获取读锁;
     * 如果写锁被占用, 则将当前读线程包装成结点, 并插入等待队列(如果队尾是写结点,直接链接到队尾;否则,链接到队尾读结点的栈中)
     */
    for (int spins = -1; ; ) {
        WNode h;
        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:但读锁数量超限, 超出部分放到readerOverflow字段中
                    (m < WBIT && (ns = tryIncReaderOverflow(s)) != 0L))        
                    return ns;          // 获取成功后, 直接返回同步状态ns
                else if (m >= WBIT) {   // 写锁被占用,以随机方式探测是否要退出自旋
                    if (spins > 0) {
                        if (LockSupport.nextSecondarySeed() >= 0)
                            --spins;
                    } else {
                        if (spins == 0) {
                            WNode nh = whead, np = wtail;
                            if ((nh == h && np == p) || (h = nh) != (p = np))
                                break;
                        }
                        spins = SPINS;
                    }
                }
            }
        }
        if (p == null) {   // 表示队列为空, 则初始化队列
            WNode hd = new WNode(WMODE, null);  //构造头节点
            if (U.compareAndSwapObject(this, WHEAD, null, hd))    
                wtail = hd;
        } else if (node == null) {                  // 将当前线程包装成读结点,并指向队尾结点
            node = new WNode(RMODE, p);
        } else if (h == p || p.mode != RMODE) {     
	// 如果队列只有一个头结点, 或队尾结点不是读结点, 则直接将结点链接到队尾, 链接完成后退出自旋
            if (node.prev != p)
                node.prev = p;
            else if (U.compareAndSwapObject(this, WTAIL, p, node)) {		
                p.next = node;
                break; 						//退出自旋
            }
        }
     // 队列不为空, 且队尾是读结点, 则将添加当前结点链接到队尾结点的cowait链中(实际上构成一个栈, p是栈顶指针 )
        else if (!U.compareAndSwapObject(p, WCOWAIT, node.cowait = p.cowait, node)) {    
	 // CAS操作队尾结点p的cowait字段,实际上就是头插法(node.cowait = p.cowait)插入结点(头插法就是按栈的规则进行的)
            node.cowait = null;
        } else {
            for (; ; ) {
                WNode pp, c;
                Thread w;
                // 尝试唤醒头结点的cowait中的第一个元素, 假如是读锁会通过循环释放cowait链
                if ((h = whead) != null && (c = h.cowait) != null &&
                    U.compareAndSwapObject(h, WCOWAIT, c, c.cowait) &&
                    (w = c.thread) != null) // help release
                    U.unpark(w);  //唤醒,不让其挂起了
                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;
                    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;
        if ((h = whead) == p) {     // 如果当前线程是队首结点, 则尝试获取读锁(我认为是第二个结点,头节点为空)
            if (spins < 0)
                spins = HEAD_SPINS;
            else if (spins < MAX_HEAD_SPINS)
                spins <<= 1;
            for (int k = spins; ; ) { // spin at head
                long m, s, ns;
                if ((m = (s = state) & ABITS) < RFULL ?     // 判断写锁是否被占用
                    U.compareAndSwapLong(this, STATE, s, ns = s + RUNIT) :  //写锁未占用,且读锁数量未超限, 则更新同步状态
                    (m < WBIT && (ns = tryIncReaderOverflow(s)) != 0L)) {     
            	    //写锁未占用,但读锁数量超限, 超出部分放到readerOverflow字段中
                    // 获取读锁成功, 释放cowait链中的所有读结点
                    WNode c;
                    Thread w;
                    // 释放头结点, 当前队首结点成为新的头结点
                    whead = node;
                    node.prev = null;
                    // 从栈顶开始(node.cowait指向的结点), 依次唤醒所有读结点, 最终node.cowait==null, node成为新的头结点
                    while ((c = node.cowait) != null) {
                        if (U.compareAndSwapObject(node, WCOWAIT, c, c.cowait) && (w = c.thread) != null)
                            U.unpark(w);
                    }
                    return ns;
                } else if (m >= WBIT &&
                    LockSupport.nextSecondarySeed() >= 0 && --k <= 0)
                    break;
            }
        } else if (h != null) {     // 如果头结点存在cowait链, 则唤醒链中所有读线程
            WNode c;
            Thread w;
            while ((c = h.cowait) != null) {
                if (U.compareAndSwapObject(h, WCOWAIT, c, c.cowait) &&
                    (w = c.thread) != null)
                    U.unpark(w);
            }
        }
        if (whead == h) {
            if ((np = node.prev) != p) {
                if (np != null)
                    (p = np).next = node;   // stale
            } else if ((ps = p.status) == 0)        // 将前驱结点的等待状态置为WAITING, 表示之后将唤醒当前结点
                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) {
	 // .status < 0 :如果前驱的等待状态为WAITING, (state & ABITS) == WBIT) :且写锁被占用, 则阻塞(挂起)当前调用线程
                    U.park(false, time);
                }
                node.thread = null;
                U.putObject(wt, PARKBLOCKER, null);
                if (interruptible && Thread.interrupted())
                    return cancelWaiter(node, node, true);
            }
        }
    }
}
  • 看完一遍又一遍,忍不住说一声牛逼!
  • 总之,就是自旋,自旋再自旋,通过不断的自旋来尽可能避免线程被真的挂起,只有当自旋充分失败后,才会真正让线程去等待。
  • 读锁的释放:
public void unlockRead(long stamp) {
    long s, m; WNode h;
    for (;;) {
        // 检查同步状态,如果不匹配,或者没有被读写锁占用,则抛出异常
        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)) {            // 读锁数量-1
                // 如果当前读锁数量为1,且头节点不为空,状态不为初始态,则唤醒
                if (m == RUNIT && (h = whead) != null && h.status != 0)
                    release(h);
                break;
            }
        }
        else if (tryDecReaderOverflow(s) != 0L) 
            // 读线程个数溢出检测,则溢出数段-1
            break;
    }
}
注意,当读锁的数量变为0时才会调用release方法,唤醒队首结点:🧡🧡🧡🧡🧡🧡
private void release(WNode h) {
    if (h != null) {
        WNode q; Thread w;
        // 将其状态改为0
        U.compareAndSwapInt(h, WSTATUS, WAITING, 0);
        // 如果头节点的下一个节点为空或者其状态为已取消
        if ((q = h.next) == null || q.status == CANCELLED) {
            // 从尾节点向前遍历找到一个可用的(状态为WAITING)的节点
            for (WNode t = wtail; t != null && t != h; t = t.prev)
                if (t.status <= 0)
                    q = t;
        }
        // 唤醒
        if (q != null && (w = q.thread) != null)
            U.unpark(w);
    }
}

StampedLock写锁的申请和释放

  • 让我们再看一下StampedLock对写锁的申请:
/*
* 获取写锁,返回0表示失败,进入阻塞,且方法不相应中断,非0为成功
*/
    public long writeLock() {
        long s, next;   
        return ((((s = state) & ABITS) == 0L &&   //((s = state) & ABITS) == 0L表示读锁和写锁都未被使用
                 U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) ? //CAS操作:将第8位置为1,表示写锁被占用
                next : acquireWrite(false, 0L)); //如果成功,返回next,失败就调用acquireWrite(),加入等待队列
    }
  • 接下来我们看一下acquireWrite(boolean interruptible, long deadline) 源码
/**
 * 尝试自旋的获取写锁, 获取不到则阻塞线程
 *
 * @param interruptible true 表示检测中断, 如果线程被中断过, 则最终返回INTERRUPTED
 * @param deadline      如果非0, 则表示限时获取
 * @return 非0表示获取成功, INTERRUPTED表示中途被中断过
 */
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;                                                 // 获取成功直接返回
        } else if (spins < 0)
            spins = (m == WBIT && wtail == whead) ? SPINS : 0;
        else if (spins > 0) {
            if (LockSupport.nextSecondarySeed() >= 0)
                --spins;
        } else if ((p = wtail) == null) {       // 队列为空, 则初始化队列
            WNode hd = new WNode(WMODE, null);
            if (U.compareAndSwapObject(this, WHEAD, null, hd))
                wtail = hd;
        } else if (node == null)               // 将当前线程包装成写结点,入队
            node = new WNode(WMODE, p);
        else if (node.prev != p)
            node.prev = p;
        else if (U.compareAndSwapObject(this, WTAIL, p, node)) {    // 链接结点至队尾
            p.next = node;
            break;
        }
    }
    for (int spins = -1; ; ) {
        WNode h, np, pp;
        int ps;
        if ((h = whead) == p) {     // 如果当前结点是队首结点, 则立即尝试获取写锁(同样的疑惑,应该是前置结点)
            if (spins < 0)
                spins = HEAD_SPINS;
            else if (spins < MAX_HEAD_SPINS)
                spins <<= 1;
            for (int k = spins; ; ) { // spin at head
                long s, ns;
                if (((s = state) & ABITS) == 0L) {      // 写锁未被占用
                    if (U.compareAndSwapLong(this, STATE, s,
                        ns = s + WBIT)) {               // CAS修改State: 占用写锁
                        // 将队首结点从队列移除
                        whead = node;
                        node.prev = null;
                        return ns;
                    }
                } else if (LockSupport.nextSecondarySeed() >= 0 &&
                    --k <= 0)
                    break;
            }
        } else if (h != null) {  // 唤醒头结点的栈中的所有读线程
            WNode c;
            Thread w;
            while ((c = h.cowait) != null) {
                if (U.compareAndSwapObject(h, WCOWAIT, c, c.cowait) && (w = c.thread) != null)
                    U.unpark(w);
            }
        }
        if (whead == h) {
            if ((np = node.prev) != p) {
                if (np != null)
                    (p = np).next = node;   // stale
            } else if ((ps = p.status) == 0)        // 将当前结点的前驱置为WAITING, 表示当前结点会进入阻塞, 前驱将来需要唤醒我
                U.compareAndSwapInt(p, WSTATUS, 0, WAITING);
            else if (ps == CANCELLED) {
                if ((pp = p.prev) != null) {
                    node.prev = pp;
                    pp.next = node;
                }
            } else {        // 阻塞当前调用线程
                long time;  // 0 argument to park means no timeout
                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) != 0L) && whead == h && node.prev == p)
                    U.park(false, time);    // emulate LockSupport.park
                node.thread = null;
                U.putObject(wt, PARKBLOCKER, null);
                if (interruptible && Thread.interrupted())
                    return cancelWaiter(node, node, true);
            }
        }
    }
}
  • 通过源码我们可以总结一下,acquireWrite() 大体做了下面几件事情:
    • 入队操作:
      • 如果头结点等于尾结点wtail == whead, 表示快轮到我了,所以进行自旋等待,抢到就结束了;
      • 如果wtail==null ,说明队列都没初始化,就初始化一下队列;
      • 如果队列中有其他等待结点,那么只能老老实实入队等待了;
    • 阻塞并等待:
      • 如果头结点等于前置结点 (h = whead) == p), 那说明也快轮到我了,不断进行自旋等待争抢;
      • 否则唤醒头结点中的读线程;
      • 如果抢占不到锁,那么就park()当前线程;(下图为参考博客中的第二篇中的图)
        在这里插入图片描述
  • 总而言之,acquireWrite()函数就是用来争抢锁的,它的返回值就是代表当前锁状态的邮戳,同时,为了提高锁的性能,acquireWrite()使用大量的自旋重试,因此,它的代码看起来有点晦涩难懂。
  • 写锁的释放:
    public void unlockWrite(long stamp) {
        WNode h;
        /*
        条件1:检查当前同步状态和当时获取锁的时候的同步状态(邮戳)是否相同
        条件2:读写锁都未持有
        满足两个条件其中的一个,就抛出异常
        */
        if (state != stamp || (stamp & WBIT) == 0L)
            throw new IllegalMonitorStateException();
        // 正常情况下,stamp+=WBIT第8位应该位0,表示写锁释放,如果溢出,置为初始态ORIGN
        state = (stamp += WBIT) == 0L ? ORIGIN : stamp;
        // 头结点不为空,且不失初始状态(因为都是阻塞状态(WAITING)),尝试唤醒后续的线程
        if ((h = whead) != null && h.status != 0)
            //唤醒等待队列的(unpark)后续对首线程
            release(h);
    }
  • realese() 方法很简单,先将头结点的等待状态置为0,表示即将唤醒后继结点,然后立即唤醒下一个为等待状态的结点:
private void release(WNode h) {
    if (h != null) {
        WNode q; Thread w;
        // 将其状态改为0
        U.compareAndSwapInt(h, WSTATUS, WAITING, 0);
        // 如果头节点的下一个节点为空或者其状态为已取消
        if ((q = h.next) == null || q.status == CANCELLED) {
            // 从尾节点向前遍历找到一个可用的节点
            for (WNode t = wtail; t != null && t != h; t = t.prev)
                if (t.status <= 0)
                    q = t;
        }
        // 唤醒q节点所在的线程
        if (q != null && (w = q.thread) != null)
            U.unpark(w);
    }
}

StampedLock悲观读占满CPU的问题

  • 我们通过一个例子,说明StampedLock悲观锁疯狂占用CPU的问题:
public class StampedLockTest {
    public static void main(String[] args) throws InterruptedException {
        final StampedLock lock = new StampedLock();
        Thread t1 = new Thread(() -> {
            // 获取写锁
            lock.writeLock();
            // 模拟程序阻塞等待其他资源
            LockSupport.park();
        });
        t1.start();
        // 保证t1获取写锁
        Thread.sleep(100);
        Thread t2 = new Thread(() -> {
            // 阻塞在悲观读锁
            lock.readLock();
        });
        t2.start();
        // 保证t2阻塞在读锁
        Thread.sleep(100);
        // 中断线程t2,会导致线程t2所在CPU飙升
        t2.interrupt();
        t2.join();
    }
}
  • 上述代码中,在中断t2后,t2CPU占用率就会沾满100%。而这时候,t2正阻塞在readLock()方法上,换言之,在受到中断后,StampedLock的读锁有可能会占满CPU。
  • 这是什么原因呢? 因为StampedLock内太多的自旋引起的!
    • 如果没有中断,那么阻塞在readLock()上的线程在经过几次自旋后,会进入park()等待,一旦进入park()等待,就不会占用CPU了;
    • 但是park() 这个方法有一个特点,就是一旦线程被中断,**park()**就会立即返回,但是它不会抛出异常!!!!
    • 本来呢,你是想在锁准备好的时候,unpark()的线程的,但是现在锁没好,你直接中断了,park()也返回了,但是,毕竟锁没好,所以就又去自旋了
    • 转着转着,又转到了park() 方法,但悲催的是,线程的中断标记一直打开着,park() 就阻塞不住了,于是,下一个自旋又开始了,没完没了的自旋停不下来了,所以CPU就爆满了;
  • 那怎么解决这个问题呢?
    • 要解决这个问题,本质上需要在StampedLock内部,在park()返回时,需要判断中断标记为,并作出正确的处理,比如,退出,抛异常,或者把中断位给清理一下,都可以解决问题。
    • 但很不幸,至少在JDK8里,还没有这样的处理。因此就出现了上面的,中断readLock()后,CPU爆满的问题
    • 感谢敖丙大神的分析!!!

总结:

StampedLock的等待队列与RrentrantReadWriteLock的CLH队列相比,有以下特点:

  • 当入队一个读请求的结点:如果队尾是读结点,不会直接链接到队尾,而是链接到该读结点的cowait链中,cowait链本质是一个栈;
  • 当入队一个写请求的结点:如果队尾是写结点,则直接链接到队尾;
  • 唤醒线程的规则和AQS类似,都是首先唤醒队首结点。区别是StampedLock中,当唤醒的结点是读结点时,会唤醒该读结点的cowait链中的所有读结点(顺序和入栈顺序相反,也就是先进后出);
  • 另外,StampedLock使用时要特别小心,避免锁重入的操作,在使用乐观读锁时也需要遵循相应的调用模板,防止出现数据不一致的问题;
  • 最后,注意StampedLock悲观读占满CPU的问题!!!
  • 题外话:如果想要进一步了解,可以看第一篇参考博客的例子分析,大神级分析(个人建议看一看)!!!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值