二、并发编程的三大特性

news2024/11/28 10:42:53

文章目录

  • 并发编程的三大特性
  • 1、原子性
    • 什么是并发编程的原子性?
    • 保证并发编程的原子性
      • synchronized
      • CAS
      • Lock锁
      • ThreadLocal
  • 2、可见性
    • 什么是可见性?
    • 解决可见性的方式
      • volatile
      • synchronized
      • Lock
      • final
  • 3、有序性
      • 什么是有序性?
      • as-if-serial
      • happens-before
      • volatile

并发编程的三大特性

原子性、可见性、有序性。

1、原子性

什么是并发编程的原子性?

JMM(Java Memory Model)。不同的硬件和不同的操作系统在内存上的操作有一定差异的。 Java为了解决相同代码在不同操作系统上出现的各种问题,用JMM屏蔽掉各种硬件和操作系统带来的差异。
让Java的并发编程可以做到跨平台。 JMM规定所有变量都会存储在主内存中,在操作的时候,需要从主内存中复制一份到线程内存(CPU内存),在线程内部做计算。然后再写回主内存中(不一定)
原子性的定义:原子性指一个操作是不可分割的,不可中断的,一个线程在执行时,另一个线程不会影响到他。
并发编程的原子性用代码阐述:

    private static int count;

    public static void increment() {
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        count++;
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                increment();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                increment();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }

当前程序:多线程操作共享数据时,预期的结果,与最终的结果不符。
原子性:多线程操作临界资源,预期的结果与最终结果一致。
通过对这个程序的分析,可以查看出,++的操作,一共分为了三部,首先是线程从主内存拿到数据,保存到CPU的寄存器中,然后在寄存器中进行+1操作,最终将结果写回到主内存当中。

保证并发编程的原子性

synchronized

因为++操作可以从指令中查看到
i++操作可以在方法上追加synchronized关键字或者采用同步代码块的形式来保证原子性。
synchronized可以让避免多线程同时操作临街资源,同一时间点,只会有一个线程正在操作临界资源。
在这里插入图片描述

CAS

compare and swap也就是比较和交换,他是一条CPU的并发原语。
他在替换内存的某个位置的值时,首先查看内存中的值与预期值是否一致,如果一致,执行替换操作。这个操作是一个原子性操作。
Java中基于Unsafe的类提供了对CAS的操作的方法,JVM会帮助我们将方法实现CAS汇编指令。 但是要清楚CAS只是比较和交换,在获取原值的这个操作上,需要你自己实现。

    private static AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                count.incrementAndGet();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                count.incrementAndGet();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }

Doug Lea在CAS的基础上帮助我们实现了一些原子类,其中就包括现在看到的AtomicInteger,还有其他很多原子类。
CAS的缺点:CAS只能保证对一个变量的操作是原子性的,无法实现对多行代码实现原子性。

CAS带来的问题

  • ABA问题:问题如下,可以引入版本号的方式,来解决ABA的问题。Java中提供了一个类在CAS时,针对各个版本追加版本号的操作。 AtomicStampeReference在CAS时,不但会判断原值,还会比较版本信息。
        public static void main(String[] args) {
            AtomicStampedReference<String> reference = new AtomicStampedReference<>("AAA", 1);
            String oldValue = reference.getReference();
            int oldVersion = reference.getStamp();
            boolean b = reference.compareAndSet(oldValue, "B", oldVersion, oldVersion + 1);
            System.out.println("修改1版本的:" + b);
            boolean c = reference.compareAndSet("B", "C", 1, 1 + 1);
            System.out.println("修改2版本的:" + c);
        }
    
  • 自旋时间过长问题
    • 可以指定CAS一共循环多少次,如果超过这个次数,直接失败/或者挂起线程。(自旋锁、 自适应自旋锁)
    • 可以在CAS一次失败后,将这个操作暂存起来,后面需要获取结果时,将暂存的操作全部执行,再返回最后的结果。

ABA问题:在这里插入图片描述

  • 线程1从内存位置V中取出A
  • 线程2从内存位置V中取出A
  • 线程2进行了写操作,将B写入内存位置V
  • 线程2将A再次写入内存位置V
  • 线程1进行CAS操作,发现V中仍然是A,交换成功
    尽管线程1的CAS操作成功,但线程1并不知道内存位置V的数据发生过改变。

Lock锁

