【JUC】原子操作类及LongAddr源码分析

news2024/11/20 21:24:43

文章目录

    • 1. 十八罗汉
    • 2. 原子类再分类
      • 2.1 基本类型原子类
      • 2.2 数组类型原子类
      • 2.3 引用类型原子类
      • 2.4 对象的属性修改原子类
      • 2.5 原子操作增强类
    • 3. 代码演示及性能比较:
    • 4. LongAddr原理
    • 5. LongAddr源码分析
      • 5.1 add()
      • 5.2 longAccumulate()
      • 5.3 sum()
    • 6. 小总结
      • 6.1 AtomicLong
      • 6.2 LongAdder

1. 十八罗汉

底层使用Unsafe类的CAS方法,而无需使用synchronized等重量锁使操作变得线程安全

  1. AtomicBoolean
  2. AtomicInteger
  3. AtomicIntegerArray
  4. AtomicIntegerFieldUpdater
  5. AtomicLong
  6. AtomicLongArray
  7. AtomicLongFieldUpdater
  8. AtomicMarkableReference
  9. AtomicReference
  10. AtomicReferenceArray
  11. AtomicReferenceFieldUpdater
  12. AtomicStampedReference
  13. DoubleAccumulator
  14. DoubleAdder
  15. LongAccumulator
  16. LongAdder
  17. Striped64,LongAdder是Striped64的子类
  18. Number,Striped64,AtomicInteger等是Number的子类

2. 原子类再分类

2.1 基本类型原子类

  1. AtomicBoolean
  2. AtomicInteger
  3. AtomicLong

基本使用

public class Temp {
    static int num = 0;
    static AtomicInteger atomicNum = new AtomicInteger();

    public static void main(String[] args) throws InterruptedException {
        int size = 50;
        CountDownLatch count = new CountDownLatch(size);
        for (int i = 0; i < size; i++) {
            new Thread(() -> {
                try {
                    for (int j = 0; j < 1000; j++) {
                        num++;
                        atomicNum.getAndIncrement();
                    }
                } finally {
                    count.countDown();
                }
            }).start();
        }
        count.await();
        System.out.println(num);
        System.out.println(atomicNum.get());
    }
}

输出

39224
50000

2.2 数组类型原子类

如何理解?类比数组即可

  1. AtomicIntegerArray
  2. AtomicLongArray
  3. AtomicReferenceArray

2.3 引用类型原子类

  1. AtomicReference
  2. AtomicStampedReference
    • 用版本号解决CAS的ABA问题,可以统计出修改过几次
  3. AtomicMarkableReference
    • 用状态戳解决CAS的ABA问题,可以标记出是否修改过

2.4 对象的属性修改原子类

  1. AtomicIntegerFieldUpdater:原子更新对象中int类型字段的值
  2. AtomicLongFieldUpdater:原子更新对象中Long类型字段的值
  3. AtomicReferenceFieldUpdater:原子更新对象中引用类型字段的值

目的:以一种线程安全的方式操作非线程安全对象内的某些字段

要求:

  • 更新的对象属性必须使用public volatile修饰
  • 使用静态放法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性
/**
 * 需求:多线程环境下初始化资源类,要求只能初始化一次
 */
public class Temp {

    public volatile Boolean isInit = Boolean.FALSE;

    private static final AtomicReferenceFieldUpdater<Temp, Boolean> ATOMIC_REFERENCE_FIELD_UPDATER =
            AtomicReferenceFieldUpdater.newUpdater(Temp.class, Boolean.class, "isInit");

    public void init(Temp temp) {
        if (ATOMIC_REFERENCE_FIELD_UPDATER.compareAndSet(temp, Boolean.FALSE, Boolean.TRUE)) {
            System.out.println(Thread.currentThread().getName()+":init start");
            try { TimeUnit.SECONDS.sleep(1); } catch (Exception e) {}
            System.out.println(Thread.currentThread().getName()+":init finished");
        } else {
            System.out.println(Thread.currentThread().getName()+":已有线程正在初始化");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        int size = 5;
        CountDownLatch count = new CountDownLatch(size);
        Temp temp = new Temp();
        for (int i = 0; i < size; i++) {
            new Thread(() -> {
                try {
                    temp.init(temp);
                } finally {
                    count.countDown();
                }
            }).start();
        }
        count.await();
    }
}

2.5 原子操作增强类

