25. 悲观锁 和 乐观锁

news2024/11/18 19:55:22

文章目录

  • 悲观锁 和 乐观锁
    • 1.基于CAS实现乐观锁
    • 2.自旋锁
      • 2.1.不可重入自旋锁
      • 2.2.可重入自旋锁
      • 2.3.CLH自旋锁

悲观锁 和 乐观锁

Java中的synchronized就是悲观锁的一个实现,悲观锁可以确保无论哪个线程持有锁,都能独占式的访问临界区代码,虽然悲观锁的实现比较简单,但是还是会存在不少问题。

悲观锁总是假设会发生最坏的情况,每次线程去读取数据的时候,也会上锁。这样其他线程在读取数据的时候也会被阻塞,直到它拿到锁,传统的关系型数据库就用到了很多的悲观锁,如行锁表锁读锁写锁

悲观锁会存在以下几个问题:

  1. 在多线程环境下, 加锁和释放锁都会导致线程上下文的切换以及调度延时,会引发一系列性能问题。
  2. 一个线程持有锁的时候,会导致其他抢锁线程都被临时挂起
  3. 如果一个线程优先级高的线程等待一个优先级低的线程释放锁,就会导致线程优先级倒置,从而引发性能风险。

解决以上悲观锁的方式,就是使用乐观锁去替代 悲观锁。乐观锁其实时一种思想,在使用乐观锁的时候,每次线程都去读取的数据的时候都认为其他线程不会进行修改,所以不会上锁,仅仅在更新的时候判断一下其他线程有没有去更新这个数据

数据库操作中的带版本号的数据更新,JUC原子类中都使用了乐观锁的方式来提高性能。

这里针对于悲观锁,我们就就不在举例说明了,感兴趣的话可以去学习一下看一下之前的synchronized章节。

1.基于CAS实现乐观锁

乐观锁的实现步骤主要就两个

(1)冲突监测

(2)数据更新

乐观锁时一种比较典型的CAS原子操作,JUC强大的高并发性能就是建立在CAS原子操作上的,CAS操作中包含三个操作数

(1)需要操作的内存位置(V)

(2)进行比较的预期原值(A)

(3)拟写入的新值(B)

如果内存 V的位置的值 预期原值 A 比较一致,那么CPU会自动将该位置的值替换为新的值 B否则CPU不做任何操作

下面我们通过一个案例来了解一下CAS中的乐观锁,在Java中,乐观锁的一种常见实现方式是借助于java.util.concurrent.atomic包下的原子类,比如AtomicInteger。下面我们通过一个简单的银行账户转账的例子来展示如何使用CAS(Compare-And-Swap)操作实现乐观锁。

/**
 * CAS乐观锁的一个实现
 */
public class OptimismLockDemo {

    private final Logger logger = LoggerFactory.getLogger(OptimismLockDemo.class);


    @Test
    @DisplayName("测试JUC的CAS操作")
    public void testOptimismLock() {
        // 初始余额设置100
        Bank bank = new Bank(100);

        // 线程1存钱 100
        Thread t1 = new Thread(() -> {
            if (bank.updateCount(100)) {
                logger.error("存钱100成功!");
            } else {
                logger.error("取钱成功!");
            }
        }, "t1");

        // 线程2 取钱 100
        Thread t2 = new Thread(() -> {
            if (bank.updateCount(-100)) {
                logger.error("取钱100成功!");
            } else {
                logger.error("取钱失败!");
            }

        }, "t2");


        // 启动t1 和 t2 线程
        t1.start();
        t2.start();

        // 等待全部线程执行完毕
        try {
            t2.join();
            t1.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }


    }


    static class Bank {
        // 定义余额
        private AtomicInteger balance;

        Bank(int initBalance) {
            this.balance = new AtomicInteger(initBalance);
        }


        public boolean updateCount(int money) {
            // 获取当前余额
            int currentMoney = balance.get();


            // 计算出预期的结果
            int newMoney = currentMoney + money;

            // 如果是转出且余额不足,直接返回false,否则尝试更新余额
            if (money < 0 && newMoney < 0) {
                return false;
            }
            // 尝试更新余额,这里就是CAS操作的核心
            // compareAndSet比较并交换,如果当前余额仍为currentMoney 则更新为newMoney,否则就告知更新失败
            return balance.compareAndSet(currentMoney, newMoney);
        }
    }
}

