Random(一)高并发问题,ThreadLocalRandom源码解析

news2024/11/24 12:25:44

目录

    • 1.什么是伪随机数?
    • 2.Random
      • 2.1 使用示例
      • 2.2 什么种子重复,随机数会重复?
      • 2.3 nextInt() 源码分析
      • 2.4 线程安全的实现
      • 2.5 高并发问题
    • 3.ThreadLocalRandom
      • 3.1 使用示例
      • 3.2 current() 源码解析
        • 3.2.1 Thread中保存的变量:
        • 3.2.2 ThreadLocalRandom 中保存的变量和方法:
      • 3.2 nextInt() 源码分析
    • 4.SecureRandom
    • 5.关于 Random 的常见面试题

1.什么是伪随机数?

伪随机数: 通过固定算法产生的随机数就是伪随机数。

  • 只有通过真实的随机时间产生的随机数才是真正的随机数。比如:通过机器的硬件、CPU温度、当天天气、噪声等真随机事件产生随机数。
  • 如果想产生真的随机数,需要一定的物理手段,开销极大,得不偿失。
  • 目前 Java 语言或者说整个计算机行业中使用的随机数函数,都是通过不同的算法计算出来的伪随机数。

2.Random

Random: 是非常常见的伪随机数生成类,种子默认使用系统的时间,生成结果可预测,安全性不高,线程安全。

  • java.lang.Math 中的伪随机数生成使用的就是 Random 实例。

2.1 使用示例

代码测试:

public static void main(String[] args) {
    // Random
    Random random1 = new Random(100);
    Random random2 = new Random(100);
    print(random1);
    print(random2);
}

private static void print(Random random) {
    for (int i = 0; i < 10; i++) {
        System.out.print(random.nextInt(100) + ",");
    }
    System.out.println();
}

执行结果:

15,50,74,88,91,66,36,88,23,13,
15,50,74,88,91,66,36,88,23,13,

2.2 什么种子重复,随机数会重复?

Random 随机数的生成需要一个默认的 long 类型的种子,用于生成一个新的随机种子和随机数,之后产生的随机数都依赖于前一个随机数种子。所以当第一个种子重复时,后面的随机数都会重复

2.3 nextInt() 源码分析

public int nextInt(int bound)

用法: 返回 0(包括)和指定的绑定值(排除)之间的伪随机数 int 值。

当调用 Random.nextInt() 时,实际上时获取 Random 实例的 seed 变量作为当前种子来计算新的种子,然后更新新的种子到当前线程的 threadLocalRandomSeed 变量上,最后根据新种子的值和具体算法计算随机数。

/**
 * @param bound 要返回的随机数的上限范围。必须为正数。
 * @return 返回位于[0, bound]之间的伪随机整数
 * @throws IllegalArgumentException 如果 n 不是正数
 */
public int nextInt(int bound) {
    //bound小于等于0则抛出异常
    if (bound <= 0)
        throw new IllegalArgumentException(BadBound);
    //初步生成伪随机数r以及更新下一个种子值
    int r = next(31);
    //对r使用固定算法进行进一步处理
    int m = bound - 1;
    if ((bound & m) == 0)
        r = (int) ((bound * (long) r) >> 31);
    else {
        for (int u = r;
             u - (r = u % bound) + m < 0;
             u = next(31))
            ;
    }
    return r;
}

2.4 线程安全的实现

Random 在多线程环境下也可以正常使用,主要得益于 next 方法中生成下一个种子的 CAS 处理:

/**
 * 种子(可以看到种子变量是使用 AtomicLong 变量来保存的,方便后续执行 CAS 操作)
 */
private final AtomicLong seed;
/**
 * @param bits 随机位数
 * @return 初步生成的随机数,以及下一个种子
 */
protected int next(int bits) {
    // 定义旧种子,下一个种子
    long oldseed, nextseed;
    // 获取此时的 seed
    AtomicLong seed = this.seed;
    do {
        // 获得此时最新的 seed 值 oldseed 
        oldseed = seed.get();
        // 计算下一个 seed 值 nextseed
        nextseed = (oldseed * multiplier + addend) & mask;
        // 循环尝试 CAS,将 seed 从 oldseed 更新为 nextseed 值,成功后返回,失败重试
        // 如果 seed 的值还是为 oldseed,就用 nextseed 替换掉,并且返回 true,退出 while 循环
        // 如果已经不是 oldseed 了,就返回 false,继续循环
    } while (!seed.compareAndSet(oldseed, nextseed));
    
    // 根据新种子计算出随机数并返回
    return (int)(nextseed >>> (48 - bits));
}

