2023年3月

前言

此篇文章主要是对整体项目做个简单介绍,对于各个部分下边会有对应链接跳转访问

第一行代码都快忘了是什么时候开始的了,期间还拿它参加了一次开发者比赛,项目初衷就是想让更多的人能够快速的做一款App出来,经过两年的断断续续开发,项目的功能算是更加完善了

这算是第一次我正式的对他进行正式的宣传(最新动态可关注公众号【
穿裤衩闯天下
】获取)

项目整体总共有四部分:后端、管理端、客户端、门户网站(简单)

后端项目

这部分主要是为客户端和管理端提供接口,承载了连接数据库,以及数据处理,包含比较一套比较完整的社交项目所需的接口:
登录注册、账户注销、签到、匹配、好友关系、内容发布、点赞评论、个人信息、附件上传、验证码处理(目前只有邮箱)、举报反馈、充值消费、礼物系统、金币系统、三方支付等
具体可看后端项目详细介绍
忘忧大陆-功能完备的社交项目之服务器端介绍

管理端项目

这部分主要是管理员使用的平台,和大多数平台一样,可以查看整体数据,以及现在加上的一个简单的数据统计,后续可以根据需要完善,可以在这里对后端所提供的所有数据进行增删改查
具体可看管理端项目详细介绍
忘忧大陆-功能完备的社交项目之管理端介绍

门户站点项目

这部分主要是主要作用就是展示项目首页,作为一个宣传门户显示信息等,其实没啥好说的,就是一个脸面,可以自由发挥
具体可看门户站点项目详细介绍
忘忧大陆-功能完备的社交项目之门户站点介绍

客户端项目

这部分主要是面向用户使用的 App,目前只有 Android 端,主要功能也都在这部分,
具体可看客户端项目详细介绍
忘忧大陆-功能完备的社交项目之Android端介绍

更多

总体介绍
忘忧大陆项目整体介绍

项目客户端下载体验

开源仓库地址

问卷反馈

都看到这里了辛苦做个小问卷吧,你的反馈有助于促进项目的完善
忘忧项目使用问卷

快就一个字,甚至比以快著称于世的Sublime 4编辑器都快,这就是Zed.dev编辑器。其底层由 Rust 编写,比基于Electron技术微软开源的编辑器VSCode快一倍有余,性能上无出其右,同时支持多人编辑代码。

安装和配置Zed.dev

Zed.dev编辑器还在灰度测试阶段,暂时只释出了Mac版本,在Zed.dev官网下载,安装成功后,进入Zed.dev编辑器,使用组合键 Command + , 调出编辑器的配置文件:

// Zed settings  
//  
// For information on how to configure Zed, see the Zed  
// documentation: https://zed.dev/docs/configuring-zed  
//  
// To see all of Zed's default settings without changing your  
// custom settings, run the `open default settings` command  
// from the command palette or from `Zed` application menu.  
{  
    "theme": "One Dark",  
}

编辑器默认使用暗黑风格的One Dark主题,也可以通过配置theme来选择别的主题,比如"Rosé Pine Moon:

"theme": "Rosé Pine Moon",

如图所示:

除此之外,我们也可以配置其他的设置,以方便日常的开发:

// Zed settings  
//  
// For information on how to configure Zed, see the Zed  
// documentation: https://zed.dev/docs/configuring-zed  
//  
// To see all of Zed's default settings without changing your  
// custom settings, run the `open default settings` command  
// from the command palette or from `Zed` application menu.  
{  
    "theme": "Rosé Pine Moon",  
    "soft_wrap": "editor_width",  
    "autosave": "on_focus_change",  
    "tab_size": 4,  
    "buffer_font_size": 15,  
    "language_overrides": {  
      "Python": {  
        "format_on_save": {  
          "external": {  
            "command": "black",  
            "arguments": ["-"]  
          }  
        }  
      }  
    }  
}

这里配置了自动保存,缩进空格,自适应断行等等。

Zed.dev默认支持的语言列表:

C  
C++  
CSS  
HTML  
Elixir  
Go  
JavaScript  
JSON  
Markdown  
Python  
Ruby  
Rust  
TOML  
TSX  
TypeScript

也就是说默认支持上述语言的自动补全,而不需要单独配置:

虽然Zed.dev还不支持插件,但内部集成了系统的终端,直接通过组合键 esc + shift 打开终端即可运行代码:

非常方便,也可以通过组合键 Command + b 来自由收放左侧文件列表菜单栏。

大体上,基本不需要配置什么,就可以直接用Zed.dev来写代码了,即所谓开箱可用。

项目共享协作

我们可以从协作菜单中添加一个现有的Zed.dev用户作为联系人,从窗口右上角的加号图标进行部署,或者通过组合键command-shift-c,然后单击搜索框右侧的添加图标按钮:

随后可以在协作菜单中看到所有在线或者离线联系人。搜索或点击他们将发送一个请求,开始呼叫并与他们共享当前的项目,他们将收到加入呼叫的通知。

这之后所有连入Zed.dev项目的人就可以进行代码联调了,效率上要比Git高出了不少。

结语

快速轻便,简单清爽,这就是Zed.dev给我们的第一印象,很明显,在桌面编辑器层面,Rust具有及其出挑的优势,它以闪电般的速度处理功能复杂的任务,同时还减少了与内存、边界、空变量、初始化变量或整数溢出相关的错误,下面是Zed.dev的内存占用情况:

最后附上邀请码,与君共觞:zed.dev/invites/T7MtltpVii8thwIW

当大家了解了如何编写一个简单的Spring MVC程序后,大家心中应该会有一些好奇:这背后到底发生了什么?

Spring MVC是怎么把这些功能串联起来的?我们只是写了一个控制器而已,HTTP请求是怎么转换为控制器方法的调用的?结果又是怎么变成JSON的.....啊这小伙伴们是不是已经混乱了!?

接下来让我们看看这背后究竟发生了什么。

请求的处理流程

现代Java Web项目在处理HTTP请求时基本都遵循一样的规范,即Java Servlet规范(JSR 340)。其处理流程都是Servlet容器(例如Tomcat或Jetty)收到一个请求,接着找到合适的Servlet进行处理,随后返回响应。

在SpringMVC中,这个处理请求的Servlet就是前面提到过的
DispatcherServlet

根据配置,Servlet容器会将指定的请求都交由它来处理,在收到请求后,DispatcherServlet会在Spring容器中找到合适的处理器(大部分情况下是控制器,即带有@Controller注解的类)来处理请求,处理结果经过视图模版后得到呈现(render)的响应内容,最后再返回给用户,具体流程如下图所示:

image

  • DispatcherServlet 接收到客户端发送的请求。
  • DispatcherServlet 收到请求,调用HandlerMapping 处理器映射器
  • HandleMapping 根据请求URL 找到对应的handler 以及处理器 拦截器,返回给DispatcherServlet
  • DispatcherServlet 根据handler 调用HanderAdapter 处理器适配器。
  • HandlerAdapter 根据handler 执行处理器,也就是我们controller层写的业务逻辑,并返回一个ModeAndView
  • HandlerAdapter 返回ModeAndView 给DispatcherServlet
  • DispatcherServlet 调用 ViewResolver 视图解析器来 来解析ModeAndView
  • ViewResolve 解析ModeAndView 并返回真正的view 给DispatcherServlet
  • DispatcherServlet 将得到的视图进行渲染,填充到request域中
  • 返回给客户端响应结果。

1.DispatcherServlet的初始化

Servlet继承图

image

既然DispatcherServlet是一个Servlet的实现,那就会遵循其生命过程,例如会在创建后进行初始化。

image

HttpServletBean.init()方法调用了子类的方法FrameworkServlet.initServletBean(),其中做了Web应用上下文的初始化,用的就是initWebApplicationContext()。

image

在初始化Web应用上下文或者是上下文更新时,都会调用DispatcherServlet.onRefresh(),而这个方法就一句话,直接调用initStrategies(),就是在初始化Spring MVC的很多特殊类型的Bean