Lock锁是在JDK1.5由Doug Lea研发的,他的性能相比synchronized在JDK1.5的时期,性能好了很很多,但是在JDK1.6对synchronized优化之后,性能相差不大,但是如果涉及并发比较多时,推荐ReentrantLock锁,性能会更好。

	private static int count;
    private static ReentrantLock lock = new ReentrantLock();

    public static void increment() {
        lock.lock();
        try {
            count++;
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                increment();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                increment();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }

ReentrantLock可以直接对比synchronized,在功能上来说,都是锁。但是ReentrantLock的功能性相比synchronized更丰富。
ReentrantLock底层是基于AQS实现的,有一个基于CAS维护的state变量来实现锁的操作。

ThreadLocal

Java中的四种引用类型分别是强软弱虚
User user = new User();
在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它始终处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到JVM也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之一。
SoftReference
其次是软引用,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中,作为缓存使用。
然后是弱引用,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。可以解决内存泄漏问题,ThreadLocal就是基于弱引用解决内存泄漏的问题。
最后是虚引用,它不能单独使用,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态。不过在开发中,我们用的更多的还是强引用。

ThreadLocal保证原子性的方式,是不让多线程去操作临界资源,让每个线程去操作属于自己的数据。

    static ThreadLocal tl1 = new ThreadLocal();
    static ThreadLocal tl2 = new ThreadLocal();

    public static void main(String[] args) {
        tl1.set("123");
        tl2.set("456");
        Thread t1 = new Thread(() -> {
            System.out.println("t1:" + tl1.get());
            System.out.println("t1:" + tl2.get());
        });
        t1.start();
        System.out.println("main:" + tl1.get());
        System.out.println("main:" + tl2.get());
    }

ThreadLocal实现原理:

  • 每个Thread中都存储着一个成员变量,ThreadLocalMap。
  • ThreadLocal本身不存储数据,像是一个工具类,基于ThreadLocal去操作ThreadLocalMap。
  • ThreadLocalMap本身就是基于Entry[]实现的,因为一个线程可以绑定多个ThreadLocal,这样一来,可能需要存储多个数据,所以采用Entry[]的形式实现。
  • 每一个现有都自己独立的ThreadLocalMap,再基于ThreadLocal对象本身作为key,对value进行存取ThreadLocalMap的key是一个弱引用,弱引用的特点是,即便有弱引用,在GC时,也必须被回收。这里是为了在ThreadLocal对象失去引用后,如果key的引用是强引用,会导致 ThreadLocal对象无法被回收。

ThreadLocal内存泄漏问题:

  • 如果ThreadLocal引用丢失,key因为弱引用会被GC回收掉,如果同时线程还没有被回收,就会导致内存泄漏,内* * 存中的value无法被回收,同时也无法被获取到。
    只需要在使用完毕ThreadLocal对象之后,及时的调用remove方法,移除Entry即可。
    ThreadLocal

2、可见性

什么是可见性?

可见性问题是基于CPU位置出现的,CPU处理速度非常快,相对CPU来说,去主内存获取数据这个 事情太慢了,CPU就提供了L1,L2,L3的三级缓存,每次去主内存拿完数据后,就会存储到CPU的 三级缓存,每次去三级缓存拿数据,效率肯定会提升。

这就带来了问题,现在CPU都是多核,每个线程的工作内存(CPU三级缓存)都是独立的,会告知每个线程中做修改时,只改自己的工作内存,没有及时的同步到主内存,导致数据不一致问题。
MESI
可见性问题的代码逻辑:

    private static boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while (flag) {
                // ....
            }
            System.out.println("t1线程结束");
        });
        t1.start();
        Thread.sleep(10);
        flag = false;
        System.out.println("主线程将flag改为false");
    }

解决可见性的方式

volatile

volatile是一个关键字,用来修饰成员变量。

如果属性被volatile修饰,相当于会告诉CPU,对当前属性的操作,不允许使用CPU的缓存,必须去 和主内存操作。

volatile的内存语义:

  • volatile属性被写:当写一个volatile变量,JMM会将当前线程对应的CPU缓存及时的刷新到主内存中。
  • volatile属性被读:当读一个volatile变量,JMM会将对应的CPU缓存中的内存设置为无效,必须去主内存中重新读取共享变量。

其实加了volatile就是告知CPU,对当前属性的读写操作,不允许使用CPU缓存,加了volatile修饰的 属性,会在转为汇编之后,追加一个lock的前缀,CPU执行这个指令时,如果带有lock前缀会做两个事情:

  • 将当前处理器缓存行的数据写回到主内存。
  • 这个写回的数据,在其他的CPU内核的缓存中,直接无效。
    总结:volatile就是让CPU每次操作这个数据时,必须立即同步到主内存,以及从主内存读取数据。
    private volatile static boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while (flag) {
                // ....
            }
            System.out.println("t1线程结束");
        });
        t1.start();
        Thread.sleep(10);
        flag = false;
        System.out.println("主线程将flag改为false");
    }