  1. DoubleAccumulator:一个或多个变量共同维护使用的函数更新的运行double值
  2. DoubleAdder:一个或多个变量共同维持最初的零和double总和
  3. LongAccumulator:一个或多个变量共同维护使用的函数更新的运行long值
  4. LongAdder:一个或多个变量共同维持最初的零和long总和

volatile 解决多线程内存不可见问题。对于一写多读,是可以解决变量同步问题,但是如果多写,同样无法解决线程安全问题。

说明: 如果是 count++操作,使用如下类实现: AtomicInteger count = new AtomicInteger0;count.addAndGet(1);如果是JDK8,推荐使用 LongAdder 对象,比 AtomicLong 性能更好( 减少乐观锁的重试次数 );同时,使用LongAdder的空间代价更大,故越是高并发越推荐使用LongAdder 。

LongAdder只能用来计算加法,且从零开始计算;

LongAccumulator提供了自定义函数的操作且可以传入初始值。

3. 代码演示及性能比较:

/**
 * 需求:点赞数统计,50个线程,每个点赞1千万次
 */
public class Temp {

    long i = 0;
    public synchronized void synchronizedIncrement() {
        i++;
    }

    AtomicLong atomicLong = new AtomicLong();
    public void atomicLongIncrement() {
        atomicLong.getAndIncrement();
    }

    LongAdder longAdder = new LongAdder();
    public void longAdderIncrement() {
        longAdder.increment();
    }

    LongAccumulator longAccumulator = new LongAccumulator(Long::sum, 0);
    public void longAccumulatorIncrement() {
        longAdder.increment();
    }

    public static void main(String[] args) throws InterruptedException {
        int threadNum = 50;
        int times = 10000000;

        process(threadNum, times, 1);
        process(threadNum, times, 2);
        process(threadNum, times, 3);
        process(threadNum, times, 4);
    }

    public static void process(int threadNum, int times, int type) throws InterruptedException {
        long startTime = System.currentTimeMillis();
        CountDownLatch countDownLatch = new CountDownLatch(threadNum);
        Temp temp = new Temp();
        for (int i = 0; i < threadNum; i++) {
            new Thread(() -> {
                try {
                    for (int j = 0; j < times; j++) {
                        switch (type) {
                            case 1: temp.synchronizedIncrement();break;
                            case 2: temp.atomicLongIncrement();break;
                            case 3: temp.longAdderIncrement();break;
                            case 4: temp.longAccumulatorIncrement();break;
                            default: throw new RuntimeException("不存在的类型");
                        }
                    }
                } finally {
                    countDownLatch.countDown();
                }
            }).start();
        }
        countDownLatch.await();
        long endTime = System.currentTimeMillis();
        System.out.println("costTime:" + (endTime - startTime) + " 毫秒," + typeStr(type));
    }

    public static String typeStr(int type) {
        switch (type) {
            case 1: return "synchronizedIncrement";
            case 2: return "atomicLongIncrement";
            case 3: return "longAdderIncrement";
            case 4: return "longAccumulatorIncrement";
            default: throw new RuntimeException("不存在的类型");
        }
    }
}

执行结果:

costTime:22854 毫秒,synchronizedIncrement
costTime:4888 毫秒,atomicLongIncrement
costTime:215 毫秒,longAdderIncrement
costTime:214 毫秒,longAccumulatorIncrement

4. LongAddr原理

LongAddr继承了Striped64,LongAdder的基本思路就是分散热点,将value值分散到一个Cell数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。如果要获取真正的long值,只要将各个槽中的变量值累加返回

sum()会将所有Cell数组中的value和base累加作为返回值,核心的思想就是将之前AtomicLong一个value的更新压力分散到多个value中去,
从而降级更新热点

Striped64比较重要的成员变量

    /** Number of CPUS, to place bound on table size 
     *  CPU数量,即cells数组的最大长度
     */
    static final int NCPU = Runtime.getRuntime().availableProcessors();

    /**
     * Table of cells. When non-null, size is a power of 2.
     * cells数组,长度为2的幂,2,4,8,16...,方便以后位运算
     */
    transient volatile Cell[] cells;