image

Spring MVC中的特殊Bean类型

Bean类型 说明
MultipartResolver 用来解析Multipart请求的解析器,通常是上传文件的请求,MultipartResolver这层抽象的背后会有多种实现,例如基于Commons FileUpload
LocaleResolver 和语言环境有关的解析器,通常用于国际化相关的场景中,包含时区、语言等多种信息
ThemeResolver 主题(Theme)解析器,选择应用程序的外观界面,主题通常是一组静态资源
HandlerMapping 用于将请求映射到处理器上,过程中还包括各种前置与后置处理,两个主要的实现类是RequestMappingHandlerMapping和SimpleUrlHandlerMapping
Handler Adapter 用于触发执行处理器,通过这层抽象,DispatcherServler可以不用关心具体如何执行调用
HandlerExceptionResolver 异常处理解析器
ViewResolver 用于将字符串形式的视图名称转化为具体的View,RequestToViewNameTranslator会根据请求信息转换对应的视图名称
FlashMapManager 用于存取在请求暂存的输入与输出信息,通常会用在重定向时

2.请求的处理过程

DispatcherServlet在收到请求后,会交由doService()方法来进行处理,其中包含了两个主要的步骤:

  • 第一步,向请求中设置Spring MVC相关的一些属性
  • 第二步,调用doDispatch(request, response);将请求分派给具体的处理器执行实际的处理。

在这里说一下,DispatcherServlet用到的设计模式是
委派模式

委派模式:干活算你的(普通员工),功劳算我的(一些项目经理,他们不干活!)
例如:老板(Boss)给项目经理(Leader)下达任务,项目经理会根据实际情况给每个员工
派发任务
,待员工把任务完成后,再由项目经理向老板汇报结果

下表是doService()方法中设置到HttpServletRequest里的几个属性:

属性名 说明
WEB_APPLICATION_CONTEXT_ATTRIBUTE WebApplicationContext,Web的应用上下文
LOCALE_RESOLVER_ATTRIBUTE 处理请求时可能会需要用到的LocaleResolver,如果没有国际化需求,可以忽略它
THEME_RESOLVER_ATTRIBUTE 用来决定使用哪个显示主题的ThemeResolver,如果没有这个需求,也可以忽略它
THEME_SOURCE_ATTRIBUTE 用来获取主题的ThemeSource,默认是当前的WebApplicationContext
INPUT_FLASH_MAP_ATTRIBUTE 上个请求传递过来暂存到FlashMapManager里的FlashMap
OUTPUT_FLASH_MAP_ATTRIBUTE 用来向后传递的FlashMap中的暂存信息
FLASH_MAP_MANAGER_ATTRIBUTE 如果当前存在FlashMapManager,则将它设置到请求里

doDispatch()方法的大致处理逻辑如下图,DispatcherServlet会尝试根据请求来找到合适的处理器,再通过HandlerAdapter来执行处理器的逻辑,经过前置处理、处理器处理和后置处理等多个步骤,最终返回一ModelAndView。

doDispatch()方法处理逻辑

Request MappingHandlerAdapter是专门用来调用@RequestMapping注解标记的处理器的。在处理结果的那一步,如果有异常就处理异常,例如交给专门的HandlerExceptionResolver来处理;如果没有异常就看ModelAndView,不为空则呈现具体的视图,不存在也不用担心,因为请求可能已经处理完成了。

在调用处理器逻辑处理的过程中,针对方法的返回值,会调用HandlerMethodReturnValueHandler进行处理——根据不同的情况,会有不同实现来做处理。

例如,加了@ResponseBody的方法,返回值就直接被RequestResponseBodyMethodProcessor处理掉了,选择合适的HttpMessageConverter将对象直接序列化为相映的内容;而返回字符串作为视图名的情况,则是由ViewNameMethodReturnValueHandler来处理的。

大家好,我是王有志。关注
王有志
,一起聊技术,聊游戏,聊在外漂泊的生活。
鸽了这么久怪不好意思的,因此送一本《多处理器编程的艺术》,快点击
此处
参加吧。另外欢迎大家加入“
共同富裕的Java人
”互助群。