流程图如下:

在这里插入图片描述

  • CAS 操作保证每次只有一个线程可以获取并成功更新种子,失败的线程则需要自选重试,自旋的时候又会获取最新的种子。
  • 因此每一个线程最终总会计算出不同的种子,保证了多线程环境下的数据安全。

2.5 高并发问题

问题简述: 高并发使用 Random 或 SecureRandom 会变慢。

  • 在多线程下使用单个 Random 实例生成随机数时,可能会存在多个线程同时尝试 CAS 更新同一个种子的操作。
  • CAS 保证并发操作只有一个线程能够成功,其他线程会自旋重试,保证了线程安全。
  • 但是如果大量线程频繁的尝试生成随机数,那么可能会造成大量线程因为失败而自旋重试,降低并发性能,消耗 CPU 资源。
  • 因此在 JDK1.7 中出现了 ThreadLocalRandom,就是为了解决这个问题。

3.ThreadLocalRandom

ThreadLocalRandom: 位于 jdk1.7JUC 包,继承并扩展了 Random 类,弥补了多线程下 CAS 共同竞争同一个 seed 导致性能低下的缺陷,多线程下推荐使用

  • ThreadLocalRandom 使用 ThreadLoca 的原理,让每个线程内持有一个本地的种子变量,该种子变量只有在使用随机数时候才会被初始化,多线程下计算新种子时是每一个线程是根据自己内部维护自己线程维护的种子变量进行更新,从而避免了竞争。
  • ThreadLocalRandom 内部的属性操作使用到了 Unsafe 类,这是一个根据字段偏移量来操作对象字段的类,是 JUC 包实现的底层基石类,Unsafe 直接操作 JVM 内存,效率更高,同时也提供了 CAS 实现的 Java 本地接口。

3.1 使用示例

代码实现:

public static void main(String[] args) {
    // 获取 ThreadLocalRandom 伪随机数生成器
    ThreadLocalRandom current = ThreadLocalRandom.current();
    // 生成10个[0,100)的伪随机数
    for (int i = 0; i < 100; i++) {
        System.out.print(current.nextInt(100) + ",");
    }
}

执行结果:

64,15,98,14,68,6,……,73,64,

3.2 current() 源码解析

public static ThreadLocalRandom current()

  • ThreadLocalRandom 的构造函数是私有的,只能使用静态工厂方法 current() 返回它的实例——instance。instance 是ThreadLocalRandom 的一个 static final 属性,该变量是 static 的。在 ThreadLocalRandom 类加载的初始化阶段就初始化好了的,因此不同线程的 current() 方法实际返回的是一个对象,这是单例模式的应用

  • 所以当线程调用 ThreadLocalRandom.current() 静态方法的时候,真正的作用并不是创建实例,而是初始化调用线程的 threadLocalRandomSeed 变量,也就是当前线程的初始化种子,和 threadLocalRandomProbe 变量,并返回之前已经初始化好的实例

3.2.1 Thread中保存的变量:

  • 为了应对线程竞争,Java 中有一个 ThreadLocal 类,为每一个线程分配了一个独立的,互不相干的存储空间。ThreadLocal 的实现依赖于 Thread 对象的 ThreadLocal.ThreadLocalMap threadLocals 成员字段。

  • 与之类似,为了让随机数生成器只访问本地线程数据,从而避免竞争,在 Thread 中,又增加了 3 个成员变量。

  • 这 3 个成员变量只能在当前线程中被访问,并且使用了 @sun.misc.Contended 注解这种缓存填充来避免伪共享。

    什么是伪共享?https://blog.csdn.net/qq_33204709/article/details/129229353

/**
 * 线程本地随机的当前种子。
 */
@sun.misc.Contended("tlr")
long threadLocalRandomSeed;

/**
 * 探测哈希值;如果线程本地随机种子被初始化,那么该值也非0。使用缓存填充(注解方式)来避免伪共享。
 */
