Java & Lock & StampedLock & 总结-邮戳

时间:2024-10-29 14:27:33

 读写锁接口/AQS类/乐观读锁/邮戳

    邮戳锁类致力于提供更加优越的读/写性能。邮戳锁类作为读写锁其存在其实是有些莫名其妙的,因为众所周知在Java锁框架中早已存在读写锁功能非常全面的可重入读写锁类,那么邮戳锁类又是基于什么原因而存在呢?事实上虽然本质相同,但邮戳锁类在设计定位上与可重入读写锁类其实存在较大差异。可重入读写锁类更加注重功能的全面性,即其在提供读写锁基本功能的同时还致力于为现实开发中的公平/重入等需求提供支持;而邮戳锁类则只在读写锁的基础上追求读/写性能的极致提升,并为此专门设计了全新的乐观读锁概念…我们可以发现这两类读写锁在设计上其实存在对立/互补关系,因为世间万物有得有失,全面性的获取往往需要牺牲性能为代价;而想要获得高效性也必然要有所舍弃。因此邮戳锁类的存在为开发者提供了高效选择,令其可以在简单的读写场景中避免使用可重入读写锁类而减少性能损失。

    乐观读锁的本质是无锁。乐观读锁是邮戳锁类额外设计的一种比读锁更加理想化的资源共享方案,目的是在无写操作的场景下彻底避免读锁的加/解锁以进一步提升读操作的并发性能。毕竟在无写操作的情况下,令线程出于阻塞写锁的目的加锁读锁并没有实际意义。乐观读锁的本质是无锁,顾名思义就是令线程在没有任何保护措施的情况下直接访问共享资源。这么做固然可以令读操作的并发性能达到最佳,但同时也将共享资源置于了不安全的境地,毕竟我们无法在此情况下保证获取到的共享资源状态是绝对正确的,即无法保证共享资源在读取期间未被并发写入,因此便有了邮戳这一专门解决该问题的具体手段存在。

    邮戳用于代表锁的个体/顺序。在邮戳锁类的无锁资源共享方案中,线程需要在访问共享资源前先加锁乐观读锁以获取无锁邮戳,该无锁邮戳代表了该锁的个体/顺序。而在完成访问后线程还需要将该无锁邮戳传回邮戳锁中进行校验,其本质是将该无锁邮戳与邮戳锁中记录的最新锁状态(是否锁/锁类型/锁个体/锁顺序/锁总数)进行计算/对比以判断是否有其它线程在此乐观读锁后正持有写锁/持有过写锁。是则说明可能存在并发写操作,此次线程读取到的共享资源快照并不可靠,需要继续通过常规读锁或循环乐观读锁的方式来进行安全读取;否则便意味着此次访问是线程安全的。

    邮戳锁类不是读写锁接口的实现类。由于乐观读锁只有校验没有解锁,以及加锁/校验都有邮戳参与的原因,其锁方法并没有继承Lock @ 锁接口定义的标准模板,而是由邮戳锁类专属定义/实现的。而出于风格统一上的考量,邮戳锁类也同样未继承读写锁接口来实现读/写锁的获取/加锁/解锁,而是采用了与乐观读锁一样的“邮戳”式风格进行了特化定义/实现。即读/写锁的加锁可以获取到读/写锁邮戳,并且其解锁方法也需要相应读/写锁邮戳的参与,由此我们可知邮戳除了可以代表个体/顺序外还可以代表锁的类型。而或许是因为读写锁接口/锁接口定义的标准模板已过于深入人心,因此邮戳锁类很“体贴”的给出了相应的视图方法以供部分“强迫症”开发者按标准模板的方式来获取/加锁/解锁读/写锁…当然,这一切都与乐观读锁没什么关系。

    邮戳锁类不基于AQS类实现。深入学习过Java锁框架各类API的开发者应该知道,其实现类实际上都是基于AQS类实现的,但邮戳锁类是例外。关于AQS类的详细内容此处不会提及:一是AQS类的内容十分庞大/晦涩,总体性的概述并无法令初学者窥其门径,反而会令其感到不知所云,因此其内容会在专属文章中单独讲解;二是对于邮戳锁类这一使用率相对较低的锁框架API而言,会在完全不知道AQS类的情况下学习其底层实现原理的开发者大概率是凤毛麟角/万中无一的,故而本文对AQS类所有知识点的引用/对比都建立在开发者已知的基础上。此处我们只需知道的是:邮戳锁类之所以选择自我特化实现而不选择基于AQS类实际上是出于性能/开销/结构等优化性质而非单纯实现性质上的考量。

    AQS类可以支持邮戳锁类的实现。首先(我个人)确定的一点是:基于AQS类的已有功能而言,其完全可以支持邮戳锁类的设计实现,这一点可以从“锁状态记录”与“线程管理”两个方面进行佐证。关于AQS类的[状态]及其读/写方法能否支持邮戳锁锁状态记录这一点其实无需深究,因为AQS类对其自身[状态]读/写的支持非常全面,充其量就是实现时为了兼容邮戳设计而在调用的时机/次数上有所调整而已。而如果真要谈及有所疑虑的点,那就是AQS类[状态]的int类型限制是否会对锁状态的记录造成影响,因为邮戳锁类的锁状态容器是一个long类型的字段…但这一点实际上也无需在意,因为AQS类还存在除[状态]被变更为long类型以外完全没有其它差异的兄弟类AQLS…因此在“锁状态记录”方面AQS类完全满足邮戳锁类的需要;而在“线程管理”方面,唯一需要在意的是AQS类能否管理等待加锁乐观读锁的线程,毕竟AQS类中只存在对应读/写线程的共享/独占模式。但这一点实际上也同样无需在意…因为邮戳锁类只设计了“特殊值”形式的乐观读锁加锁方法,即乐观读锁的加锁方法本就是不支持阻塞的…由此我们可以断言AQS类可以支持邮戳锁类的实现。
 

 锁状态/唯一性/顺序性/批次/特殊值

    邮戳的本质是[state @ 状态]的快照。[状态]是邮戳锁类内部用于记录锁状态的long类型字段,与AQS类[状态]同名。当线程对邮戳锁执行加/解锁时,其本质便是在计算/写入[状态]。而如果执行的是加锁,则操作完成后便会将新[状态]作为邮戳返回,因此邮戳的本质实际上就是[状态]的快照,这也是邮戳可以代表锁的根本原因。

    邮戳锁类以“位划分”的方案将读/写锁的状态统一保存在[状态]中。“位划分”属于单仓多源数据存储方案中的一种,通过将单仓库划分为多区块的方式来保存多类/个数据。虽然在现实开发中的使用率较低,但该方案在API中其实并不罕见,例如我们熟知的可重入读写锁类就通过“平均位划分”方案在AQS类[状态]的高/低16位比特中各自记录了读/写锁的状态。而与之有所差异的是:邮戳锁类采用了“不均位划分”方案在[状态]的高57位/低7位比特中记录了写/读锁的状态。这种严重的比例失衡设计有其深度的内在考量,目的是尽可能增强[状态]的唯一性/顺序性。我们可以发现“不均位划分”方案并未给乐观读锁分配数据存储空间,这是因为乐观读锁的本质是无锁,因此乐观读锁的加锁并不需要真的向[状态]中写入数据,只需读取[状态]并进行某些位运算后便可直接作为邮戳返回…该知识点会在下文讲解加/解/校验/转换锁时详述。

