2024年3月

写在开头

在介绍synchronized关键字时,我们提到了锁升级时所用到的CAS算法,那么今天我们就来好好学一学这个
CAS算法

CAS算法对build哥来说,可谓是刻骨铭心,记得是研二去找实习的时候,当时对很多八股文的内容浅尝辄止,很多深奥的知识点只是知道个概念,源码看的也不深,代码量也不够,京东一面,面试官问了CAS算法,大概的介绍了之后,他紧接着追问CAS的三大问题,在很多面试类书籍中背过ABA问题,然后就囫囵吞枣的答了这个,即便后面在面试官的引导下,也没有说清楚其他两个,最终遗憾败北。

面试官当时给的面试表现是:只注重死记硬背,程序员是一个需要创造性的工作,而不是做一个笔者。回来难过了很久,从那时候起,就痛定思痛,大量的看源码,写demo,争取不做同一个知识点上的小丑!现在回想起来,仍然是一份激励,不知道大家在面试时有没有过窘迫,希望诸君能铭记于心,勉而励之!

原子性问题

好了,废话说太多了,现在进入正题!在之前的文章中调到了并发多线程的三大问题,其中之一就是
原子性
,讲volatile关键字时,说到它可以保证有序性和可见性但无法保证原子性,啥是原子性呢?

原子性:
一个或者多个操作在 CPU 执行的过程中不被中断的特性;

原子操作:
即最小不可拆分的操作,也就是说操作一旦开始,就不能被打断,直到操作完成。

什么时候原子性问题呢?引用我们之前写过的案例。

【代码示例1】

public class Test {
    //计数变量
    static volatile int count = 0;
    public static void main(String[] args) throws InterruptedException {
        //线程 1 给 count 加 10000
        Thread t1 = new Thread(() -> {
            for (int j = 0; j <10000; j++) {
                count++;
            }
            System.out.println("thread t1 count 加 10000 结束");
        });
        //线程 2 给 count 加 10000
        Thread t2 = new Thread(() -> {
            for (int j = 0; j <10000; j++) {
                count++;
            }
            System.out.println("thread t2 count 加 10000 结束");
        });
        //启动线程 1
        t1.start();
        //启动线程 2
        t2.start();
        //等待线程 1 执行完成
        t1.join();
        //等待线程 2 执行完成
        t2.join();
        //打印 count 变量
        System.out.println(count);
    }
}

我们创建了2个线程,分别对count进行加10000操作,理论上最终输出的结果应该是20000万对吧,但实际并不是,我们看一下真实输出。

输出:

thread t1 count 加 10000 结束
thread t2 count 加 10000 结束
14281

原因:
Java 代码中 的 count++ ,至少需要三条CPU指令:

  • 指令 1:把变量 count 从内存加载到CPU的寄存器
  • 指令 2:在寄存器中执行 count + 1 操作
  • 指令 3:+1 后的结果写入CPU缓存或内存

即使是单核的 CPU,当线程 1 执行到指令 1 时发生线程切换,线程 2 从内存中读取 count 变量,此时线程 1 和线程 2 中的 count 变量值是相等,都执行完指令 2 和指令 3,写入的 count 的值是相同的。从结果上看,两个线程都进行了 count++,但是 count 的值只增加了 1。这种情况多发生在cpu占用时间较长的线程中,若单线程对count仅增加100,那我们就很难遇到线程的切换,得出的结果也就是200啦。
image

解决办法:
可以通过JDK Atomic开头的原子类、synchronized、LOCK解决多线程原子性问题,这其中Atomic开头的原子类就是使用乐观锁的一种实现方式CAS算法实现的,那么在了解CAS算法之前,我们还是要先来聊一聊乐观锁。

乐观锁与悲观锁

乐观锁与悲观锁是一组互反锁。

悲观锁(Pessimistic Lock):
线程每次在处理共享数据时都会上锁,其他线程想处理数据就会阻塞直到获得锁。如 synchronized、java.util.concurrent.locks.ReentrantLock;

【代码示例2】

public void testSynchronised() {
    synchronized (this) {
        // 需要同步的操作
    }
}

private Lock lock = new ReentrantLock();
lock.lock();
try {
   // 需要同步的操作
} finally {
    lock.unlock();
}