@sun.misc.Contended("tlr")
int threadLocalRandomProbe;

/**
 * 从公共线程本地随机序列中分离的二级种子。
 */
@sun.misc.Contended("tlr")
int threadLocalRandomSecondarySeed;

这3个字段作为 Thread 类的成员,便自然和每一个 Thread 对象牢牢地捆绑在一起,因 此成了名副其实的 ThreadLocal 变量,而依赖这几个变量实现的随机数生成器,也就成了 ThreadLocalRandom。

3.2.2 ThreadLocalRandom 中保存的变量和方法:

/**
 * 单例对象,在类加载的时候就被初始化了,后续的线程每次都是获取同一个实例
 */
static final ThreadLocalRandom instance = new ThreadLocalRandom();

/**
 * @return 返回当前线程的 ThreadLocalRandom
 */
public static ThreadLocalRandom current() {
    //使用UNSAFE获取当前线程对象的PROBE偏移量处的int类型的值(PROBE就是static静态块中获取的threadLocalRandomProbe变量的偏移量)
    //如果等于0(每一个线程的threadLocalRandomProbe变量在默认情况下是没有初始化的,默认值就是0)
    //说明当前线程第一次调用 ThreadLocalRandom 的 current 方法,那么就需要调用 localInit 方法计算当前线程的初始化种子变量。
    if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
        //初始化种子
        localInit();
    //返回单例
    return instance;
}

/**
 * 初始化种子
 */
static final void localInit() {
    int p = probeGenerator.addAndGet(PROBE_INCREMENT);
    //跳过0
    int probe = (p == 0) ? 1 : p; //
    long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
    //获取当前线程
    Thread t = Thread.currentThread();
    //设置当前线程的初始化种子,SEED就是static静态块中获取的threadLocalRandomSeed变量的偏移量,值就是seed
    UNSAFE.putLong(t, SEED, seed);
    //设置当前线程的初始化探针,PROBE就是static静态块中获取的threadLocalRandomProbe变量的偏移量,值就是probe
    UNSAFE.putInt(t, PROBE, probe);
}

/**
 * 该字段用于计算当前线程中 threadLocalRandomProbe 的初始化值
 * 这是一个static final变量,但是它的值却是可变的,多线程共享
 */
private static final AtomicInteger probeGenerator = new AtomicInteger();
/**
 * 初始化threadLocalRandomProbe值的默认增量,每个线程初始化时调用一次,就增加0x9e3779b9,值为-1640531527
 * 这个数字的得来是 2^32 除以一个常数,而这个常数就是传说中的黄金比例 1.6180339887
 * 目的是为了让随机数取值更加均匀:https://www.javaspecialists.eu/archive/Issue164.html(0x61c88647就是1640531527)
 */
private static final int PROBE_INCREMENT = 0x9e3779b9;
/**
 * 该字段用于计算初始化SEED值,这是一个static final常量,但是它的值却是可变的
 */
private static final AtomicLong seeder = new AtomicLong(initialSeed());
/**
 * 初始化seeder值的默认增量,每个线程初始化时调用一次,就会增加SEEDER_INCREMENT
 */
private static final long SEEDER_INCREMENT = 0xbb67ae8584caa73bL;

3.2 nextInt() 源码分析

public int nextInt(int bound)

用法: 返回 0(包括)和指定的绑定值(排除)之间的伪随机数 int 值。

当调用 ThreadLocaRandom.nextInt() 时,实际上时获取当前线程的 threadLocalRandomSeed 变量作为当前种子来计算新的种子,然后更新新的种子到当前线程的 threadLocalRandomSeed 变量上,最后根据新种子的值和具体算法计算随机数。

/**
 * @param bound 伪随机数上限
 * @return 获取[0, bound)的伪随机整数
 */
public int nextInt(int bound) {
    //bound范围校验
    if (bound <= 0)
        throw new IllegalArgumentException(BadBound);
    //根据当前线程中的种子计算新种子
    int r = mix32(nextSeed());
    //根据新种子和bound计算随机数
    int m = bound - 1;
    if ((bound & m) == 0) // power of two
        r &= m;
    else { // reject over-represented candidates
        for (int u = r >>> 1;
             u + m - (r = u % bound) < 0;
             u = mix32(nextSeed()) >>> 1)
            ;
    }
    return r;
}