在这个例子中,我们创建了一个初始余额为100的账户,然后启动了两个线程分别执行转出100元和转入100元的操作。由于转账方法基于CAS实现,因此在并发环境下能够确保转账的原子性和正确性,避免了传统锁机制可能导致的性能瓶颈。

在这里插入图片描述

在这里插入图片描述

2.自旋锁

在实际情况中,一个成功的数据更新操作可能需要多次执行CAS(比较并交换)操作,这就是所谓的CAS自旋。通过反复尝试,直至更新成功,这样的机制无需锁定资源,实现了多线程环境下变量状态的高效协同,我们称之为“无锁同步”或“非阻塞同步”。这种方式,正是乐观锁的核心思想之一,它体现了在并发编程领域追求高性能与低冲突的“乐观”策略。

2.1.不可重入自旋锁

自旋锁(SpinLock)的基本含义就是:当一个线程在获取锁 的时候,如果锁已经被其他线程获取,那么该调用线程就一致那里循环监测锁是否已经被释放,一直到获取那个锁之后才会退出循环。

package com.hrfan.thread.lock.type.spin;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
 * 自旋锁
 * 不可重入锁
 */
public class SpinLockDemo {


    private static final Logger log = LoggerFactory.getLogger(SpinLockDemo.class);

    @Test
    @DisplayName("测试不可重入自旋锁")
    public void testSpinLock() {
        SpinLock spinLock = new SpinLock();
        // 这里为了模拟多线程的一个并发性能 我们使用线程池来进行测试
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 3; i++) {
            executorService.submit(() -> {
                spinLock.lock();
                try {
                    // 模拟执行操作
                    log.error("抢锁成功!执行操作");
                } finally {
                    spinLock.unlock();

                }
            });
        }
        executorService.shutdown();


    }


    static class SpinLock implements Lock {

        /**
         * 当前锁的拥有者
         */
        private AtomicReference<Thread> owner = new AtomicReference<>();


        @Override
        public void lock() {
            // 抢占锁
            // 获取当前线程
            Thread thread = Thread.currentThread();
            // 开始抢占锁 (不断cas 直到owner的值为null 才更新为当前线程)
            while (!owner.compareAndSet(null, thread)) {
                log.error("抢锁失败!让出剩余CPU时间片!");
                // 如果抢锁失败(即当前锁已被其他线程持有),则让出CPU时间片给其他线程,稍后再试
                Thread.yield();
            }
        }

        @Override
        public void unlock() {
            // 通过代码 我们可以发现,SpinLock是不支持重入的,在一个线程获取锁没有释放之前,它不可能再次获得锁。
            // 释放锁
            Thread thread = Thread.currentThread();
            // 只有拥有者才能够释放锁
            if (thread == owner.get()) {
                log.error("释放锁成功!");
                // 这里设置为拥有者为空,不需要在使用compareAndSet() 因为上面已经判断
                owner.set(null);
            }
        }

        @Override
        public void lockInterruptibly() throws InterruptedException {

        }

        @Override
        public boolean tryLock() {
            return false;
        }

        @Override
        public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
            return false;
        }


        @Override
        public Condition newCondition() {
            return null;
        }
    }
}

在这里插入图片描述

2.2.可重入自旋锁

为了实现可重入自旋锁,这里引入一个计数器,用来记录一个线程获取锁的次数。

package com.hrfan.thread.lock.type.spin;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
 * 可重入自旋锁
 */
public class ReentrantSpinLockDemo {
    ReentrantSpinLock lock = new ReentrantSpinLock();

    @Test
    @DisplayName("测试可重入锁")
    public void test() throws InterruptedException {


        new Thread(() -> {
            lock.lock();
            try {
                log.error("开始执行任务! 重入次数{}", lock.getCount());
                reentrantSpinLock();
            } finally {
                lock.unlock();
            }
        }, "t1").start();
        Thread.sleep(1000);
    }