乐观锁(Optimistic Lock):
相对乐观,线程每次在处理共享数据时都不会上锁,在更新时会通过数据的版本号机制判断其他线程有没有更新数据,或通过CAS算法实现,乐观锁适合读多写少的应用场景。

版本号机制:
所谓版本号机制,一般是在数据表中加上一个数据版本号 version 字段,来记录数据被修改的次数,线程读取数据时,会把对应的version值也读取下来,当发生更新时,会先将自己读取的version值与数据表中的version值进行比较,如果相同才会更新,不同则表示有其他线程已经抢先一步更新成功,自己继续尝试。

CAS算法:
CAS全称为
Compare And Swap(比较与交换)
,是一种算法,更是一种思想,常用来实现乐观锁,通俗理解就是在更新数据前,先比较一下原数据与期待值是否一致,若一致说明过程中没有其他线程更新过,则进行新值替换,否则更新失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。

两种锁的优缺点:

  • 乐观锁适用于读多写少的场景,可以省去频繁加锁、释放锁的开销,提高吞吐量;
  • 在写比较多的场景下,乐观锁会因为版本号不一致,不断重试更新,产生大量自旋,消耗 CPU,影响性能。这种情况下,适合悲观锁。

CAS算法

那么CAS算法是如何实现的呢?其实在Java中并没有直接给与实现,而是通过JVM底层实现,底层依赖于一条 CPU 的原子指令。
那我们在Java中怎么使用,或者说哪里准寻CAS的痕迹呢?
别急,跟着build哥继续向下看!

我们在上面提到了JDK Atomic开头的原子类可以解决原子性问题,那我们就跟进去一探究竟,首先,进入到
java.util.concurrent.atomic
中,里面支持原子更新数组、基本数据类型、引用、字段等,如下图:

image

现在,我们以比较常用的AtomicInteger为例,选取其getAndAdd(int delta)方法,看一下它的底层实现。

【源码解析1】

public final int getAndAdd(int delta) {
    return unsafe.getAndAddInt(this, valueOffset, delta);
}

这里返回的时Unsafe类的getAndAddInt()方法,Unsafe类在sun.misc包中。我们继续根据方法中看源码:

【源码解析2】

 public final int getAndAddInt(Object var1, long var2, int var4) {
     int var5;
     do {
         var5 = this.getIntVolatile(var1, var2);
     } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

     return var5;
 }

我们看一下方法中的参数含义

  • Object var1
    :这个参数代表你想要进行操作的对象的起始地址,如:0x00000666666。
  • long var2
    :这个参数是你想要操作的 var1对象中的偏移量。这个偏移量可以通过 Unsafe 类的 objectFieldOffset 方法获得。通俗理解就是需要修改的具体内存地址:如100 ,0x0000011+100 = 0x0000666666就是要修改的值的最终内存地址。
  • int var4
    :这个参数是你想要增加的值。

首先
,在这个方法中采用了do-while循环,通过getIntVolatile(var1, var2)获取当前对象指定的字段值,并将其存入var5中作为预期值,这里的getIntVolatile方法可以保证读取的可见性(禁止指令重拍和CPU缓存,这个之前的文章里解释过,不然冗述);

然后
,在while中调用了Unsafe类的compareAndSwapInt()方法,进行数据的CAS操作。其实在这个类中有好几个CAS操作的实现方法

【源码解析3】

/**
  *  CAS
  * @param o         包含要修改field的对象
  * @param offset    对象中某field的偏移量
  * @param expected  期望值
  * @param update    更新值
  * @return          true | false
  */
public final native boolean compareAndSwapObject(Object o, long offset,  Object expected, Object update);

public final native boolean compareAndSwapInt(Object o, long offset, int expected,int update);

public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update);

这几个方法都是native方法,相关的实现是通过 C++ 内联汇编的形式实现的(JNI 调用),因此,和cpu与操作系统都有关系,这也是我们在上文中提到CAS失败后,大量自旋带来CPU消耗严重的原因。

继续
,我们回到compareAndSwapInt(var1, var2, var5, var5 + var4)方法中来,我们通过var1对象在var2内存地址上的值与先查到的预期值比较一致性,若相等,则将var5 + var4更新到对应地址上,返回true,否则不做任何操作返回false。