synchronized

synchronized也是可以解决可见性问题的,synchronized的内存语义。
如果涉及到了synchronized的同步代码块或者是同步方法,获取锁资源之后,将内部涉及到的变量从CPU缓存中移除,必须去主内存中重新拿数据,而且在释放锁之后,会立即将CPU缓存中的数据同步到主内存。

    private static boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while (flag) {
                synchronized (MiTest.class) {
                    //...
                }
                System.out.println(111);
            }
            System.out.println("t1线程结束");
        });
        t1.start();
        Thread.sleep(10);
        flag = false;
        System.out.println("主线程将flag改为false");
    }

Lock

Lock锁保证可见性的方式和synchronized完全不同,synchronized基于他的内存语义,在获取锁和释放锁时,对CPU缓存做一个同步到主内存的操作。
Lock锁是基于volatile实现的。Lock锁内部再进行加锁和释放锁时,会对一个由volatile修饰的state属性进行加减操作。
如果对volatile修饰的属性进行写操作,CPU会执行带有lock前缀的指令,CPU会将修改的数据,从 CPU缓存立即同步到主内存,同时也会将其他的属性也立即同步到主内存中。还会将其他CPU缓存 行中的这个数据设置为无效,必须重新从主内存中拉取。

    private static boolean flag = true;
    private static Lock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while (flag) {
                lock.lock();
                try {
                    //...
                } finally {
                    lock.unlock();
                }
            }
            System.out.println("t1线程结束");
        });
        t1.start();
        Thread.sleep(10);
        flag = false;
        System.out.println("主线程将flag改为false");
    }

final

final修饰的属性,在运行期间是不允许修改的,这样一来,就间接的保证了可见性,所有多线程读 取final属性,值肯定是一样。
final并不是说每次取数据从主内存读取,他没有这个必要,而且final和volatile是不允许同时修饰一个属性的。
final修饰的内容已经不允许再次被写了,而volatile是保证每次读写数据去主内存读取,并且volatile 会影响一定的性能,就不需要同时修饰。
在这里插入图片描述

3、有序性

什么是有序性?

在Java中,.java文件中的内容会被编译,在执行前需要再次转为CPU可以识别的指令,CPU在执行 这些指令时,为了提升执行效率,在不影响最终结果的前提下(满足一些要求),会对指令进行重排。
指令乱序执行的原因,是为了尽可能的发挥CPU的性能。
Java中的程序是乱序执行的。

    static int a, b, x, y;

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            a = 0;
            b = 0;
            x = 0;
            y = 0;
            Thread t1 = new Thread(() -> {
                a = 1;
                x = b;
            });
            Thread t2 = new Thread(() -> {
                b = 1;
                y = a;
            });
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            if (x == 0 && y == 0) {
                System.out.println("第" + i + "次,x = " + x + ",y = " + y);
            }
        }
    }

单例模式由于指令重排序可能会出现问题:
线程可能会拿到没有初始化的对象,导致在使用时,可能由于内部属性为默认值,导致出现一些不必
要的问题。

    private static volatile MiTest test;
    private MiTest() {
    }
    public static MiTest getInstance() { // B
        if (test == null) {
            synchronized (MiTest.class) {
                if (test == null) {
                    // A , 开辟空间,test指向地址,初始化
                    test = new MiTest();
                }
            }
        }
        return test;
    }

as-if-serial

as-if-serial语义: 不论指定如何重排序,需要保证单线程的程序执行结果是不变的。 而且如果存在依赖的关系,那么也不可以做指令重排。

// 这种情况肯定不能做指令重排序 int i = 0;
i++;
// 这种情况肯定不能做指令重排序 int j = 200;
j * 100;
j + 100;
// 这里即便出现了指令重排,也不可以影响最终的结果,20100

happens-before

具体规则:

  1. 单线程happen-before原则:在同一个线程中,书写在前面的操作happen-before后面的操作。
  2. 锁的happen-before原则:同一个锁的unlock操作happen-before此锁的lock操作。
  3. volatile的happen-before原则:对一个volatile变量的写操作happen-before对此变量的任意操作。
  4. happen-before的传递性原则:如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。
  5. 线程启动的happen-before原则:同一个线程的start方法happen-before此线程的其它方法。
  6. 线程中断的happen-before原则:对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。
  7. 线程终结的happen-before原则:线程中的所有操作都happen-before线程的终止检测。
  8. 对象创建的happen-before原则:一个对象的初始化完成先于他的finalize方法调用。