/**
 * 用于根据当前种子,计算和更新下一个种子
 *
 * @return
 */
final long nextSeed() {
    Thread t;
    long r; // read and update per-thread seed
    //更新计算出的种子,即更新当前线程的threadLocalRandomSeed变量
    UNSAFE.putLong(t = Thread.currentThread(), SEED,
            //计算新种子,为原种子+增量
            r = UNSAFE.getLong(t, SEED) + GAMMA);
    return r;
}

/**
 * 种子增量
 */
private static final long GAMMA = 0x9e3779b97f4a7c15L;

4.SecureRandom

SecureRandom: 伪随机数,可以理解为 Random 的安全升级版,它的种子选取比较多,主要有:时间,cpu,使用情况,点击事件等一些种子,安全性高。

相同点: 除 ThreadLoalRandom 不能指定种子外,Random 和 SecureRandom 在种子相同时,产生的随机数相同。

代码测试:

public static void main(String[] args) {
    // SecureRandom
    SecureRandom secureRandom1 =new SecureRandom("abcd".getBytes());
    SecureRandom secureRandom2 =new SecureRandom("abcd".getBytes());
    print(secureRandom1);
    print(secureRandom2);
}

/** 打印 */
private static void print(Random random) {
    for (int i = 0; i < 10; i++) {
        System.out.print(random.nextInt(100) + ",");
    }
    System.out.println();
}

执行结果:

15,50,74,88,91,66,36,88,23,13,
15,50,74,88,91,66,36,88,23,13,

34,44,40,30,1,73,49,4,37,56,
34,44,40,30,1,73,49,4,37,56,

5.关于 Random 的常见面试题

Q:Random是不是线程安全的?
A: Random是线程安全的,但是多线程下可能性能比较低。

Q:ThreadLocalRandom为什么这么快?
A: 其实这个看下源码就知道了,因为Random用了很多 CAS 的操作,ThreadLocalRandom 根本没有用到。

Q:为什么在高强度要求的情况下,不要用Random?
A: 特别是在生成验证码的情况下,不要使用Random,因为它是线性可预测的。记得有个新闻说的是一个赌博网站,为了说明其公平,公开的它的源代码,结果因为随机数可预测漏洞被攻击了。所以在安全性要求比较高的场合,应当使用SecureRandom。
参考:http://news.cnblogs.com/n/206074/

Q:从理论上来说计算机产生的随机数都是伪随机数,那么如何产生高强度的随机数?
A: 产生高强度的随机数,有两个重要的因素:种子和算法。当然算法是可以有很多的,但是如何选择种子是非常关键的因素。如Random,它的种子是System.currentTimeMillis(),所以它的随机数都是可预测的。那么如何得到一个近似随机的种子?这里有一个很别致的思路:收集计算机的各种信息,如键盘输入时间,CPU时钟,内存使用状态,硬盘空闲空间,IO延时,进程数量,线程数量等信息,来得到一个近似随机的种子。这样的话,除了理论上有破解的可能,实际上基本没有被破解的可能。而事实上,现在的高强度的随机数生成器都是这样实现的。
比如Windows下的随机数生成器:cryptGenRandom 函数 (wincrypt.h)
http://msdn.microsoft.com/en-us/library/aa379942%28VS.85%29.aspx
Linux下的 /dev/random:
http://zh.wikipedia.org/wiki//dev/random
据SecureRandom的Java doc,说到在类unix系统下,有可能是利用 /dev/random,来实现的。

其它的一些有意思的东东:
最快的安全性要求不高的生成UUID的方法(注意,强度不高,有可能会重复):

new UUID(ThreadLocalRandom.current().nextLong(), ThreadLocalRandom.current().nextLong());

整理完毕,完结撒花~





参考地址:

1.Random在高并发下的缺陷以及JUC对其的优化 ,https://www.cnblogs.com/CodeBear/p/10748407.html

2.Java ThreadLocalRandom 伪随机数生成器的源码深度解析与应用,https://blog.csdn.net/weixin_43767015/article/details/108121269