在这里插入图片描述

    [状态]需要具备唯一性/标记性/顺序性以支持邮戳对锁个体/类型/顺序的代表。[状态]其实具备唯一性/标记性/计数性/顺序性四项特性,而这其中标记性/计数性其实无需多说,因为明确表示出邮戳锁是否锁/锁类型/锁总数是[状态]作为锁状态记录的基本条件,因此这里只针对唯一性/顺序性进行讲解。唯一性/顺序性顾名思义是说[状态]是无重复且具备先后关系的,而又因为每个邮戳都有其各自代表的锁,因此在无锁邮戳与[状态]的校验/对比中,通过唯一性我们可以确定两者各自代表的乐观读锁/写锁…如果此时[状态]代表的是写锁的话…而通过顺序性我们可以知晓乐观读锁与写锁的顺序,从而判断得知是否存在并发写操作的可能。

    [状态]的唯一性/顺序性并不绝对。[状态]是可能重复/乱序的,因此上文才有“增强[状态]唯一性/顺序性”的说法,故而邮戳也是可能重复/乱序的。关于[状态]重复/乱序的原因与周期/记录方式有关,这些知识点会在下文讲解特殊值时详述。此处我们可以/应该意识到的是:既然[状态]是可能重复/乱序的,那么邮戳也就无法准确代表锁的个体/顺序,因为不同锁完全可能拥有相同的邮戳,如此一来无锁邮戳的校验也就不再可靠…这个推测是完全正确的。事实上如果从个体的角度上看,除了写锁[状态]可以在单周期内具备绝对的唯一性/顺序性外,无/读锁[状态]并不具备唯一性,并且顺序性也会受到很大限制。因此邮戳实际代表的是锁批次而非锁个体,只是上文为了简化理解才使用了个体的说法。

    批次具备绝对的唯一性/顺序性。批次是组概念的具象化,其本质是一系列锁的集合。邮戳锁类以锁的独占/共享特性来划分批次,并依赖写锁的加/解锁开启新批次。因此每个写锁都是单独的批次,而两个相邻写锁间的所有乐观读/读锁也会被视作批次,具体如下图所示。邮戳锁类为批次设计/赋予了“单周期内”的绝对唯一性/顺序性,即[状态]中记录的锁批次信息在单周期内绝不会出现重复/乱序。因此[状态]虽然无法准确代表锁个体,但却可以准确代表锁批次。