如果 CAS 操作成功,说明我们成功地将 var1 对象的 var2 偏移量处的字段的值更新为 var5 + var4,并且这个更新操作是原子性的,因此我们跳出循环并返回原来的值 var5。

如果 CAS 操作失败,说明在我们尝试更新值的时候,有其他线程修改了该字段的值,所以我们继续循环,重新获取该字段的值,然后再次尝试进行 CAS 操作。

注意:
以上是JDK1.8的源码,在JDK1.9后底层实现逻辑略有改动,增加了@HotSpotIntrinsicCandidate 注解,这个注解允许 HotSpot VM 自己来写汇编或 IR 编译器来实现该方法以提供更加的性能。

CAS带来的三大问题

文章写到这里,终于进入了关键,CAS虽然作为一种不加锁就可以实现高效同步的手段,但它并非完美,仍然存在着很多问题,主要分为三个,分别是:
ABA问题

长时间自旋

多个共享变量的原子操作
,这三个问题也是面试官提及CAS时常问的,希望大家能够理解记住,避免像build哥初入职场时的尴尬!

ABA问题

这是CAS非常经典的问题,由于CAS是否执行成功,是需要将当前内存中的值与期望值做判断,根据是否相等,来决定是否修改原值的,若一个变量V在初始时的值为A,在赋值前去内存中检查它的值依旧是A,这时候我们就想当然认为它没有变过,然后就继续进行赋值操作了,很明显这里是有漏洞的,虽然赋值的操作用时可能很短,但在高并发时,这个A值仍然有可能被其他线程改为了B之后,又被改回了A,那对于我们最初的线程来说,是无法感知的。

很多人可能会问,既然这个变量从A->B->A,这个过程中,它不还是原来的值吗,过程不同但结果依旧没变呀,会导致什么问题呢?我们看下面这个例子:

小明在提款机,提取了50元,因为提款机卡住了,小明点击后,又点击了一次,产生了两个修改账户余额的线程(可以看做是线程1和线程2),假设小明账户原本有100元,因此两个线程同时执行把余额从100变为50的操作。
线程1
(提款机):获取当前值100,期望更新为50。
线程2
(提款机):获取当前值100,期望更新为50。
线程1成功执行,CPU并没有调度线程2执行,
这时,小华给小明转账50,这一操作产生了
线程3
,CPU调度线程3执行,这时候线程3成功执行,余额变为100。之后,线程2被CPU调度执行,此时,获取到的账户余额是100,CAS操作成功执行,更新余额为50!此时可以看到,实际余额应该为100(100-50+50),但是实际上变为了50(100-50+50-50)。

这就是ABA问题带来的错误,而对于一个银行的提款机来说,发生这种问题可以说是灾难性的,会大大降低客户对于这家银行的信任程度!

那有没有什么解决方案呢,答案是肯定的!在JDK 1.5 以后的 AtomicStampedReference 类就是用来解决 ABA 问题的,其中的 compareAndSet() 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

【源码解析4】

public boolean compareAndSet(V   expectedReference,
                             V   newReference,
                             int expectedStamp,
                             int newStamp) {
    Pair<V> current = pair;
    return
        expectedReference == current.reference &&
        expectedStamp == current.stamp &&
        ((newReference == current.reference &&
          newStamp == current.stamp) ||
         casPair(current, Pair.of(newReference, newStamp)));
}

长时间自旋

我们在前面说过CAS适用于读多写少的场景,若被使用在写多的场景,必然会产品大量的版本号不一致情况,从而导致很多线程自旋等待,这对CPU来说很糟糕,可以通过让JVM 能支持处理器提供的 pause 指令,这样对效率会有一定的提升。

PAUSE指令提升了自旋等待循环(spin-wait loop)的性能。当执行一个循环等待时,Intel P4或Intel Xeon处理器会因为检测到一个可能的内存顺序违规(memory order violation)而在退出循环时使性能大幅下降。PAUSE指令给处理器提了个醒:这段代码序列是个循环等待。处理器利用这个提示可以避免在大多数情况下的内存顺序违规,这将大幅提升性能。因为这个原因,所以推荐在循环等待中使用PAUSE指令。