今天的主题是
AbstractQueuedSynchronizer
,即AQS

作为
java.util.concurrent
的基础,AQS在工作中的重要性是毋庸置疑的。通常在面试中也会有两道“必考”题等着你

  • 原理相关:
    AQS是什么?它是怎样实现的?

  • 设计相关:
    如何使用AQS实现Mutex?

原理相关的问题几乎会出现在每场Java面试中
,是面试中的“明枪”,是必须要准备的内容;而设计相关的问题更多的是对技术深度的考察,算是“暗箭”,要尤为谨慎的去应对。

我和很多小伙伴交流关于AQS的问题时发现,大部分都只是为了应付面试而“背”了AQS的实现过程。为了全面地理解AQS的设计,今天我们会从1990年T.E.Anderson引入排队的思想萌芽开始,到Mellor-Crummey和Scott提出的MCS锁,以及Craig,Landin和Hagersten设计的CLH锁。

AQS的内容整体规划了4个部分:

今天我们一起学习前两个部分,了解AQS的前世。

Tips
:本文基于Java 11完成,与Java 8存在部分差异,请注意区分源码之间的差异。

AQS是什么?

通常我们按照类名将
AbstractQueuedSynchronizer
翻译为
抽象队列同步器
。单从类名来看,我们就已经可以得到3个重要信息:

  • Abstract:
    抽象类
    ,通常无法直接使用;

  • Queued:
    队列
    ,借助队列实现功能;

  • Synchronizer:
    同步器
    ,用于控制并发。

源码中的注释也对AQS做了全面的概括:

Provides a framework for implementing blocking locks and related synchronizers (semaphores, events, etc) that rely on first-in-first-out (FIFO) wait queues.

提供了依赖于FIFO等待队列用于实现阻塞锁和同步器(信号量,事件等)的框架。
这段描述恰好印证了我们通过类名得到的信息,我们来看Java中有哪些AQS的实现:

可以看到,JUC中有大量的同步工具内部都是通过继承AQS来实现的,而这也正是Doug Lea对AQS的期望:
成为大部分同步工具的基础组件。

Tips
:至少在Java 8中,
FutureTask
已经不再依赖AQS实现了(未考证具体版本)。

接着我们来看注释中提到的“rely on first-in-first-out (FIFO) wait queues”,这句话指出AQS依赖了FIFO的等待队列。那么这个队列是什么?我们可以在注释中找到答案:

The wait queue is a variant of a "CLH" (Craig, Landin, and Hagersten) lock queue. CLH locks are normally used for spinlocks.

AQS中使用的等待队列时CLH队列的变种
。那么CLH队列是什么呢?AQS做了哪些改变呢?

AQS的前世

AQS明确揭示了它使用CLH队列的变种,因此我从CLH队列的相关论文入手:

  • Craig于1993年发表的《Building FIFO and priority-queueing spin locks from atomic swap》

  • Landin和Hagersten于1994年发表的《Efficient Software Synchronization on Large Cache Coherent Multiprocessors》

这两篇文章都引用了T.E.Anderson于1990年发表的的《The Performance of Spin Lock Alternatives for Shared-Memory Multiprocessors》,因此我们以这篇文章中提出的
基于数组的自旋锁设计
作为切入点。

Tips

基于
数组
的自旋锁

1990年T.E.Anderson发表了《The Performance of Spin Lock Alternatives for Shared-Memory Multiprocessors》,文章讨论了基于CPU原子指令自旋锁的性能瓶颈,并提出了基于数组的自旋锁设计。

基于原子指令的自旋锁

第一种设计(SPIN ON TEST-AND-SET),即
TASLock
,使用CPU提供的原子指令test-and-set尝试更新锁标识:

初始化锁标识为CLEAR,获取锁时尝试更新锁标识为BUSY,更新成功则获取到锁,释放时将锁标识更新为CLEAR。