在这里插入图片描述

    邮戳锁类通过各类特殊值来计算/记录锁状态。邮戳锁类中用于计算/记录锁状态的特殊值很多,其具体名称/数值/作用/意义如下所示:

名称:[RUNIT @ 读单位]
数值:0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 & 0000 0001 – 001
作用/意义:
 
    [读单位]是记录读锁状态的基本单位。当线程尝试加/解读锁时,其会在确定写锁未被持有的情况下对[状态]递增/减[读单位],因此[状态]低7位比特值 > 0 便意味着邮戳锁的读锁已被持有,而具体的值则为读锁持有总数。
    读锁状态记录方式的递增/减双向性是读锁[状态]重复/乱序的直接原因,因为其使得后读锁的[状态]存在 <= 前读锁的可能,因此读锁邮戳便无法代表读锁的个体/顺序…相关情况如下:
    0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 & 0000 0000 – 无锁[状态]
    0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 0000 & 0000 1111 – 读锁[状态] – 加锁15次
    0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 0000 & 0000 1100 – 读锁[状态] – 继续解锁3次
    0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 0000 & 0000 1101 – 读锁[状态] – 继续加锁1次 < 加锁15次
    0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 0000 & 0000 1111 – 读锁[状态] – 继续加锁2次 = 加锁15次

名称:[RFULL @ 读满溢]
数值:0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 & 0111 1110 – 126
作用/意义:
 
    [读满溢]是[状态]所允许记录的最大读锁持有总数。由于邮戳锁类将[状态]的绝大部分比特位都用于记录写锁状态以增强唯一性/顺序性,因此[状态]本身能够记录的读锁持有总数其实极其有限。对于这种情况,邮戳锁类会在读锁持有总数超过[读满溢]时将超出部分额外保存在long类型字段[readerOverflow @ 读线程溢出]中。因此在不考虑乐观读线程的情况下,邮戳锁类支持的最大读线程并发量为Long.MAX_VLAUE + 126。

名称:[RBITS @ 读比特]
数值:0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 & 0111 1111 – 127
作用/意义:
 
    [读比特]是线程在读锁持有总数 >= [读满溢]的情况下递增/减读锁持有总数的标记值。邮戳锁类会通过CAS来保证[状态]的正确性,但是对于[读线程溢出]却不能“直接”这么做,因为两者作为读锁状态的共同仓库如果使用各自独立的CAS将无法保证读锁状态存储的合法性。典型的场景是:读线程A在[状态]为[读满溢]的情况下加锁/CAS递增[读线程溢出],而读线程B则在同样情况下解锁/CAS递减[状态],从而导致读锁持有总数未超过[读满溢]但[读线程溢出]却存值的情况发生。为此邮戳锁类选择将[读比特]作为线程在满溢情况下递增/减读锁持有总数的标记值,从而得以通过统一CAS来达到保证整个读锁状态存储合法性的效果。
    [读比特]的使用方式是:当读线程发现[状态]为[读满溢]时,其必须在成功将之CAS赋值为[读比特]后才能去递增/减[状态]/[读线程溢出]。而如果CAS失败/[状态]已为[读比特]则线程就要短暂放弃CPU资源以期望其它线程操作完成,并在重新获得CPU资源后再次操作。