    /**
     * Base value, used mainly when there is no contention, but also as
     * a fallback during table initialization races. Updated via CAS.
     * 基础value值,当并发较低时,只累加该值,主要用于没有竞争的情况,通过CAS更新
     */
    transient volatile long base;

    /**
     * Spinlock (locked via CAS) used when resizing and/or creating Cells.
     * 创建或扩容Cells数组时使用的自旋锁变量调整单元格大小(扩容),创建单元格时使用的锁
     */
    transient volatile int cellsBusy;

在这里插入图片描述

5. LongAddr源码分析

小总结:LongAdder在无竞争的情况,跟AtomicLong一样,对同一个base进行操作,当出现竞争关系时则是采用化整为零分散热点的做法,用空间换时间用一个数组cells,将一个value拆分进这个数组cells。多个线程需要同时对value进行换作时候,可以对线程id进行hash得到hash值,再根据hash值映射到这个数组cells的某个下标,再对该下标所对应的值进行自增操作。当所有线程换作完毕,将数组cells的所有值和base都加起来作为最终结果

5.1 add()

public void add(long x) {
    // as是Striped64中的cells数组属性
    // b是Striped64中的base属性
    // v是当前线程hash到Cell中存储的值
    // m是cells的长度-1,hash时作为掩码使用
    // a是当前线程hash到的Cell
    Cell[] as; long b, v; int m; Cell a;
    // 首次首线程(as = cells) != null一定是false,此时走casBase方法,以CAS的方式更新base值,且只有当cas失败时,才会走到if中
    // 		条件1: cells不为空
    // 		条件2: cas操作base失败,说明其他线程先一步修改了base,出现竞争
    if ((as = cells) != null || !casBase(b = base, b + x)) {
        // true无竞争,false表示竞争激烈,多个线程hash到同一个cell,可能要扩容
        boolean uncontended = true;
        // 条件1:cells为空
        // 条件2:应该不会出现
        // 条件3:当前线程所在cell为空,说明当前线程还没有更新过cell,应初始化一个cell
        // 条件4:更新当前线程所在cell失败,说明竞争很激烈,多个线程hash到了同一个cell,应扩容
        if (as == null || (m = as.length - 1) < 0 ||
            // getProbe()方法返回的是线程中的threadLocalRandomProbe字段
            // 它是通过随机数生成的一个值,对于一个确定的线程这个值是固定的(除非刻意修改它)
            (a = as[getProbe() & m]) == null ||
            !(uncontended = a.cas(v = a.value, v + x)))
            longAccumulate(x, null, uncontended);
    }
}

5.2 longAccumulate()