设计非常简单,竞争并不激烈的场景下性能也是完全没问题,但是一旦CPU的核心数增多,问题就出现了:

  • 持有者在释放锁时要和其它正在自旋的竞争者争夺锁标识内存的
    独占
    访问权限,因为test-and-set是原子写操作;

  • 在使用总线的体系结构中,无论test-and-set指令是否成功,它都会消耗一次总线事务,会使总线变得拥堵。

因此提出了第二种设计(SPIN ON READ),即
TTASLock
,加入test指令,避免频繁的:

该设计中,在执行test-and-set指令前,先进行锁标识状态的判断,处于BUSY状态,直接进入自旋逻辑(或运算的短路特性),跳过test-and-set指令的执行。

额外一次读取操作,避免了频繁的test-and-set指令造成的内存争抢,也减少了总线事务
,竞争者只需要自旋在自己的缓存上即可,只有锁标识发生改变时,才会执行test-and-set指令。

这种设计依旧有些性能问题无法解决:

  • 如果频繁锁标识频繁的发生改变,CPU的缓存会频繁的失效,重新读取;

  • 持有者释放锁时,会导致所有CPU的缓存失效,必须重新在内存或总线中竞争。

T.E.Anderson对两种设计进行了测试,计算了在不同数量的CPU上执行了100万次操作的耗时,执行等待锁,执行临界区,释放锁和延迟一段时间。

可以看到SPIN ON READ的设计随着CPU数量的增多性能确实得到了改善,但距离理想的性能曲线仍有着不小的差距。

除了这两种设计外,T.E.Anderson还考虑了在自旋逻辑中引入延迟来减少冲突:

此时需要考虑设置合理的延迟时间,选择合适的退避(backoff)算法来减少竞争。

Tips

Java版TA
S
Lock

T
T
A
S
Lock
,供大家参考。

基于
数组
的自旋锁

前面的设计中,自旋锁的性能问题是由多个CPU同时争抢内存访问权限产生的,那么让它们按顺序排队是不是就解决了这个问题?T.E.Anderson引入了队列的设计:

初始化

  • 创建长度为CPU数量P的数组flags[P]

  • flags[0]标识为HAS_LOCK(拥有锁),其余标记为MUST_WAIT(等待锁)

  • 初始化queueLast为0,
    标识当前队列位置

加锁

  • CPU通过ReadAndIncrement指令读取queueLast后保存为自己的myPlace


    • ReadAndIncrement指令,先读取,后自增
  • CPU判断自己的flags[myPlace mod P]上的标记来决定持有锁或进入自旋


    • 取模操作让数组变成了头尾相连的“环状”数组

解锁

  • 将当前CPU在队列中的位置flags[myPlace]更新为MUST_WAIT

  • 将flags[(myPlace + 1) mod P]更新为HAS_LOCK,标识下一个CPU获取锁

每个CPU只访问自己的锁标识(myPlace),避免了争抢内存访问的权限,另外锁会直接释放给队列中的下一个CPU,避免了通过竞争获取,减少了从释放锁到获取锁的时间。

当然缺点也很明显,仅从伪代码的行数上也能看出来,
基于队列的自旋锁设计更复杂,当竞争并不激烈时,它的性能会更差
。T.E.Anderson也给出了他的测试结果:

很明显,在竞争激烈的场景中,引入队列后的自旋锁性能更加优秀,并没有过多的额外开销。

Tips

  • T.E.Anderson的论文就介绍到这里,除了对自旋锁的讨论,文章中还讨论了在自旋锁引入退避算法和静态延迟(static delays)的优劣,就留给大家自行阅读了;

  • Java版TEALock
    ,供大家参考(名字是我自己起的~)。

MCS锁的设计

基于数组的自旋锁是排队思想的实现
,T.E.Anderson的论文发表后,又涌现出了许多使用排队思想锁,例如:Mellor-Crummey和Scott于1991年在论文《Algorithms for Scalable Synchronization on Shared-Memory Multiprocessors》中提出的MCS锁,也是基于排队思想实现,只不过在数据结构上选择了
单向链表