名称:[WBIT @ 写比特]
数值:0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 & 1000 0000 – 128
作用/意义:
 
    [写比特]是记录写锁状态的基本单位。当线程尝试加/解写锁时,其会在确定读锁未被持有的情况下对[状态]递增[写比特]。而由于[写比特]的第8位比特为1且初始无锁[状态]的第8位比特为0,由此我们可知当[状态]因为递增[写比特]而第8位比特为1时便意味写锁已被持有;而为0时便意味已被释放,可以支持加锁读/写锁…具体示例如下所示:
    0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 1111 & 1000 0000 – 写锁[状态]
    0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 0000 & 0000 0000 – 无锁[状态]
    0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 0000 & 0000 1111 – 读锁[状态]
    0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 0000 & 1000 0000 – 写锁[状态]
    这种仅依靠递增来同时实现加/解写锁的做法是邮戳锁类保证写锁[状态]唯一性/顺序性的具体手段,因为其能够确保后写锁[状态]必然会因为大于前写锁而唯一/顺序。因此除非[状态]的高57位比特无法继续承载[写比特]的递增而还原开启新的周期,否则写锁[状态]将永远不可能重复/乱序。那上文为什么说邮戳锁类的“不均位划分”方案可以提升写锁[状态]的唯一性/顺序性呢?这是因为多比特的记录空间使得单周期的写锁记录量/新周期的开启还原率被大幅升高/降低,从而有效减少了不同周期间相同写锁[状态]的碰撞率。
    由于写锁[状态]记录的特殊性,判断邮戳锁的写锁是否被持有无法像判断读锁持有一样通过简单查看[状态]是否 >= 128 实现,因为这其中还掺杂着无锁/读锁的情况。邮戳锁的写锁持有判断实际上依然需要借助[写比特]实现,这也是[写比特]的另一个重要作用。判断写锁持有的关键在于查看[状态]的第8位比特是否为1,邮戳锁类通过将[状态]与[写比特]进行位与操作来实现这一点。由于位与结果除第8位比特外都只有0这一种可能,因此如果位与结果不为0/等于[写比特]即意味着邮戳锁的写锁已被持有…具体示例如下所示:
    0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 1111 & 1000 0000 – 写锁[状态]
    0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 & 1000 0000 – [写比特]
    0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 & 1000 0000 – 位与结果/[写比特]

    批次信息保存于[状态]的高57位比特。我们可以从批次的划分/开启方式中推测得知的是:乐观读/读锁的加/解锁不会导致[状态]中记录的批次信息发生变化,但写锁的加/解锁会。而通过对[读单位]的分析我们还可以发现:读锁的加/解锁对[状态]的作用/影响范围其实仅限于前7位比特,即只有低7位比特会发生变化,而高57位是固定不变的,由此我们可知锁的批次信息必然保存在[状态]的高57位比特中。那么这高57位比特又有多少被用来保存批次信息呢?通过对[写比特]的分析我们可知:必然会因为[写比特]的递增而变化的只有[状态]的第8位比特/高57位比特的最后一位,因此也只有将整个高57位比特都用于保存批次信息才能保证新批次会随着写锁的加/解锁而开启。而由于高57位比特恰恰也是写锁状态的存储仓库,由此可知写锁邮戳可同时代表锁的个体/批次/顺序。

    读锁[状态]只会在同批次中重复/乱序。由于读锁的加/解锁只会令[状态]的前7位比特发生变化,并且每个批次在[状态]高57位的值都唯一/顺序,因此我们可知读锁[状态]只可能在同批次中发生重复/乱序,因为上个批次的[状态]永远不可能超出批次的限制而 >= 下个批次的任意[状态]。由此我们也可知:虽然我们无法通过邮戳明确多个同批次读锁的个体/顺序,但却可以得知不同批次间锁的顺序。