多个共享变量的原子操作

当对一个共享变量执行操作时,CAS 能够保证该变量的原子性。但是对于多个共享变量,CAS 就无法保证操作的原子性,这时通常有两种做法:

  1. 使用AtomicReference类保证对象之间的原子性,把多个变量放到一个对象里面进行 CAS 操作;
  2. 使用锁。锁内的临界区代码可以保证只有当前线程能操作。

总结

关于CAS算法以及其存在的三大问题到这里就说完了,现在再回头来看,京东这道面试题很简单,然而由于当年的不努力变成了一种遗憾说出,希望小伙伴们能够引以为戒!

结尾彩蛋

如果本篇博客对您有一定的帮助,大家记得
留言+点赞+收藏
呀。原创不易,转载请联系Build哥!

image

如果您想与Build哥的关系更近一步,还可以关注“JavaBuild888”,在这里除了看到《Java成长计划》系列博文,还有提升工作效率的小笔记、读书心得、大厂面经、人生感悟等等,欢迎您的加入!

image

前言

自从Typora收费以后经常有朋友会问有没有一个好用、简洁、免费的Markdown编辑器推荐的,今天大姚给大家分享一款比Typora更简洁优雅的、完全开源免费(MIT License)Markdown编辑器神器:MarkText。

MarkText简介

Typora的完美替换Markdown编辑器,比Typora更简洁优雅的Markdown编辑器神器。

MarkText是一个基于MIT License完全开源免费的Markdown编辑器,支持Windows、macOS和Linux系统平台。它具有实时预览功能,支持数学表达式、代码高亮,并能够输出HTML和PDF文件。此外,MarkText还支持直接从剪贴板粘贴图像,让编辑过程更加简洁和优雅。无论您是写作、笔记还是进行技术文档撰写,MarkText都是一个功能强大且易于使用的工具。

主要特点和功能

  • 所见即所得编辑器:MarkText 提供了一个直观的所见即所得编辑器,让用户可以在编辑时立即看到最终渲染的效果,这有助于用户更直观地处理 Markdown 格式。
  • 实时预览:编辑器支持实时预览功能,用户可以在编辑时随时查看 Markdown 文档的渲染效果,确保最终的呈现符合期望。
  • 语法高亮:MarkText 提供了语法高亮显示,使 Markdown 文档中的不同元素(标题、列表、代码块等)更易于辨认,提高可读性。
  • 多主题支持:用户可以选择不同的编辑主题,以适应个人偏好或不同的环境。
  • 自定义样式:用户可以通过自定义 CSS 样式来定制编辑器和渲染效果,以满足个性化需求。
  • 导出功能:MarkText 支持将 Markdown 文档导出为多种格式,包括 HTML、PDF 等,使用户能够方便地分享或发布文档。
  • 标签页支持:用户可以在同一个窗口中打开多个 Markdown 文档,通过标签页进行切换,提高多文档管理的效率。
  • 跨平台:MarkText 是跨平台的,支持 Windows、macOS 和 Linux 系统,使用户能够在不同的操作系统上使用相同的编辑工具。
  • 快捷键支持:提供了丰富的快捷键,方便用户快速执行常用操作,提高编辑效率。

多主题支持

Cadmium Light

Dark

Graphite Light

Material Dark

Ulysses Light

One Dark

常用快捷键

快捷键 功能
Ctrl + S 保存
Ctrl + B 加粗
Ctrl + I 斜体
Ctrl + U 下划线
Ctrl + D 删除线
Ctrl + L 插入链接
Ctrl +Shift +K 插入代码块
Ctrl +Shift +T 插入表格
Ctrl + O 打开文件
Ctrl + N 新建文件
Ctrl + F 查找
Ctrl + E 切换源码模式
Ctrl + Z 撤销
Ctrl + K 显示目录

下载地址

MarkText中文教程

程序员常用的工具软件

该工具已收录到程序员常用的工具软件栏目中,欢迎关注该栏目发现更多优秀实用的开发工具!

❤系列①②已经完成了这个项目的页面和项目的全部编码,前后端分离,这个文章将向你展示运维小伙伴如何部署到
windows服务器