JMM只有在不出现上述8中情况时,才不会触发指令重排效果。不需要过分的关注happens-before原则,只需要可以写出线程安全的代码就可以了。

volatile

如果需要让程序对某一个属性的操作不出现指令重排,除了满足happens-before原则之外,还可以基于volatile修饰属性,从而对这个属性的操作,就不会出现指令重排的问题了。

volatile如何实现的禁止指令重排?
内存屏障概念。将内存屏障看成一条指令。 会在两个操作之间,添加上一道指令,这个指令就可以避免上下执行的其他指令进行重排序。

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

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

相关文章

谷歌seo新站如何快速排名?如何提高Google自然排名

本文主要分享谷歌SEO如何做新站排名&#xff0c;很多刚出海的外贸小伙伴不会做谷歌SEO&#xff0c;快来学习。 本文由光算创作&#xff0c;有可能会被剽窃和修改&#xff0c;我们佛系对待这种行为吧。 谷歌seo新站如何快速排名&#xff1f; 答案是&#xff1a;大量优质原创内…

科技新浪推前浪 ChatGPT将元宇宙“拍在沙滩上”?

近期ChatGPT的热度显然已经盖过了元宇宙&#xff0c;回想去年元宇宙大热之际&#xff0c;很多企业纷纷跟进&#xff0c;甚至还有不少公司选择更名以表达All In元宇宙的决心。而如今ChatGPT抢占风头&#xff0c;成为新宠&#xff0c;元宇宙似乎被“抛弃”了&#xff0c;难道元宇…

VCL界面组件DevExpress VCL v22.2 - 拥有全新的矢量图形

DevExpress VCL是Devexpress公司旗下最老牌的用户界面套包&#xff0c;所包含的控件有&#xff1a;数据录入、图表、数据分析、导航、布局等。该控件能帮助您创建优异的用户体验&#xff0c;提供高影响力的业务解决方案&#xff0c;并利用您现有的VCL技能为未来构建下一代应用程…

python网络编程详解

最近在看《UNIX网络编程 卷1》和《FREEBSD操作系统设计与实现》这两本书&#xff0c;我重点关注了TCP协议相关的内容&#xff0c;结合自己后台开发的经验&#xff0c;写下这篇文章&#xff0c;一方面是为了帮助有需要的人&#xff0c;更重要的是方便自己整理思路&#xff0c;加…

ElasticSearch Script 操作数据最详细介绍

文章目录ElasticSearch Script基础介绍基础用法List类型数据新增、删除nested数据新增、删除根据指定条件修改数据根据指定条件修改多个字段数据-查询条件也使用脚本根据指定条件删除nested中子数据数据根据条件删除数据删除之后结果创建脚本&#xff0c;通过脚本调用根据条件查…

.net7窗口编程c#2022实战(1)-zip压缩精灵(1)

目录 创建ZIP精灵项目拖控件OpenFileDialog 类压缩与解压缩编写我们自己的代码其它参考内容创建ZIP精灵项目 VS2022中新建项目。 为窗体取一个标题名称 拖控件 左边工具栏里选择控件 拖三个按钮控件和一个listbox控件

动态规划问题汇总(一)