3.Java中的随机数生成器:Random,ThreadLocalRandom,SecureRandom(转),https://www.cnblogs.com/softidea/p/4006518.html

4.并发情况下,你还在用Random生成随机数?

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

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

相关文章

2023最新谷粒商城笔记之MQ消息队列篇(全文总共13万字,超详细)

MQ消息队列 其实队列JDK中本身就有&#xff0c;不过这种队列也只能单体服务可能会使用&#xff0c;一旦项目使用的分布式架构&#xff0c;那么一定还是需要用到一个消息中间件的。我们引入消息队列的原因就是对我们的页面相应速度再优化&#xff0c;让用户的体验更好&#xff…

Ae:使用占位符

占位符 Placeholder本质上是一个静止的彩条图像&#xff0c;用来临时代替缺失的素材。自动占位符当 Ae 找不到源素材&#xff0c;比如被移动、删除或重命名&#xff0c;Ae 将自动生成占位符&#xff0c;在项目面板中用斜体显示&#xff0c;使用该素材的任何合成将用一个占位符图…

【R统计】R语言相关性分析及其可视化

&#x1f482; 个人信息&#xff1a;酷在前行&#x1f44d; 版权: 博文由【酷在前行】原创、需要转载请联系博主&#x1f440; 如果博文对您有帮助&#xff0c;欢迎点赞、关注、收藏 订阅专栏&#x1f516; 本文收录于【R统计】&#xff0c;该专栏主要介绍R语言实现统计分析的…

libxlsxwriter簇状柱形图绘制

libxlsxwriter的功能覆盖面很大&#xff0c;今天一起来看一下如何用这个库来生成带有簇状柱形图的表格。 1 簇形柱状图 首先来看一下Excel的样例表格&#xff0c;簇状柱形图往往是用来对比若干“系列”的数据在某一时间段内&#xff0c;或某一情境下的差异情况。在商务领域还…

小白量化《穿云箭集群量化》(4)指标公式写策略

小白量化《穿云箭集群量化》&#xff08;4&#xff09;指标公式写策略 穿云箭量化平台支持中文Python写量化策略&#xff0c;同时也直接支持股票公式指标写策略。下面我们看看是如何实现的。 股票软件的指标公式语法是一样的&#xff0c;不同仅仅是个别函数或绘图函数或绘图命令…

java多态理解和底层实现原理剖析

java多态理解和底层实现原理剖析多态怎么理解java中方法调用指令invokespecial和invokevirtual指令的区别invokeinterface指令方法表接口方法调用为什么不能利用方法表快速定位小结多态怎么理解 抽象事务的多种具体表现&#xff0c;称为事务的多态性。我们在编码过程中通常都是…

计算机网络 第4章 作业1

一、选择题 1. 由网络层负责差错控制与流量控制,使分组按序被递交的传输方式是_________&#xff08;C&#xff09; A&#xff0e;电路交换 B&#xff0e;报文交换 C&#xff0e;基于虚电路的分组交换 D&#xff0e;基于数据报的分组交换 2. TCP/IP 参考…

Bunifu.UI.WinForms 6.0.2 Crack

Bunifu.UI.WinForms为 WinForms创建令人惊叹的UI Bunifu.UI.WinForms我们为您提供了现代化的快速用户界面控件。用于 WinForms C# 和 VB.NET 应用程序开发的完美 UI 工具 简单 Bunifu.UI.WinForms没有臃肿的特征。正是您构建令人惊叹的 WinForms 应用程序所需要的。只需拖放然…

计算机网络高频知识点

目录 一、http状态码 二、强缓存与协商缓存 三、简单请求与复杂请求 四、PUT 请求类型 五、GET请求类型 六、GET 和 POST 的区别 七、跨域 1、什么时候会跨域 2、解决方式 八、计算机网络的七层协议与五层协议分别指的是什么 1、七层协议 2、五层协议 九、计算机网…

监控生产环境中的机器学习模型

简介 一旦您将机器学习模型部署到生产环境中&#xff0c;很快就会发现工作还没有结束。 在许多方面&#xff0c;旅程才刚刚开始。你怎么知道你的模型的行为是否符合你的预期&#xff1f;下周/月/年&#xff0c;当客户&#xff08;或欺诈者&#xff09;行为发生变化并且您的训练…