    /**
     * Handles cases of updates involving initialization, resizing,
     * creating new Cells, and/or contention. See above for
     * explanation. This method suffers the usual non-modularity
     * problems of optimistic retry code, relying on rechecked sets of
     * reads.
     *
     * @param x the value 
     * @param fn the update function, or null for add (this convention
     * avoids the need for an extra field or function in LongAdder). 
     * @param wasUncontended false if CAS failed before call 
     * x: 需要增加的值,一般默认都是1
     * fn: 默认传递的是null
     * wasUncontended: 竞争标识,如果是false标识有竞争。只有cells初始化之后,并且当前线程CAS修改失败才会是false
     */
    final void longAccumulate(long x, LongBinaryOperator fn,
                              boolean wasUncontended) {
        // h:存储线程的probe值
        int h;
        // 如果getProbe()方法返回0,说明随机数未初始化
        if ((h = getProbe()) == 0) { // 这个操作相当于给当前线程生成一个非0的hash值
            // 使用ThreadLocalRandom为当前线程重新计算一个hash值,强制初始化
            ThreadLocalRandom.current(); // force initialization
            // 重新获取probe值,hash值被重置就好比一个全新的线程一样,所以设置了wasUncontended竞争状态为true
            h = getProbe();
            // 重新计算了当前线程的hash后认为此次不算是一次竞争,都未初始化,肯定还不存在竞争激烈,故设置wasUncontended竞争状态为true
            wasUncontended = true;
        }
        // 如果hash取模映射得到的cell单元不是null,则为true,此值也可以看做是扩容意向
        boolean collide = false;                // True if last slot nonempty
        for (;;) {// 自旋:按CASE 2,3,1看代码
            Cell[] as; Cell a; int n; long v;
            // CASE 1:cells已经被初始化了
            if ((as = cells) != null && (n = as.length) > 0) {
                // CASE 1.1 :当前线程hash到的cell还未初始化,则需要进行初始化处理,初始化的值为x即默认的1
                if ((a = as[(n - 1) & h]) == null) {
                    if (cellsBusy == 0) {       // Try to attach new Cell
                        Cell r = new Cell(x);   // Optimistically create
                        // 双端检查并尝试抢锁
                        if (cellsBusy == 0 && casCellsBusy()) {
                            boolean created = false;
                            try {               // Recheck under lock
                                Cell[] rs; int m, j;
                                if ((rs = cells) != null &&
                                    (m = rs.length) > 0 &&
                                    rs[j = (m - 1) & h] == null) {
                                    rs[j] = r;
                                    created = true;
                                }
                            } finally {
                                cellsBusy = 0;
                            }
                            if (created)
                                break;
                            continue;           // Slot is now non-empty
                        }
                    }
                    collide = false;
                }
                // CASE 1.2:竞争激烈,允许重新计算hash值
                else if (!wasUncontended)       // CAS already known to fail
                    wasUncontended = true;      // Continue after rehash
                // CASE 1.3:当前线程hash到已初始化的槽位,使用cas进行更新,更新成功则跳出循环
                else if (a.cas(v = a.value, ((fn == null) ? v + x :
                                             fn.applyAsLong(v, x))))
                    break;
                // CASE 1.4:如果cells的长度已经大于等于CPU核数,就不要再扩容了,继续hash;cells != as说明已有线程正在扩容
                else if (n >= NCPU || cells != as)
                    collide = false;            // At max size or stale
                // CASE 1.5:将扩容意向置为true,如果再hash仍然不满足上诉条件便扩容
                else if (!collide)
                    collide = true;
                // CASE 1.6:尝试扩容,将老数组复制到新数组上
                else if (cellsBusy == 0 && casCellsBusy()) {
                    try {
                        // 当前的cells数组和最先赋值的as是同一个,代表没有被其他线程扩容过
                        if (cells == as) {      // Expand table unless stale
                            Cell[] rs = new Cell[n << 1];
                            for (int i = 0; i < n; ++i)
                                rs[i] = as[i];
                            cells = rs;
                        }
                    } finally {
                        cellsBusy = 0;
                    }
                    collide = false;
                    continue;                   // Retry with expanded table
                }
                // 允许重置当前线程的hash值
                h = advanceProbe(h);
            }
            // CASE 2:cells没有加锁且没有初始化,则尝试对它进行加锁,并初始化cells数组;cellsBusy=0表示无锁状态,cellsBusy=1为持锁状态
            else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
                // 
                boolean init = false;
                try {                           // Initialize table
                    if (cells == as) {// 与条件中cells == as形成双重检查
                        Cell[] rs = new Cell[2];
                        rs[h & 1] = new Cell(x); // 按照当前线程hash到数组中的位置并创建其对应的Cell
                        cells = rs;
                        init = true;
                    }
                } finally {
                    cellsBusy = 0;
                }
                if (init)
                    break;
            }
            // CASE 3:cells正在进行初始化,则尝试直接在基数base上进行累加操作;兜底操作,当新线程进来,而数组正在初始化时,则用base进行累加
            else if (casBase(v = base, ((fn == null) ? v + x :
                                        fn.applyAsLong(v, x))))
                break;                          // Fall back on using base
        }
    }

5.3 sum()

    /**
     * Returns the current sum.  The returned value is <em>NOT</em> an
     * atomic snapshot; invocation in the absence of concurrent
     * updates returns an accurate result, but concurrent updates that
     * occur while the sum is being calculated might not be
     * incorporated.
     *
     * @return the sum
     */
    public long sum() {
        Cell[] as = cells; Cell a;
        long sum = base;
        if (as != null) {
            for (int i = 0; i < as.length; ++i) {
                if ((a = as[i]) != null)
                    sum += a.value;
            }
        }
        return sum;
    }