基本步骤 文章目录基本步骤509. 斐波那契数70. 爬楼梯746. 使用最小花费爬楼梯62.不同路径63. 不同路径 II343. 整数拆分96.不同的二叉搜索树509. 斐波那契数 递归版本 class Solution {public int fib(int n) {if(n0){return 0;}if(n1){return 1;}return fib(n-1)fib(n-2);} …

【华为OD机试模拟题】用 C++ 实现 - 求字符串中所有整数的最小和

最近更新的博客 华为OD机试 - 入栈出栈(C++) | 附带编码思路 【2023】 华为OD机试 - 箱子之形摆放(C++) | 附带编码思路 【2023】 华为OD机试 - 简易内存池 2(C++) | 附带编码思路 【2023】 华为OD机试 - 第 N 个排列(C++) | 附带编码思路 【2023】 华为OD机试 - 考古…

2023年2月22日 [随记] 理想、面包

一些简单的吐槽&#xff0c;可以当个故事看一下。 文章目录简单的经历书籍清单这些是买了看过的买了没有仔细看的眨眼间也从业2年11个月多一点&#xff08;就当是三年了&#xff09;&#xff0c;在2023年1月初&#xff0c;距离过年还有两周的时间&#xff0c;因为一些个人原因裸…

拓扑排序的思想?用代码怎么实现

目录 一、拓扑排序的思想 二、代码实现&#xff08;C&#xff09; 代码思想 核心代码 完整代码 一、拓扑排序的思想 以西红柿炒鸡蛋这道菜为例&#xff0c;其中的做饭流程为&#xff1a; 中间2 6 3 7 4的顺序都可以任意调换&#xff0c;但1和5必须在最前面&#xff0c;这是…

详细介绍React生命周期和diffing算法

事件处理 1.通过onXxx属性指定事件处理函数(注意大小写) React使用的是自定义(合成)事件, 而不是使用的原生DOM事件 —— 为了更好的兼容性&#xff1b;React中的事件是通过事件委托方式处理的(委托给组件最外层的元素) ——为了的高效。 2.通过event.target得到发生事件的DOM…

数据挖掘,计算机网络、操作系统刷题笔记54

数据挖掘&#xff0c;计算机网络、操作系统刷题笔记54 2022找工作是学历、能力和运气的超强结合体&#xff0c;遇到寒冬&#xff0c;大厂不招人&#xff0c;可能很多算法学生都得去找开发&#xff0c;测开 测开的话&#xff0c;你就得学数据库&#xff0c;sql&#xff0c;orac…

搭建kafka集群

Kafka集群依赖ZK&#xff0c;需要先启动ZK集群 机器&#xff1a;hadop101,hadoop102, hadoop103 【1】在hadoop101解压&#xff1a; tar -zxvf kafka_2.12-2.4.1.tgz -C ../module/ 【2】在hadoop101修改server.properties配置&#xff1a; #指定broker的id&#xff0c;类似zk…

亚马逊云科技SageMaker:实现自动、可视化管理迭代

现如今&#xff0c;AI正在成为跨时代的技术&#xff0c;在数字经济发展中登上舞台&#xff0c;发挥关键作用。在Gartner发布的《2022年新兴技术成熟度曲线》*报告中&#xff0c;AIGC&#xff08;即AI Generated Content&#xff0c;人工智能自动生成内容&#xff09;被列为2022…

微搭使用笔记(四) 通过循环展示组件+json配置生成表单及数据获取

背景及整体思路 上篇文章我们通过微搭提供的数据模型完成了问卷表单页面的创建和数据采集&#xff0c;相对来说除了数据模型配置略显复杂外其他的倒还算方便。 本文我们通过for循环加上json文件配置的方式实现一个通用表单页面&#xff0c;如果更换了表单只需要替换掉json配置…

stm32 VM8978 音乐播放

一、WAV文件 1、WAV文件简介 2、WAV文件的解析 二、WM8978 1、WM8978介绍 2、WM8978特点 3、WM8978接口 4、WM8978框架 5、 WM8978 寄存器 三、IIS详解 1、IIS介绍 2、 IIS 的特点 3、IIS框架 4、 音频协议 5、 IIS Philips 标准 6、 IIS 时钟 四、音乐播放硬件…

力扣-删除重复的电子邮箱

大家好&#xff0c;我是空空star&#xff0c;本篇带大家了解一道简单的力扣sql练习题。 文章目录前言一、题目&#xff1a;196. 删除重复的电子邮箱二、解题1.正确示范①提交SQL运行结果2.正确示范②提交SQL运行结果3.正确示范③提交SQL运行结果4.正确示范④提交SQL运行结果5.其…

2.22Linux系统搭建

一.搭建Linux运行环境需要部署Java程序到服务器上,这样程序才能被外面的用户访问到1.安装jdkyum install develop x86_642.安装tomcat1)下载好,通过xshell直接拖到服务器上,依赖了rz命令2)解压缩unzip命令3)使.sh都有可执行权限chmod x *.sh4)启动 sh startup.sh5) 验证 ①ps a…

MAC配置pycharm

Mac配置pycharm 恢复出厂配置 # configuration rm -rf ~/library/preferences/JetBrains/ # caches rm -rf ~/library/caches/JetBrains/ # plugins rm -rf ~/library/application support/JetBrains/ # logs rm -rf ~/library/logs/JetBrains/文件头部 Python #!/usr/bin/e…

Mind+Python+Mediapipe项目——AI健身之跳绳

原文&#xff1a;MindPythonMediapipe项目——AI健身之跳绳 - DF创客社区 - 分享创造的喜悦 【项目背景】跳绳是一个很好的健身项目&#xff0c;为了获知所跳个数&#xff0c;有的跳绳上会有计数器。但这也只能跳完这后看到&#xff0c;能不能在跳的过程中就能看到&#xff0c;…