描述MCS锁的初始化与加解锁的原理,我使用经过“本地化”的Java实现版本的MCS锁:

MCS锁的
初始化

public class MCSLock {

  AtomicReference<QNode> lock;
  
  ThreadLocal<QNode> myNode;
  
  public MCSLock() {
    this.lock = new AtomicReference<>(null);
    this.myNode = ThreadLocal.withInitial(QNode::new);
  }
  
  private static class QNode {
    private boolean locked;
    private QNode next;
  }
}
  • 声明单向链表的节点QNode,locked表示
    锁是否被前驱节点获取

  • 创建QNode节点lock,表示当前锁的位置,实际上也是链表的尾节点。

MCS锁的
加锁

public void lock() {
  QNode I = this.myNode.get();
  QNode predecessor = this.lock.getAndSet(I);
  if (predecessor != null) {
    I.locked = true;
    predecessor.next = I;
    while (I.locked) {
      System.out.println("自旋,可以加入退避算法");
    }
  }
}
  • 为每个线程初始化QNode,命名为
    I

  • 通过
    原子指令
    获取
    I
    的前驱节点lock命名为predecessor,并将
    I
    设置为lock(取出当前lock,并设置新的lock);



    • predecessor == null
      时,表示队列为空,可以直接返回,代表获取到锁;


    • predecessor != null
      时,表示前驱节点已经获取到锁;


      • 更新locked,表示锁已经被前驱节点获取;

      • 更新predecessor的后继节点为
        I
        ,否则predecessor无法唤醒
        I

      • I
        进入自旋逻辑。

MCS锁的
解锁

public void unlock() {
  QNode I = this.I.get();
  if (I.next == null) {
    if (lock.compareAndSet(I, null)) {
      return;
    }
    
    while (I.next == null) {
      System.out.println("自旋");
    }
  }
  
  I.next.locked = false;
  I.next = null;
}
  • 获取当前线程的QNode命名为
    I

  • 如果
    I.next == null
    ,队列中无其它节点,即不存在锁竞争的场景;


    • 尝试通过CAS更新lock为null,保证下次加锁时
      predecessor == null
      ,成功则直接返回;

    • 如果失败,表示此时有线程开始竞争锁,此时进入自旋,保证竞争者成功执行
      predecessor.next = I

  • 如果
    I.next != null
    ,队列中有其他节点,锁存在竞争;


    • 更新后继节点的locked标识,使其跳出自旋;

    • 更新自己的后继节点指针,断开联系。

MCS锁的逻辑并不复杂,不过有些细节设计的非常巧妙,提个问题供大家思考下:加锁过程中
I.locked = true

predecessor.next = I
的顺序可以调整吗?

MCS锁的整体设计思路到这里就结束了,Mellor-Crummey和Scott给出了MCS锁的4个优点:

  • FIFO保证了公平性,避免了锁饥饿;

  • 自旋标识是线程自身的变量,避免了共享内存的访问冲突;

  • 每个锁的创建只需要极短的时间(requires a small constant amount of space per lock);

  • 无论是否采用一致性缓存架构, 每次获取锁只需要$ O(1)$ 级别的通信开销。

除此之外,相较于T.E.Anderson的设计,
MCS锁在内存空间上是按需分配,并不需要初始化固定长度数组,避免了内存浪费

Tips

CLH锁的设计

1993年Craig发表了《Building FIFO and priority-queueing spin locks from atomic swap》,文章中描述了另一种基于排队思想的队列锁,即CLH 锁(我觉得称为Craig Lock更合适)的雏形,它和MCS锁很相似,但有一些差异:

  • CLH旋转在队列前驱节点的锁标识上;

  • CLH锁使用了一种“隐式”的链表结果。

我们带着这两个差异来看CLH的锁的设计,原文使用Pascal风格的伪代码,这里我们使用《多处理器编程的艺术》中提供的Java版本,与论文中的差异较大,重点理解实现思路即可。

CLH锁的
初始化

public class CLHLock {
  
  AtomicReference<Node> tail;
  