名称:[ABITS @ 全比特]
数值:0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 & 1111 1111 – 255
作用/意义:
 
    [全比特]是判断读/写锁持有状态的辅助值。我们已知的是:邮戳锁类会借助[写比特]来判断写锁的持有状态,但该辅助效果也只仅限于写锁而已,而[全比特]则具备同时判断读/写锁持有状态的辅助能力。
    我们可以发现的是:虽然[状态]的64位比特都被用于记录读/写锁状态,但关于读/写锁持有的数据记录实际上都集中在低8位比特中。因此只要提取/分析低8位比特,我们就能够知道邮戳锁的读/写锁持有状态,而[全比特]便起到了提取的作用。[全比特]的使用方式与[写比特]是相同的,即通过与[状态]进行位与操作来提取数据,随后再对位与结果进行分析来确定持有状态…具体示例如下所示:
 
    [状态] & [全比特] = [写比特] – 写锁
    [状态] & [全比特] > 0 且 < [写比特] – 读锁
    [状态] & [全比特] = 0 – 无锁

名称:[ORIGIN @ 原始]
数值:0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 & 0000 0000 – 256
作用/意义:
 
    [原始]是状态的初始值。当邮戳锁被创建/[状态]开启新周期时,[状态]会被赋值/还原为[原始]。[原始]在初见时确实会令人会感到疑惑,因为相比而言似乎0更加适合作为[状态]的初始值。实际上这是因为0已被邮戳锁类作为各类加锁操作失败时返回的标记邮戳,因此为了避免歧义[状态]便不允许再被赋予0值。典型的场景是:如果[状态]的初始值为0,则当线程在[状态]为0的情况下加锁乐观读锁获得无锁邮戳0时,其将同时带有成功/失败的双重对立含义。

名称:[SBITS @ 非比特]
数值:1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 & 1000 0000
作用/意义:
 
    [非比特]是用于计算/对比[状态]/邮戳批次的辅助值。我们已知[状态]/邮戳的高57位比特中存有批次信息,因此通过与[非比特]进行位与操作就可以将低7位比特中的值略去以将之轻易提取出来。
 

 加锁/解锁/校验

    邮戳锁类不支持重入。邮戳锁类不支持重入的直接原因是写锁状态采用了单向性记录方式,如此[状态]便无法再支持写锁持有总数的记录,因为[写比特]的递增代表的只是持有状态的变化。而[状态]/[读线程溢出]虽然可以记录读锁持有总数,但该读锁持有总数隶属于所有读线程,读线程各自的私有读锁持有总数依然无法被记录,因此邮戳锁类是不支持重入的。

    无锁邮戳在同批次内相同/唯一。乐观读锁的本质是无锁,其加锁的实际行为是在确定写锁未被持有的前提下将[状态]与[非比特]进行位与操作以获取高57位比特的批次信息。由于该操作并不会开启新的批次/对[状态]进行修改,因此同批次内无论乐观读锁加锁多少次得到的无锁邮戳都是相同的,故而这种不变性也正是无锁邮戳重复/乱序的直接原因。