    public void reentrantSpinLock() {
        lock.lock();
        try {
            log.error("开始执行重入方法! 重入次数{}", lock.getCount());
        } finally {
            lock.unlock();
        }
    }


    private static final Logger log = LoggerFactory.getLogger(ReentrantSpinLockDemo.class);

    static class ReentrantSpinLock implements Lock {

        /**
         * 当前锁的拥有者
         * 使用拥有者的Thread作为同步状态,而不是使用一个简单的整数作为同步状态
         */
        private AtomicReference<Thread> owner = new AtomicReference<>();


        /**
         * 记录一个线程同步获取锁的状态
         */
        private int count = 0;

        @Override
        public void lock() {
            // 抢占锁
            Thread thread = Thread.currentThread();
            // 如果是重入 增加重入次数返回
            if (thread == owner.get()) {
                count++;
                return;
            }

            // 如果不是重入 那么进行自旋操作
            while (!owner.compareAndSet(null, thread)) {
                log.error("抢锁失败!让出剩余CPU时间片!");
                // 如果抢锁失败(即当前锁已被其他线程持有),则让出CPU时间片给其他线程,稍后再试
                Thread.yield();
            }
        }


        @Override
        public void unlock() {
            // 只有拥有者才能释放锁
            Thread thread = Thread.currentThread();
            if (thread == owner.get()) {
                // 如果发现count的次数不是 0减少重入次数 并返回
                if (count > 0) {
                    count--;
                } else {
                    // 直接将拥有者设置为空
                    owner.set(null);
                }
            }
        }

        @Override
        public void lockInterruptibly() throws InterruptedException {

        }

        @Override
        public boolean tryLock() {
            return false;
        }

        @Override
        public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
            return false;
        }


        @Override
        public Condition newCondition() {
            return null;
        }

        public int getCount() {
            return count;
        }
    }
}

在这里插入图片描述

自旋锁的特点:线程在获取锁的时候,如果锁被其他线程持有,当前线程循环等待,直到获取锁。线程抢锁期间线程的状态不会改变,一直时运行状态,在操作系统层面线程处于用户态。

自旋锁的问题:在争用激烈的场景下,如果某个线程持有的锁的时间太长,就会导致其他自旋线程的CPU资源耗尽,另外,如果大量的线程进行空自旋,还可能导致硬件层面的总线风暴

2.3.CLH自旋锁

前面提到了CAS自旋可能引发的一些性能问题,尤其是它可能导致CPU层面的总线争用(总线风暴)。面对这一挑战,Java并发包(JUC)中的轻量级锁如何有效规避这一问题呢?其解决方案在于利用队列机制对试图获取锁的线程进行有序排列,从而大幅度减少了CAS操作的频次,从根本上减轻了对CPU和总线的压力。

具体而言,CLH锁作为一种典型的基于队列(常采用单向链表形式)实现的自旋锁机制,为这一问题提供了高效的解答。在CLH锁的模型中,任何一个请求加锁的线程首先会尝试通过CAS操作将自己的信息节点添加到队列的末端。一旦成功入队,该线程随后仅需在其直接前驱节点上执行相对简单的自旋等待操作,直至前驱释放锁资源。

这一设计精妙之处在于,CLH锁仅在节点初次尝试加入队列时涉及一次CAS操作。一旦入队完成,后续的自旋过程无需进一步的CAS介入,仅需执行标准的自旋逻辑即可。因此,在高度竞争的并发场景下,CLH锁能够显著削减CAS操作的总量,有效避开了可能引发的总线风暴现象。

在Java并发工具包(JUC)中,显式锁的实现底层依赖于AbstractQueuedSynchronizer(AQS),这一框架实质上是对CLH锁原理的一种扩展与变体应用,进一步强化了锁的管理和线程调度能力,展现了高级的并发控制策略。

上面提到的简单自旋 其实就是

普通自旋是指线程在未能立即获取锁时,不进行复杂的操作如CAS(比较并交换)而是执行简单的循环检查某个条件是否满足(比如前驱节点是否已经释放了锁)。