  ThreadLocal<Node> myPred;
  
  ThreadLocal<Node> myNode;
  
  public CLHLock() {
    this.tail = new AtomicReference<>(new Node());
    this.myNode = ThreadLocal.withInitial(Node::new);
    this.myPred = new ThreadLocal<>();
  }
  
  private static class Node {
    private volatile boolean locked = false;
  }
}

Craig的设计中,请求锁的队列节点有两种状态,在实现中可以使用布尔变量代替:

  • PENDING,表示获取到锁或者等待获取锁,可以使用true;

  • GRANTED,表示释放锁,可以使用false。

另外CLHLock的初始化中,
this.tail = new AtomicReference<>(new QNode())
添加了默认节点,该节点的locked默认为false,这是借鉴了链表处理时常用到技巧虚拟头节点。

CLH锁的
加锁

public void lock() {
  Node myNode = this.myNode.get();
  myNode.locked = true;
  Node pred = this.tail.getAndSet(myNode);
  this.myPred.set(pred);
  while(myPred.locked) {
    System.out.println("自旋,可以加入退避算法");
  }
}

实现中巧妙的使用了两个ThreadLocal变量来构建出了逻辑上的链表,和传统意义的单向链表不同,
CLH的链表从尾节点开始指向头部

另外,CLH锁中的节点只关心自身前驱节点的状态,当前驱节点释放锁的那一刻,节点就知道轮到自己获取锁了。

CLH锁的
解锁

public void unlock() {
  Node myNode = this.myNode.get();
  myNode.locked = false;
  this.myNode.set(this.myPred.get());
}

解锁的逻辑也非常简单,只需要更新自身的锁标识即可。但是你可能会疑问
this.myNode.set(this.myPred.get())
是用来干嘛的?删除会产生什么影响吗?

Tips

Java版
CLH
Lock
,供大家参考,代码有详细的注释。

单线程场景

在单线程场景中,完成CLH锁的初始化后,锁的内部结构是如下:

Tips
:@后表示Node节点的地址。

第一次加锁后状态如下:

这时前驱节点的锁标记为false,表示当前节点可以直接获取锁。

第一次解锁后状态如下:

到目前为止一切都很正常,但是当我们再次加锁时会发现,好像没办法加锁了,我们来逐行代码分析锁的状态。当获取myNode后并更新锁标识,即执行如下代码后:

Node myNode = this.myNode.get();
myNode.locked = true;

当获取并更新tail和myPred后,即执行如下代码后:

Node pred = this.tail.getAndSet(myNode);
this.myPred.set(pred);

这时候问题出现了,
myNode == myPred
,导致永远无法获取锁。
this.myNode.set(this.myPred.get())
相当于在链表中移除当前节点,
使获取锁的节点的直接前驱节点永远是初始化时锁标识为false的默认节点。

多线程场景

再来考虑多线程的场景,假设有线程t1和线程t2争抢锁,此时t1率先获取到锁:

线程t1释放后再次立即获取是有可能出现的,最典型的情况是如果为自旋逻辑添加了退避算法,当线程t2多次自旋后再次进入自旋逻辑,此时线程t1释放锁后立即尝试获取锁,先更新线程t1的锁标记为true,接着从tail节点中获取前驱节点线程t2,然后再更新tail节点,此时线程t1在线程t2的锁标记上自旋,线程t2在线程t1的锁标记上自旋,凉凉~~

留个思考题,为什么
this.myNode.set(this.myPred.get())
可以避免这种情况?

CLH锁和MCS锁的对比

首先是代码实现上,CLH锁的实现非常简单,除了自旋的部分其余全是平铺直叙,反观MCS锁,分支,嵌套,从实现难度上来看CLH锁更胜一筹(难点在于逆向思维,让当前节点自旋在直接前驱节点的锁标识上)。另外,CLH锁只在加锁时使用了一次原子指令,而MCS锁的加解锁中都需要使用原子指令,性能上也略胜一筹。

那么CLH锁是全面超越了MCS锁吗?不是的,在
NUMA
架构下,CLH锁的自旋性能非常差。先来看NUMA架构的示意图:

NUMA架构中,每个CPU有自己缓存,访问不同CPU缓存的成本较高,在需要频繁进入自旋的场景中CLH锁自旋的性能较差,而在需要频繁解锁更新其他CPU锁标识的场景中MCS锁的性能较差。

结语

到目前为止,我们一起学习了3种基于排队思想的自旋锁设计,作为AQS的“前世”,理解它们的设计能够帮助我们理解AQS的原理。当然并非只有这3种基于排队思想的自旋锁,还有如RHLock,HCLHLock等,感兴趣的可以自行探索,这里提供论文链接:


好了,今天就到这里了,Bye~~

一、源起

作者是名超大龄程序员,曾涉及了包括Web端、桌面端、移动端等各类前端技术,深受这些前端技术的苦,主要但不限于:

  1. 每种技术编写代码的语言及技术完全不同,同样呈现形式的组件各端无法通用;
  2. 大部分前端开发语言跟后端开发语言不同,不能共用一些数据结构;

前端UI的本质是在显示器上呈现由像素点组成的画面,并且响应外部输入事件作出相应的重绘。由于作者对Skia2D绘图引擎比较熟悉,又恰好可以借鉴一下Flutter引擎的跨端实现,所以作者动起了重新造一个跨端UI的念头。 阿基米德说过:“给我一个支点,我可以撬动地球”,那作者要说:"给我一块画布,我可以造一个全新的跨端UI"。

二、画布及画笔:

有了画布才能绘制用户界面,目前画布的来源主要是两类:

  1. Web端参考Flutter的实现,利用编译为WebAssembly的CanvasKit提供;
  2. 桌面端及移动端参考Xamarin的实现,利用原生操作系统的视窗结合Skia的SkCanvas提供;

每个窗体的画布分为两层,一层绘制Widget,另一层用于弹出层的绘制及一些组件装饰器的绘制。绘制引擎暂统一由Skia来处理,将来可能会考虑抽象绘制引擎。

三、组件树、布局及样式

Flutter有三棵树,作者嫌啰嗦所以只有一棵WidgetTree,好处是实现简单且方便维持组件实例的状态。每个界面都由组件树结构组成。有些组件为布局类的(eg: Column、Stack等),具备单或多子组件;有些组件为叶子节点(eg: Text、PieChart等),通过设置相应的属性后直接绘制至画布。

四、组件状态

实现组件时如果需要外部状态驱动,可以定义状态变量并绑定至组件的相关属性,这样当状态值发生变更时,绑定的组件根据状态影响进行重新布局或仅重新绘制。

public class DemoCounter : View 
{
    private readonly State<int> _counter = 0; //定义状态
    
    public DemoCounter() 
    {
        Child = new Column
        {
            Children = new Widget[]
            {
                new Text(_counter.AsStateOfString()/*绑定至组件*/),
                new Button("+") { OnTap = e => _counter.Value+=1/*改变状态值*/ }
            }
        };
    }
}

五、组件动画

动画实现基本照搬Flutter的实现方式,由AnimationController在指定时间段内驱动各Animation的动画值变化,从而连续改变组件的状态值。

public class DemoAnimation : View
{
    private readonly AnimationController _controller;
    private readonly Animation<Offset> _offsetAnimation;
    
    public DemoAnimation()
    {
        _controller = new AnimationController(1000/*动画时长*/);
        _offsetAnimation = new OffsetTween(new Offset(-1, 0), new Offset(1, 0))
            .Animate(_controller); //位移变换并绑定至动画控制器
        
        Child = new Column
        {
            Children = new Widget[]
            {
                new Button("播放动画") { OnTap = e => _controller.Forward() },
                new SlideTransition(_offsetAnimation)
                {
                    Child = new Text("动画")
                }
            }
        };
    }
}

六、后续

力量有限,在此抛砖引玉希望更多感兴趣的伙伴加入完善,也希望成为跟华为ArkUI类似的国产UI,对了暂时就叫PixUI吧。