linux服务器

docker部署
,一学就会,快来看看吧!

❤说明:这个系列准备用Simple快速框架搞个自己能用的网盘,来个实战,教大家如何搞一个项目,其中你能学到如何进行项目级对接,如何快速进行项目编码,如何完善你的项目,以及如何部署它。

为了重复利用已有的代码,我使用自定义插件进行开发。当每个插件独立开发时没有遇到问题,但是当插件B引用了插件A时就会在编译时报错 error: LNK2001: 无法解析的外部符号。
例如,先定义一个插件ColorPicker,用于颜色选取。关键代码如下:

class QDESIGNER_WIDGET_EXPORT ColorPicker : public QWidget
{
    Q_OBJECT
    Q_PROPERTY(QColor color READ getColor WRITE setColor NOTIFY colorChanged)

public:
    explicit ColorPicker(QWidget *parent = nullptr);
    ~ColorPicker();
    QColor getColor() const;
    void setColor(QColor newColor);
Q_SIGNALS:
    void colorChanged(const QColor oldColor, const QColor newColor);
protected:
    void paintEvent(QPaintEvent *event);
    void mousePressEvent(QMouseEvent *event);
private:
    QScopedPointer<ColorPickerPrivate> d_ptr;
    Q_DECLARE_PRIVATE(ColorPicker)
};

该插件在APP中使用正常,但是当在另外一个插件LineProperty中使用ColorPicker 时就会报错。

class QDESIGNER_WIDGET_EXPORT LineProperty : public QWidget
{
    Q_OBJECT
    Q_PROPERTY(Qt::PenStyle lineStyle READ lineStyle WRITE setLineStyle NOTIFY lineStyleChanged FINAL);
    Q_PROPERTY(qreal lineWidth READ lineWidth WRITE setLineWidth NOTIFY lineWidthChanged FINAL);
    Q_PROPERTY(QColor lineColor READ lineColor WRITE setLineColor NOTIFY lineColorChanged FINAL);
public:
    explicit LineProperty(QWidget *parent = nullptr);
    ~LineProperty();
    Qt::PenStyle lineStyle() const;
    void setLineStyle(Qt::PenStyle newLineStyle);
    qreal lineWidth() const;
    void setLineWidth(qreal newLineWidth);
    QColor lineColor() const;
    // 在UI中使用了ColorPicker 插件
    void setLineColor(const QColor &newLineColor);

Q_SIGNALS:
    void linePropertyChanged(Qt::PenStyle style, QColor Color, qreal width);
    void lineStyleChanged(Qt::PenStyle style);
    void lineWidthChanged(qreal width);
    void lineColorChanged(QColor color);
protected:
    bool eventFilter(QObject *watched, QEvent *event) override;
private slots:
    void on_lineStyle_currentIndexChanged(int index);
    void on_lineColor_colorChanged(const QColor &oldColor, const QColor &newColor);
    void on_lineWidth_valueChanged(int arg1);
private:
    QScopedPointer<LinePropertyPrivate> d_ptr;
    Q_DECLARE_PRIVATE(LineProperty)
    Q_DISABLE_COPY(LineProperty)
};

网上大部分解决方法是这样的:

如果用到信号槽但类的定义没有放在.h文件中, qmake不会自动调moc, 你就需要写#include "moc_ColorPicker.cpp"告诉qmake你有文件需要moc

经过验证,在LineProperty .cpp文件末尾追加#include "moc_ColorPicker.cpp" 确实没有再报错。
实际上是编译器在LINK这个环节没有找到ColorPicker库文件,但是在pro文件中已经配置LIBS += -L$$OUT_PWD/../ColorPicker/release/ -lcolorpickerplugin
问题只可能是ColorPicker.h文件的定义问题。
ColorPicker类定义使用了QDESIGNER_WIDGET_EXPORT 宏,该宏用于将自定义组件类从插件导出给 Qt Designer 使用。查看QDESIGNER_WIDGET_EXPORT 的定义如下:

#if defined(QDESIGNER_EXPORT_WIDGETS)
#  define QDESIGNER_WIDGET_EXPORT Q_DECL_EXPORT
#else
#  define QDESIGNER_WIDGET_EXPORT Q_DECL_IMPORT
#endif