服务器部署—部署springboot之Linux服务器安装jdk和tomcat【建议收藏】

我是用的xshell连接的云服务器&#xff0c;今天想在服务器上面部署一个前后端分离【springbootvue】项目&#xff0c;打开我的云服务器才发现&#xff0c;过期了&#xff0c;然后又买了一个&#xff0c;里面环境啥都没有&#xff0c;正好出一期教程&#xff0c;方便大家也方便自…

大数据框架之Hadoop:MapReduce(三)MapReduce框架原理——ReduceTask工作机制

1、ReduceTask工作机制 ReduceTask工作机制&#xff0c;如下图所示。 &#xff08;1&#xff09;Copy阶段&#xff1a;ReduceTask从各个MapTask上远程拷贝一片数据&#xff0c;并针对某一片数据&#xff0c;如果其大小超过一定阈值&#xff0c;则写到磁盘上&#xff0c;否则直…

DHTMLX Suite 8.0.0 Crack

适用于现代 Web 应用程序的强大 JavaScript 小部件库 - DHTMLX 套件 用于创建现代用户界面的轻量级、快速且通用的 JavaScript/HTML5 UI 小部件库。 DHTMLX Suite 有助于推进 Web 开发和构建具有丰富功能的数据密集型应用程序。 DHTMLX Suite 是一个 UI 小部件库&#xff0c;用…

指针数组和数组指针的区别

数组指针&#xff08;也称行指针&#xff09;定义 int (*p)[n];()优先级高&#xff0c;首先说明p是一个指针&#xff0c;指向一个整型的一维数组&#xff0c;这个一维数组的长度是n&#xff0c;也可以说是p的步长。也就是说执行p1时&#xff0c;p要跨过n个整型数据的长度。如要…

【前端】JavaScript构造函数

文章目录概念执行过程返回值原型与constructor继承方式原型链其他继承方式&#xff08;还没写&#xff09;参考概念 在JS中&#xff0c;通过new来实例化对象的函数叫构造函数。实例化对象&#xff0c;也就是初始化一个实例对象。构造函数一般首字母大写。 构造函数的目的&…

Android性能调优 - 启动优化

一、APP启动优化1、 你对 APP 的启动有过研究吗? 有做过相关的启动优化吗?程序员&#xff1a;之前做项目的时候&#xff0c;我发现程序在冷启动时&#xff0c;会有 1s 左右的白屏闪现&#xff0c;低版本是黑屏的现象&#xff0c;在这期间我通过翻阅系统主题源码&#xff0c;发…

26 openEuler管理网络-使用ip命令配置网络

文章目录26 openEuler管理网络-使用ip命令配置网络26.1 配置IP地址26.1.1 配置静态地址26.1.2 配置多个地址26.2 配置静态路由26 openEuler管理网络-使用ip命令配置网络 说明&#xff1a; 使用ip命令配置的网络配置可以立即生效但系统重启后配置会丢失。 26.1 配置IP地址 使用…

JVM - G1垃圾收集器深入剖析

​​​​​​​1、G1收集器概述 HotSpot团队一直努力朝着高效收集、减少停顿(STW: Stop The World)的方向努力&#xff0c;也贡献了从串行Serial收集器、到并行收集器Parallerl收集器&#xff0c;再到CMS并发收集器&#xff0c;乃至如今的G1在内的一系列优秀的垃圾收集器。 G…

ER图、ERD图

ER图、ERD图1. 什么是ERD1.1 举例2. ERD符号指南2.1 实体2.2 属性2.3 主键2.4 外键2.4 关系2.5 基数2.5.1 一对一的基数的例子2.5.2 一对多的基数的例子2.5.3 多对多的基数的例子3.概念、逻辑和物理数据模型3.1 概念数据模型3.2 逻辑数据模型3.3 物理数据模型4.如何绘制ER图?5…

python的装饰器与设计模式中的装饰器模式

相信很多人在初次接触python中的装饰器时&#xff0c;会跟我一样有个疑问&#xff0c;这跟设计模式中的装饰器模式有什么区别吗&#xff1f;本质上是一样的&#xff0c;都是对现有对象&#xff0c;包括函数或者类的一种扩展。这篇文档将进行对比分析。 python的装饰器 装饰器…