0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 1111 & 0000 0111 – 读锁[状态]1
0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 1111 & 0000 1111 – 读锁[状态]2
1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 & 1000 0000 – [非比特]
0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 1111 & 0000 0000 – 批次信息相同

    无锁邮戳校验的本质是批次校验。所谓批次校验是指提取[状态]/邮戳中的批次信息并判断两者是否相同的行为。由于写锁的加/解锁都会开启新批次,因此线程便可以借此校验判断写锁在其加锁乐观读锁后是否正在被持有/已被持有过,从而判断是否有并发写操作存在的可能。批次校验在邮戳锁类的各项操作中使用非常频繁,包括但不限于乐观读/读/写锁的校验/加锁/解锁/转换等,因为其是邮戳正确性的首层保证。以解锁举例:当线程试图以指定邮戳解锁时,如果在首层的批次校验中就被驳回,那就说明该邮戳所代表的锁必然已经解锁/失效。这是因为新批次由写锁的加/解锁负责开启,而写锁又是独占特性的锁,因此新批次的开启也意味着旧批次锁的彻底解锁,故而邮戳锁不存在多批次锁并存的情况。这种规则下一旦发现邮戳与[状态]不属于相同批次就说明该邮戳必然是非法的,那么后续校验自然也就无需再进行了。

    邮戳锁类会借助类型校验再次确定读锁邮戳的正确性。我们已知批次校验是邮戳正确性的首层保证,而如果线程执行的是读锁的解/转换锁操作,则在此之后邮戳锁还会继续校验类型以再次确保正确。类型校验的本质是判断邮戳/[状态]是否都代表了读锁。这是非常合理的行为,毕竟我们显然不能使用无锁邮戳来解锁读锁。那为什么写锁的解/转换锁操作不需要执行该校验呢?这是因为每个写锁都是单独的批次,因此写锁邮戳如果与[状态]在类型/个体/批次任意一层面存在差异那都会在首层的批次判断中被过滤掉。

    邮戳锁类不保证读锁邮戳的绝对正确。对于读锁邮戳而言,虽然已经在解锁时经历了额外的类型校验,但实际上邮戳锁类依然无法完全确保其正确性,因为其无法得知该读锁邮戳是否已被用于解锁过。与能够同时代表批次/个体的写锁邮戳不同,读锁邮戳只能代表批次而无法代表个体,故而即使某读锁邮戳已被用于解锁也无法被具体探知,因此读锁邮戳实际上可解锁同批次的所有读锁…虽然这并不是合理/合法的做法。

    邮戳锁类不保证邮戳与线程的关联正确性。我们已知邮戳锁的解锁/校验/转换需要依赖邮戳实现,那如果将某线程获取到邮戳交由其它线程解锁/校验/转换又会是怎样的结果呢?实际上除不保证读锁邮戳的正确性外,邮戳锁类也不确保邮戳与线程的关联正确性。因此如果邮戳被有意/无意的暴露给其它线程,则其它线程同样可以凭借该邮戳完成相应的解锁/校验/转换操作…显然这看起来并不合理。因此我们会产生疑惑:为什么邮戳锁类不将邮戳与线程直接绑定呢?即为什么不将邮戳通过ThreadLoacl @ 线程本地类直接保存在线程中呢?毕竟这样就可以避免邮戳混乱的问题,并且锁的各类方法也可以因为邮戳不再对外暴露而采用锁接口/读写锁接口定义的标准模板…实际上这是邮戳锁类为了追求极致性能而做出的取舍。线程本地类并不是特别高效的工具,并且其引入还会导致内存泄露/实现复杂/开销增大等问题。这对于追求极致性能的API而言显然是无法接受的,并且邮戳混乱的问题也可通过线程局部变量的方式简单/高效的处理,因此邮戳锁类才会放弃实现这一点,也因此开发者有义务在使用邮戳锁类时正确维护邮戳与线程的关联正确性。

    邮戳锁类的一切“不支持/不保证”都是基于性能方面的考量。实际上不仅是邮戳与线程的关联正确性,包括无/读锁邮戳无法代表个体/不保证读锁邮戳的绝对正确/不支持重入等在内的一切“不支持/不保证”其实都是邮戳锁类追求极致性能锁所做出的取舍。想要提供上述功能实际上并不困难,只需将读锁状态的记录方式同样设计为单向性,再引入线程本地类来记录持有总数即可…但我们必须明白的是:邮戳锁类的设计初衷就是为了提供更加优越的读/写性能,上述机制的引入必然会成为性能的拖累。因此虽说导致上述“不支持/不保证”的直接原因各异,但其根本原因都是邮戳锁类为了追求极致性能所做出的让步。
 

 锁转换/锁升级/锁降级

    转换的本质是先解锁再加锁。所谓转换是指先解锁当前锁再加锁指定锁的过程,该操作可在一定程度上简化各类锁综合使用而导致的频繁加/解操作。但需要注意的是这种连续的加/解锁并不会必然发生,而是只会在指定邮戳能够正确代表当前锁且锁类型与指定锁不同的情况下执行。如果指定邮戳无法正确代表当前锁则方法会返回邮戳0以表示转换失败;而如果能够正确代表但当前锁类型与指定锁相同则方法也只会直接返回指定邮戳而不执行加/解锁操作。此外还有一点相对特殊的是:由于读锁具备共享特性,因此如果试图将读锁转换为写锁则必须保证读锁持有总数为1,因为只有这样才能保证读锁能够彻底解锁来加锁写锁;否则会直接返回邮戳0表示失败。

    写锁转换/读锁转换可以被视作锁升级/锁降级。与可重入读写锁类不同,邮戳锁类支持锁升级,而这一点的直接原因与邮戳锁类不保证邮戳与线程之间的关联关系有关。由于线程不是方法的逻辑依据,因此锁升级这种理论上不允许或只允许唯一读线程执行的行为在邮戳锁类中是可行的…只要开发者自身保证使用无异常即可。