下面我们通过一个案例来学习一下CLH自旋锁

package com.hrfan.thread.lock.type.spin;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.sound.sampled.FloatControl;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * CLH版本自旋锁
 */
public class CLHSpinLockDemo {
    private static final Logger log = LoggerFactory.getLogger(CLHSpinLockDemo.class);
    public static int count = 0;

    @Test
    @DisplayName("测试CLH自旋锁")
    public void test() {
        long startTime = System.currentTimeMillis();
        // 创建CLH自旋锁
        CLHSpinLock lock = new CLHSpinLock();
        // ReentrantLock lock = new ReentrantLock();
        // 线程数量
        int threads = 10;
        // 每次执行的次数
        int turns = 10000;
        // 通过线程池来创建线程
        ExecutorService executorService = Executors.newFixedThreadPool(threads);
        // 创建计时器
        CountDownLatch latch = new CountDownLatch(threads);
        for (int i = 0; i < threads; i++) {
            executorService.submit(() -> {
                for (int j = 0; j < turns; j++) {
                    // 创建锁
                    lock.lock();
                    try {
                        count++;
                    } finally {
                        lock.unlock();
                    }
                }
                // 更新计时器
                latch.countDown();
            });
        }
        // 等待全部线程执行完毕
        try {
            latch.await();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        long endTime = System.currentTimeMillis();
        log.error("-------------线程执行结束,最终结果为:{}", count);
        log.error("耗时:{}", endTime - startTime);
    }

    static class Node {
        // 当前线程正在抢占锁,或者已经占有锁 true
        // 当前线程已经释放锁 下一个线程可以占有锁了 false
        volatile boolean locked;


        // 前驱节点 需要监听其lock字段
        Node preNode;


        public Node(boolean locked, Node preNode) {
            this.locked = locked;
            this.preNode = preNode;

        }

        // 空节点
        public static final Node EMPTY = new Node(false, null);


        public boolean isLocked() {
            return locked;
        }

        public void setLocked(boolean locked) {
            this.locked = locked;
        }

        public Node getPreNode() {
            return preNode;
        }

        public void setPreNode(Node preNode) {
            this.preNode = preNode;
        }
    }


    static class CLHSpinLock implements Lock {
        // 创建当前节点的本地变量
        private static ThreadLocal<Node> currentNodeLocal = new ThreadLocal<Node>();

        // CLH队列的尾指针,使用AtomicReference,方法CAS操作
        AtomicReference<Node> tail = new AtomicReference<>(null);

        public CLHSpinLock() {
            // 设置尾部节点
            tail.getAndSet(Node.EMPTY);
        }

        @Override
        public void lock() {
            // 加锁操作
            // 将节点添加到等待队列的尾部
            Node curNode = new Node(true, null);
            Node preNode = tail.get();
            // 通过CAS自旋 将当前节点插入到队列的尾部
            while (!tail.compareAndSet(preNode, curNode)) {
                preNode = tail.get();
            }
            // 设置前驱节点
            curNode.setPreNode(preNode);

            // 自旋监听前驱节点的locked变量,直到其值为false,如果前驱节点为true说明上一个线程还没释放锁
            while (curNode.getPreNode().isLocked()) {
                // 抢锁失败 让出cpu时间片
                Thread.yield();
            }

            // 到这里说明已经抢到了锁
            // log.error("已经抢锁成功!");
            // 将当前节点缓存到线程本地变量中 释放锁的时候需要使用
            currentNodeLocal.set(curNode);

        }

        @Override
        public void lockInterruptibly() throws InterruptedException {

        }

        @Override
        public boolean tryLock() {
            return false;
        }

        @Override
        public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
            return false;
        }

        @Override
        public void unlock() {

            // 释放锁
            // 获取当前线程的threadLocal
            Node node = currentNodeLocal.get();
            // 将locked标志改为false
            node.setLocked(false);
            // 将前驱节点设置为null 断开引用方便垃圾回收
            node.setPreNode(null);
            // 释放当前缓存中的线程信息
            currentNodeLocal.set(null);
        }

        @Override
        public Condition newCondition() {
            return null;
        }
    }
}