sum执行时,并没有限制对base和cells的更新。所以LongAdder不是强一致性的,它是最终一致性的

6. 小总结

6.1 AtomicLong

原理:CAS+自旋

场景:低并发下的全局计算,AtomicLong能保证并发情况下计数的准确性,内部使用CAS来解决并发安全问题

缺陷:高并发后性能急剧下降,N个线程CAS操作修改线程的值,每次只有一个成功,其它N-1失败,失败的不停的自旋直到成功,这样大量失败自旋的情况,很浪费CPU资源

6.2 LongAdder

原理:CAS+Base+Cell数组分散,空间换时间并分散了热点数据

场景:高并发下的全局计算

缺陷:sum求和时还有计算线程修改结果的话,最后的结果不够准确

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1088414.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【JUC】CAS原理

CAS原理 文章目录 CAS原理1. 概述2. 原理3. Unsafe4. CAS与自旋锁 1. 概述 compare and swap的缩写&#xff0c;中文翻译成比较并交换,实现并发算法时常用到的一种技术。它包含三个操作数一一内存位置、预期原值及更新值 执行CAS操作的时候&#xff0c;将内存位置的值与预期原…

和鲸 × 北中医:高规格、高并发,一场真正的人工智能分析应用临场实践考核

百舸争流&#xff0c;奋楫者先&#xff0c;2023 年&#xff0c;人工智能已逐渐成为引发千行百业产生颠覆性变革的前沿技术。随着人工智能时代的加速到来&#xff0c;我国政产学研各界对于创新型、复合型 AI 人才的能力要求正不断提升。 面对一批又一批激流勇进、乘风破浪的莘莘…

【Leetcode】 738. 单调递增的数字

当且仅当每个相邻位数上的数字 x 和 y 满足 x < y 时&#xff0c;我们称这个整数是单调递增的。 给定一个整数 n &#xff0c;返回 小于或等于 n 的最大数字&#xff0c;且数字呈 单调递增 。 示例 1: 输入: n 10 输出: 9 示例 2: 输入: n 1234 输出: 1234 示例3 : …

精通Maven的捷径:一文包揽所有必知必学

Maven是每个Java程序都会遇到的包管理工具&#xff0c;今天整理一下Maven的相关知识&#xff0c;从青铜到王者&#xff0c;一文全了解&#xff0c;我们开始吧&#xff01; 1、maven是什么&#xff0c;为什么存在&#xff1f;项目结构是什么样子&#xff0c;怎么定位jar 官方网…

【ROS】ros-noetic和anaconda联合使用【教程】

【ROS】ros-noetic和anaconda联合使用【教程】 文章目录 【ROS】ros-noetic和anaconda联合使用【教程】1. 安装anaconda2. 创建虚拟环境3. 查看python解释器路径4. 在虚拟环境中使用任意的包5. 创建工作空间和ros功能包进行测试Reference 1. 安装anaconda 在Ubuntu20.04中安装…

统一观测丨使用 Prometheus 监控 SQL Server 最佳实践

作者&#xff1a;啃唯 SQL Server 简介 SQL Server 是什么&#xff1f; Microsoft SQL Server 是 Microsoft 推出的关系型数据库解决方案&#xff0c;支持企业 IT 环境中的各种事务处理、商业智能和分析应用程序。Microsoft SQL Server 是市场领先的数据库技术之一。 SQL S…

【算法-动态规划】最长上升子序列-力扣 300

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kuan 的首页,持续学…

无限连接:前端跨页面通信的实现与应用

&#x1f3ac; 江城开朗的豌豆&#xff1a;个人主页 &#x1f525; 个人专栏 :《 VUE 》 《 javaScript 》 &#x1f4dd; 个人网站 :《 江城开朗的豌豆&#x1fadb; 》 ⛺️ 生活的理想&#xff0c;就是为了理想的生活 ! 目录 引言 1. 前端跨页面通信的概述 2. 前端跨页…

RunnerGo测试平台,无代码玩转UI自动化测试

首先需要进入官网&#xff0c;RunnerGo支持开源&#xff0c;可以自行下载安装&#xff0c;也可以点击右上角体验企业版按钮快速体验 点击体验企业版进入工作台后可以点击页面上方的UI自动化 进入到测试页面 创建元素 我们可以在元素管理中创建我们测试时需要的元素 这里我们以…