LineProperty 引入ColorPicker.h文件时QDESIGNER_WIDGET_EXPORT 仍然解释为Q_DECL_EXPORT,从而导致ColorPicker库文件没有被导入到LineProperty ,所以LINK时会报错。

只需要修改ColorPicker.h文件定义,使用ColorPicker 专属的COLOR_PICKER_EXPORT即可:

#if defined(COLOR_PICKER_LIB)
#define COLOR_PICKER_EXPORT Q_DECL_EXPORT
#else
#define COLOR_PICKER_EXPORT Q_DECL_IMPORT
#endif
class COLOR_PICKER_EXPORT ColorPicker : public QWidget
{
   ...略
};

参考:
error LNK2001: 无法解析的外部符号 Qt的moc机制
vs+qt error LNK2001: 无法解析的外部符号 “public: static struct QMetaObject 。。

参考:https://blog.csdn.net/qinguan666666/article/details/132877842(连接不上nacos)

https://verytoolz.com/yaml-formatter.html(yaml格式工具)

https://zhuanlan.zhihu.com/p/661765880?utm_id=0(安装)

https://blog.csdn.net/ifhuke/article/details/130154632(feign)

好吧,从昨天下午到今天快上午一直在被接入nacos这个问题拦在这

1.一开始我就直接搜的springboot如何接入nacos

build.gradle配置:

implementation group: 'com.alibaba.cloud', name: 'spring-cloud-starter-alibaba-nacos-discovery', version: '2022.0.0.0-RC2'

yuml配置:

spring:
  cloud:
    nacos:
      discovery:
        group: XX_GROUP
        namespace: prod
        server-addr: nacos.xxx.com:8848
      config:
        group: XX_GROUP
        namespace: prod
        server-addr: nacos.xxx.com:8848
        shared-configs[0]:
          data-id: xxx-platform-dev.yaml

然后按照教程启动倒是启动起来了,但是我不管怎么在后台刷新都没有看到新服务接入,

于是我便在配置里面加个我的服务名字,看看能不能出来,尽管我的服务正常起来了但是后台依然没有起色。于是我便开始在kimiChat,百度文心一言,chatGpt里面找”springboot3如何接入nacos“,很多个答案, 都尝试了个遍但是感觉都不大行。

最后想会不会要不我自己本地先起一个nacos服务,我先自己尝试连下先连成功了再说吧,于是命令行

docker run --name nacos -e MODE=standalone -p 8848:8848  -d nacos/nacos-server

进入后台
http://127.0.0.1:8848/nacos
看到启动成功

说明我nacos启动成功了,同时看到现在这个都不需要密码就可以登录,好吧,我项目配置先试试,尝试连接~~

报错“Client not connected, current status:STARTING”啥意思啊,我现在要养成好习惯不直接去百度查,先去那三大平台查,

查的结果大概说版本不兼容,网络连接有问题,防火墙等,我看了下我的电脑防火墙关了呀,再本地尝试用
telnet 127.0.0.1:8848
显示连不上,好吧这问题有点玄乎,

算了,百度下吧,搜第一个说新版本的nacos我要多开启两个端口才能连接,执行下面命令

docker run --name nacos -e MODE=standalone -p 8848:8848 -p 9848:9848 -p 9849:9849 -d nacos/nacos-server

然后再尝试本地项目连接

可以了,那就说明我配置没问题,就是那nacos有问题,问了同事,原来是他没有开启9848和9849那两个端口,好吧,我去阿里云给开启下,本地服务再跑下,终于可以了。

2.接着我跟着三大平台教程尝试接入nacos的配置

build.gradle

implementation group: 'com.alibaba.cloud', name: 'spring-cloud-starter-alibaba-nacos-config', version: '2022.0.0.0-RC2'

yaml 配置见上面的config

启动本地项目起来了,然后自己引用了下随便一个配置能返回,说明没啥问题,至此,配置就可以了,接下来就是把之前写在bootstrap-dev.yml里面的全部迁移到nacos上,除了连接nacos的配置

好了能读取上面的yaml就行了,接着搬砖。