在这里插入图片描述

在这里插入图片描述

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

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

相关文章

Linux基本命令的使用(ls cd touch)

一、Windows系统常见的文件类型 • 文本文件格式&#xff1a;txt、doc、pdf、html等。 • 图像文件格式&#xff1a;jpg、png、bmp、gif等。 • 音频文件格式&#xff1a;mp3、wav、wma等。 • 视频文件格式&#xff1a;mp4、avi、wmv、mov等。 • 压缩文件格式&#xff1a;zip…

连通块中点的数量-java

本次我们通过连通块中点的数量来加深我们对并查集的基本操作和原理&#xff0c;并且知道如何在并查集中添加附属信息。 目录 前言☀ 一、连通块中点的数量☀ 二、算法思路☀ 1.无向图&#x1f319; 2.在a b之间连一条边&#xff0c;a b可能相等&#x1f319; 3.询问a和b是否在一…

sudo命令的隐患-要注意安全使用!!严格管理!!严格控制

前言 众所周知&#xff0c;sudo命令非常方便&#xff0c;而且有一定的优点。比如不需要知道root密码就可以执行一些root的命令。相比于su 必须知道root密码来说&#xff0c;减少了root密码泄露的风险。 但是sudo也是一把非常锋利的双刃剑&#xff0c;需要加以限制&#xff0c;…

重庆人文科技学院建立“软件安全产学研基地”,推动西南地区软件安全发展

5月29日&#xff0c;重庆人文科技学院与开源网安签订了《产学研校企合作协议》&#xff0c;并举行了“重庆人文科技学院产学研基地”授牌仪式&#xff0c;此次合作不仅深化了双方在软件安全领域的产学研紧密联结&#xff0c;更是对川渝乃至西南地区软件供应链安全发展起到重要的…

微信小程序 npm构建+vant-weaap安装

微信小程序&#xff1a;工具-npm构建 报错 解决&#xff1a; 1、新建miniprogram文件后&#xff0c;直接进入到miniprogram目录&#xff0c;再次执行下面两个命令&#xff0c;然后再构建npm成功 npm init -y npm install express&#xff08;Node js后端Express开发&#xff…

【mysql】ssl_choose_client_version:unsupported protocol

起因&#xff1a;项目上的DolphinScheduler连接不上数据库&#xff0c;查看worker日志提到SSL协议问题&#xff1a; com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failureCaused by: java.io.EOFException: SSL peer shut down incorrectly 我…

KMPlayer v2024.4.25.13 官方版 (万能播放器)

前言 KMPlaye通过各种插件扩展KMP可以支持层出不穷的新格式。KMPlaye强大的插件功能&#xff0c;直接从Winamp继承的插件功能&#xff0c;能够直接使用Winamp的音频&#xff0c;输入&#xff0c;视觉效果插件&#xff0c;而通过独有的扩展能力&#xff0c;只要你喜欢&#xff…

工厂条码仓库管理系统是做什么的?

工厂条码仓库管理系统&#xff0c;可以分为两个概念&#xff1a;一个是仓库管理系统、一个是工厂条码。 在了解仓库管理和工厂条码之前&#xff0c;题主先了解一下企业的信息化建设&#xff1a; 企业信息化建设是企业提升生产效率、优化管理的重要手段。企业实现生产流程的数…

UE4 使用自带的插件制作音频可视化

1.插件默认为开启 2.新建共感NRT&#xff0c;选择要使用的音频 3.添加音频组件&#xff0c;添加共感NRT变量&#xff0c;选择新建的共感NRT对象 4.编写蓝图

从零到一建设数据中台 - 关键技术汇总

一、数据中台关键技术汇总 语言框架&#xff1a;Java、Maven、Spring Boot 数据分布式采集&#xff1a;Flume、Sqoop、kettle 数据分布式存储&#xff1a;Hadoop HDFS 离线批处理计算&#xff1a;MapReduce、Spark、Flink 实时流式计算&#xff1a;Storm/Spark Streaming、…

算法每日一题(python,2024.06.01)