PPT制作软件哪个好?各种类型的10款好用PPT软件盘点!

想必大家对PPT并不陌生&#xff0c;它在日常的工作或学习中有诸多应用场景&#xff0c;小到制作一份电子相册&#xff0c;大到一场产品发布会&#xff0c;都可以看到PPT的影子。 如果我们细究PPT的话&#xff0c;会发现可以细分出各种类型的PPT软件&#xff0c;诸如&#xff1…

Linux指令集合

磁盘相关命令 df//查看磁盘整体状况df -h //查看磁盘整体状况du //查看磁盘使用情况&#xff0c;如果不加目录&#xff0c;默认为当前目录du -h -d 0//表示我们只希望看当前目录磁盘使用情况&#xff0c;不会继续展示这层目录的下一级目录 du -h -d 1//表示展示当前目录的下一…

微信自动批量添加好友的方法

在现在的营销中微信已成为一种重要的沟通方式。微信目前是没有自动批量添加好友的功能&#xff0c;需要运营者一个一个手动去添加&#xff0c;这样太过于浪费时间&#xff0c;并且加频繁了还容易被封号&#xff0c;今天给大家介绍几种手动批量加好友的方式以及怎么借助第三方软…

深入理解AQS之ReentrantLock源码分析

开题&#xff1a;如何自己生成一把独占锁&#xff1f; 1. 管程 — Java同步的设计思想 管程&#xff1a;指的是管理共享变量以及对共享变量的操作过程&#xff0c;让他们支持并发。 互斥&#xff1a;同一时刻只允许一个线程访问共享资源&#xff1b; 同步&#xff1a;线程之间…

【测试】丝滑版本测试记录2023-10-13

目录 四组24个视频拉取 视频拉取的带宽 本地内存不足 四组24个视频拉取 视频拉取的带宽 本地内存不足 由于 edge需要内存大几百M,加截wasm要更多一点&#xff0c;只剩下 400M,不足以使用

git+gitee代码上传

gitgitee代码上传 1、git应用工具下载安装 gitee应用注册并创建仓库点创建后进入到仓库 git命令关于.gitignore只执行一次的命令经常执行的命令 1、git应用工具 下载安装 https://git-scm.com/downloads $ git --version git version 2.39.0.windows.2代码仓库&#xff1a; …

全力以赴,火山引擎边缘云代表团出战亚运会

END 未来&#xff0c;火山引擎边缘云赛事阵容将继续全力以赴&#xff0c;通过领先、可信赖的云和智能技术&#xff0c;助力游戏行业呈现更加精彩的竞技赛事。

LeetCode【240】搜索二维矩阵

题目&#xff1a; 思路&#xff1a; 1、单靠对角线元素无法判定位置 2、主要逐行进行二分 代码&#xff1a; public boolean searchMatrix(int[][] matrix, int target) {int rows matrix.length;int columns matrix[0].length;// 按行进行二分for (int i 0; i < rows…

Apipost连接数据库详解

Apipost提供了数据库连接功能&#xff0c;在接口调试时可以使用数据库获取入参或进行断言校验。目前的Apipost支持&#xff1a;Mysql、SQL Sever、Oracle、Clickhouse、达梦数据库、PostgreSQL、Redis、MongoDB 8种数据库的连接操作 新建数据库连接&#xff1a; 在「项目设置…

ue5蓝图请求接口

安装与使用 1、在虚幻商城搜索 VaRest 插件 2、选择自己项目的对应版本安装 3、查看是否安装成功 4、进入项目后,分别启动VaRest、JSON Blueprint Utilities两个插件(勾选后会提示重启项目) 5、基本用法:打开关卡蓝图使用(url改为自己的接口、Verb是请求方式) 5.1、或者…

了解什么是JWT

目录 JWT 是什么 有什么用 Jwt的使用 依赖 工具类 过滤器 控制器 配置 测试类 案列 JWT 是什么 JWT是指JSON Web Token&#xff0c;它是一种用于在网络应用间安全传递信息的开放标准&#xff08;RFC 7519&#xff09;。JWT通过使用私钥对JSON数据进行签名或者使用公钥…