题目来源&#xff1a;&#xff08;力扣. - 力扣&#xff08;LeetCode&#xff09;&#xff0c;简单&#xff09; 解题思路&#xff1a; 注意题目说的俩个是异位&#xff0c;所以需要加上俩个是否完全相等的判断 法一&#xff1b;排序比对法 如果俩个字符串中每个字符出现的次…

TiDB-从0到1-部署篇

TiDB从0到1系列 TiDB-从0到1-体系结构TiDB-从0到1-分布式存储TiDB-从0到1-分布式事务TiDB-从0到1-MVCCTiDB-从0到1-部署篇 一、TiUP TiUP是TiDB4.0版本引入的集群运维工具&#xff0c;通过TiUP可以进行TiDB的日常运维工作&#xff0c;包括部署、启动、关闭、销毁、弹性扩缩容…

网络安全基础技术扫盲篇 — 名词解释之“数据包“

用通俗易懂的话说&#xff1a; 数据包就像是一个信封。当你写信给某个人时&#xff0c;你将内容写在一张纸上&#xff0c;然后将纸叠起来并放入信封中&#xff0c;就形成了一个完整要发送的数据内容。信封上有发件人和收件人的详细地址&#xff0c;还有一些其他必要的信息&…

交易员摩拳擦掌,就在今年夏天,极端气候引爆商品?

有史以来最严重的高温炙烤下&#xff0c;从农业到能源到航运都可能受到严重负面影响&#xff0c;大宗商品市场波动将大幅加剧。 2024年有望成为有史以来最炎热的一年&#xff0c;随着北半球步入夏季&#xff0c;世界各地都将遭受由全球变暖造成的极端高温困扰。极端天气不仅给民…

WalleWeb简化你的DevOps部署流程

walle-web&#xff1a;简化部署流程&#xff0c;提升开发效率&#xff0c;Walle Web让DevOps触手可及 - 精选真开源&#xff0c;释放新价值。 概览 Walle Web是一个功能强大且免费开源的DevOps平台&#xff0c;旨在简化和自动化代码部署流程。它支持多种编程语言&#xff0c;包…

Python魔法之旅-魔法方法(07)

目录 一、概述 1、定义 2、作用 二、应用场景 1、构造和析构 2、操作符重载 3、字符串和表示 4、容器管理 5、可调用对象 6、上下文管理 7、属性访问和描述符 8、迭代器和生成器 9、数值类型 10、复制和序列化 11、自定义元类行为 12、自定义类行为 13、类型检…

自动控制系统

文章目录 1234 1 最后是一个水位控制的 &#xff08;感觉最好&#xff09; 2 阻力增大了 就不够了跟古代行军打仗似的感觉设计的话 要考虑的更多 3 4 刚开始转速 为0这调压电路是 因为是直流有刷电机 代码部分 主要看main 其他和测速例程一样 这调电压 好像是和功率相关pfv 负…

Three.js 性能监测工具 Stats.js

目录 前言 性能监控 引入 Stats 使用Stats 代码 前言 通过stats.js库可以查看three.js当前的渲染性能&#xff0c;具体说就是计算three.js的渲染帧率(FPS),所谓渲染帧率(FPS)&#xff0c;简单说就是three.js每秒钟完成的渲染次数&#xff0c;一般渲染达到每秒钟60次为…

AI视频教程下载:给数据分析师的生成式AI课

生成式人工智能知识现已成为数据科学的一项基本技能。根据 Gartner 的数据&#xff0c;"到 2026 年&#xff0c;20% 的顶级数据科学团队将改名为认知科学或科学咨询公司&#xff0c;员工技能的多样性将增加 800%"。 考虑到这一行业趋势&#xff0c;IBM 为您带来了这…

C++ | Leetcode C++题解之第123题买卖股票的最佳时机III

题目&#xff1a; 题解&#xff1a; class Solution { public:int maxProfit(vector<int>& prices) {int n prices.size();int buy1 -prices[0], sell1 0;int buy2 -prices[0], sell2 0;for (int i 1; i < n; i) {buy1 max(buy1, -prices[i]);sell1 max(…