深入理解 Java 中的 synchronized 关键字

news2024/11/29 20:53:12

引入多线程的重要性和挑战

可以参考另一篇文章 https://blog.csdn.net/qq_41956309/article/details/133717408

JMM(Java Memory Model,Java 内存模型)

什么是JMM

JMM(Java Memory Model,Java 内存模型)是一种规范,用于定义多线程程序中的内存访问规则和语义,确保多线程程序的正确性和可移植性。JMM 定义了线程如何与主内存和工作内存交互,以及如何确保多线程程序中的内存可见性和一致性。

引入JMM的目的

引入 Java Memory Model(JMM)的主要目的是为了解决多线程编程中的内存可见性和一致性问题,以及确保多线程程序的正确性和可移植性 它提供了规则和机制,使得多线程编程更容易管理和理解,减少了开发人员因多线程编程而引入的错误和 bug。

引入JMM的原因

在Java语言之前,C、C++等语言是直接使用物理硬件和操作系统的内存模型的,正因为这些语言直接和底层打交道,使得这些语言执行效率更高,但同时也带来了一些问题:由于不同平台上,软件和硬件都有一定差异(比如硬件厂商不同、操作系统不同),导致有可能同一个程序在一套平台上执行没问题,另一个平台上执行却得到不一样的结果,甚至报错。
Java语言试图定义一个Java内存模型来屏蔽掉各种硬件和操作系统的内存访问差异,达到让Java程序在不同平台上都能达到一致的内存访问效果的目的,这就是Java内存模型的意义。

JMM的主要结构和概念

1.主内存(Main Memory): 主内存是所有线程共享的内存区域,用于存储共享数据,包括全局变量、对象实例数据和类信息。主内存是多线程程序中的主要数据存储区域。

2.工作内存(Working Memory): 每个线程都有自己的工作内存,用于存储线程私有的数据,包括局部变量、方法参数和方法调用信息。工作内存用于缓存主内存中的数据。

3.主内存与工作内存交互规则:要想读主内存的变量,首先得将主内存中的变量读取到工作内存中;要想修改主内存的变量,首先得先修改工作内存中的变量副本,然后再将变量副本写到主内存中。

4.同步操作: JMM 提供了一些同步操作,如锁、synchronized 块和 volatile 关键字,用于协调线程之间的数据访问。这些同步操作确保了线程之间的协同和互斥访问。

5.Happens-Before 关系: JMM 引入了 Happens-Before 关系,定义了事件发生的顺序和相关性。如果事件 A Happens-Before 事件 B,那么事件 B 将看到事件 A 之前的所有操作。这有助于确保多线程程序中的正确性。

i++为什么线程不安全

假设我们有一个共享变量 int i = 0;,两个线程 A 和 B 同时执行 i++ 操作。
1.线程 A 和线程 B 同时启动,各自创建自己的工作内存,其中 i 的初始值为 0
请添加图片描述
2.线程A从主内存中读取i=0 然后执行i++操作此时 i=1 并且将i的值写回主内存中
请添加图片描述
3.线程B因为是和线程A同时执行 所以在线程A将i进行++ 导致i=1并写会主内存之前就从主内存中读到了i=0 因此读到线程B自己工作内存中的 i 的值依然 = 0 并对0 进行++ 此时线程B自己工作内存中i =1 并把i=1写回主内存中 覆盖掉了原先A线程写到主存中的i=1 此时主存中的i =1 而不是 =2 所以导致了线程不安全的问题
请添加图片描述

如何解决并发导致的线程不安全问题

假设我们增加一个共享变量 lock 当线程A抢占到这个lock的时候才能继续执行,线程B没抢占的时候加入一个阻塞队列中等待只有等线程释放lock之后裁让继续执行 那么此时等线程A对i进行++之后 主内存中的i=1 然后释放lock 此时B能拿到lock在去读主内存中 i的值此时i已经=1了 那么将1 加载进自己的工作内存在进行++ 此时就能得到正确的值并且写回到主内存中
在这里插入图片描述

synchronized 的基本概念:

synchronized 是 Java 中用于实现线程同步的关键字。它的主要作用是确保多个线程能够安全地访问共享资源,避免竞态条件和数据不一致性的问题

特性

1.互斥性(Mutual Exclusion): synchronized 确保在同一时间只有一个线程可以获得锁并执行被 synchronized 修饰的代码块或方法。这防止多个线程同时访问共享资源,从而避免竞态条件和数据不一致性。

2.对象级别锁: synchronized 可以用于锁定对象或类。当它用于实例方法时,它锁定了对象实例;当它用于静态方法时,它锁定了类。这意味着不同的对象实例可以同时调用不同的实例方法,而不会互相阻塞。

3.重入性(Reentrancy): Java 中的 synchronized 支持重入性,即一个线程可以多次获得同一个锁而不会被阻塞。这允许线程在持有锁的情况下进入另一个 synchronized 块或方法。重入性有助于避免死锁。

4.内置锁(Intrinsic Lock): synchronized 使用内置锁,也称为监视器锁。每个 Java 对象都有一个关联的内置锁,线程可以通过 synchronized 来获取或释放这个锁。当一个线程获取锁时,其他线程会被阻塞,直到锁被释放。

5.阻塞性质: 当一个线程无法获得 synchronized 块或方法的锁时,它会被阻塞,直到锁可用。这确保了线程的排队执行,避免了竞争条件。

6.性能开销: synchronized 在某些情况下可能引入性能开销,因为只有一个线程可以执行被锁定的代码块。在高并发情况下,这可能导致性能下降。因此,在某些情况下,更高级的同步工具,如 ReentrantLock,可以提供更好的性能控制

synchronized 的作用范围:

例方法级别的 synchronized

当 synchronized 修饰实例方法时,锁的范围是该实例对象。这意味着不同实例对象之间的锁互不干扰,每个实例对象都有自己的锁,可以并发执行相同的方法,但同一个实例对象的方法只能被一个线程执行,其他线程会被阻塞。

public synchronized void instanceMethod() {
    // 该方法的锁作用范围是当前对象实例
}

静态方法级别的 synchronized

当 synchronized 修饰静态方法时,锁的范围是该类的 Class 对象,因此它是全局的,对该类的所有实例对象生效。这意味着无论多少实例对象存在,只有一个线程能够同时执行该静态方法。

public static synchronized void staticMethod() {
    // 该方法的锁作用范围是当前类的 Class 对象
}

对象锁

当 synchronized 修饰实例方法时,它使用的是对象锁,作用范围是该对象实例。多个线程可以同时访问不同对象实例的方法,但同一个对象实例的方法只能被一个线程执行

class Example {
    public synchronized void instanceMethod() {
        // 该方法的锁作用范围是当前对象实例
    }
}

Example obj1 = new Example();
Example obj2 = new Example();

// 不同对象实例可以并发执行
obj1.instanceMethod();
obj2.instanceMethod();

类锁

当 synchronized 修饰静态方法时,它使用的是类锁,作用范围是整个类,对该类的所有实例对象生效。只有一个线程能够同时执行该静态方法,无论有多少实例对象存在。

class Example {
    public static synchronized void staticMethod() {
        // 该方法的锁作用范围是当前类的 Class 对象
    }
}

Example obj1 = new Example();
Example obj2 = new Example();

// 不论有多少实例对象,同一个类的静态方法只能被一个线程执行
Example.staticMethod();

synchronized案例:

演示如何使用 synchronized 实现多线程访问共享资源的同步。在这个例子中,我们有一个银行账户对象,多个线程同时进行存款和取款操作,需要确保线程安全。

class BankAccount {
    private int balance = 1000;

    public synchronized void deposit(int amount) {
        balance += amount;
    }

    public synchronized void withdraw(int amount) {
        if (balance >= amount) {
            balance -= amount;
        } else {
            System.out.println("Insufficient funds");
        }
    }

    public synchronized int getBalance() {
        return balance;
    }
}

public class BankExample {
    public static void main(String[] args) {
        BankAccount account = new BankAccount();

        // 创建多个存款线程
        Thread depositThread1 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                account.deposit(10);
            }
        });

        Thread depositThread2 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                account.deposit(10);
            }
        });

        // 创建多个取款线程
        Thread withdrawThread1 = new Thread(() -> {
            for (int i = 0; i < 50; i++) {
                account.withdraw(20);
            }
        });

        Thread withdrawThread2 = new Thread(() -> {
            for (int i = 0; i < 50; i++) {
                account.withdraw(20);
            }
        });

        // 启动线程
        depositThread1.start();
        depositThread2.start();
        withdrawThread1.start();
        withdrawThread2.start();

        // 等待所有线程执行完毕
        try {
            depositThread1.join();
            depositThread2.join();
            withdrawThread1.join();
            withdrawThread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 打印账户余额
        System.out.println("Final account balance: " + account.getBalance());
    }
}

在这个示例中,BankAccount 类的存款和取款方法都使用 synchronized 修饰,以确保同一时间只有一个线程可以执行这些方法。这样,多个线程可以安全地访问共享的账户对象,避免了竞态条件和数据不一致性。

synchronized 的原理:

Java对象结构

请添加图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

重量级锁存在的性能问题

在Linux系统架构中可以分为用户空间和内核,我们的程序都运行在用户空间,进入用户运行状态就是所谓的用户态。在用户态可能会涉及到某些操作如I/O调用,就会进入内核中运行,此时进程就被称为内核运行态,简称内核态。

内核: 本质上可以理解为一种软件,控制计算机的硬件资源,并提供上层应用程序运行的环境。
用户空间: 上层应用程序活动的空间。应用程序的执行必须依托于内核提供的资源,包括CPU资源、存储资源、I/O资源等。

系统调用: 为了使上层应用能够访问到这些资源,内核必须为上层应用提供访问的接口:即系统调用。
使用monitor是重量级锁的加锁方式。在objectMonitor.cpp中会涉及到Atomic::cmpxchg_ptr,Atomic::inc_ptr等内核函数,
执行同步代码块,没有竞争到锁的对象会park()被挂起,竞争到锁的线程会unpark()唤醒。这个时候就会存在操作系统用户态和内核态的转换,这种切换会消耗大量的系统资源。试想,如果程序中存在大量的锁竞争,那么会引起程序频繁的在用户态和内核态进行切换,严重影响到程序的性能。这也是为什么说synchronized效率低的原因

synchronized锁优化

JDK1.6中引入偏向锁和轻量级锁对synchronized进行优化。此时的synchronized一共存在四个状态:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。锁着锁竞争激烈程度,锁的状态会出现一个升级的过程。即可以从偏向锁升级到轻量级锁,再升级到重量级锁。锁升级的过程是单向不可逆的,即一旦升级为重量级锁就不会再出现降级的情况。

优化后锁的状态

偏向锁

什么是偏向锁

偏向锁(Biased Locking)是Java中用于提高单线程访问同步块的性能的一种锁机制。它的核心思想是,当一个线程第一次访问一个同步块时,虚拟机会将锁对象标记为偏向锁,并记录获取锁的线程ID。之后,如果同一个线程再次访问该同步块,它可以直接获取锁,而无需竞争,从而提高了性能。

为什么要引入偏向锁

大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,才引入的偏向锁。

偏向锁的作用

减少同一线程获取锁的代价。

加锁过程

1.首次加锁:当第一个线程尝试获取该对象的锁时,虚拟机检测到该对象没有被任何线程锁定,于是它将该对象的状态设置为偏向锁状态,并将偏向锁标志位设置为1。

2.线程ID记录:虚拟机记录获取锁的线程的ID,并将其存储在对象头中的偏向线程ID字段。此时,对象已经被偏向于第一个线程。

3.偏向锁校验: 当同一个线程再次尝试获取该对象的锁时,虚拟机会检查记录在对象头中的线程ID,与当前线程的ID进行比较。如果两者相同,说明当前线程已经获取了偏向锁,可以直接进入临界区执行,无需进一步竞争。

4.偏向锁升级: 如果当前线程的ID与记录在对象头中的线程ID不匹配,表示其他线程曾经获取过这个锁,偏向锁不再生效,会被撤销。此时,虚拟机会尝试使用CAS(Compare and Swap)操作来竞争锁。

5.获取锁: 如果CAS操作成功,当前线程获得了锁,进入临界区执行。如果CAS操作失败,虚拟机会尝试使用轻量级锁或重量级锁来竞争锁,具体取决于竞争情况。

什么是偏向锁撤销

偏向锁的撤销是为了应对多线程竞争的情况,以保证多线程环境下的锁操作能够正确执行。一旦偏向锁被撤销,锁对象会升级为轻量级锁或重量级锁,这些锁提供了更复杂的锁协议,以确保多线程环境下的正确性和公平性。

偏向锁撤销流程

竞争检测: 当有一个线程尝试获取一个对象的偏向锁时,虚拟机会进行竞争检测。它会检查对象头中的偏向锁标志位和线程ID字段,以确认是否有其他线程曾经获取过该锁。

判断偏向锁状态: 如果虚拟机发现偏向锁标志位为1,而且线程ID字段不是当前线程的ID,那么说明有其他线程曾经获取过该锁。

偏向锁撤销: 当偏向锁被撤销时,虚拟机会将对象头中的偏向锁标志位设置为0,表示不再使用偏向锁。线程ID字段也会被清空。

锁升级: 偏向锁被撤销后,虚拟机会将对象的锁状态升级为轻量级锁或重量级锁,具体升级方式取决于当前的竞争情况。这通常涉及到CAS(Compare and Swap)操作,用于确保多线程环境下的正确性和公平性。

轻量级锁

加锁过程

轻量级锁优化性能的依据是对于大部分的锁,在整个同步生命周期内都不存在竞争。 当升级为轻量级锁之后,MarkWord的结构也会随之变为轻量级锁结构。JVM会利用CAS尝试把对象原本的Mark Word 更新为Lock Record的指针,成功就说明加锁成功,改变锁标志位为00,然后执行相关同步操作。
轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁就会失效,进而膨胀为重量级锁。

解锁过程

当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cpu。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。

自旋锁

轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。
自旋锁是基于在大多数情况下,线程持有锁的时间都不会太长。如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),不断的尝试获取锁。空循环一般不会执行太多次,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入同步代码。如果还不能获得锁,那就会将线程在操作系统层面挂起,即进入到重量级锁。

重量级锁

当线程的自旋次数过长依旧没获取到锁,为避免CPU无端耗费,锁由轻量级锁升级为重量级锁。获取锁的同时会阻塞其他正在竞争该锁的线程,依赖对象内部的监视器(monitor)实现,monitor又依赖操作系统底层,需要从用户态切换到内核态,成本非常高。

成本高的原因

当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cpu。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。

偏向锁、轻量级锁、重量级锁的对比

在这里插入图片描述

synchronized锁升级过程

在这里插入图片描述
1.当线程A尝试获取该对象的锁时,虚拟机检测到该对象没有被任何线程锁定,于是它将该对象的状态设置为偏向锁状态,并将偏向锁标志位设置为1。并且在Mark Word中记录线程A的ID 此时,对象已经被偏向于线程A。
假设当线程A再次尝试获取锁 虚拟机会检查对象的状态。由于对象已经被偏向于当前线程,偏向锁标志位和线程ID会与当前线程匹配。因此,虚拟机可以直接让线程获取锁,无需竞争直接执行同步代码

2.当线程B试图获得这个锁时,JVM发现同步锁处于偏向状态,但是Mark Word中的线程id记录的不是B,那么线程B会先用CAS操作试图获得锁,这里的获得锁操作是有可能成功的,因为线程A一般不会自动释放偏向锁。如果抢锁成功,就把Mark Word里的线程id改为线程B的id,代表线程B获得了这个偏向锁,可以执行同步代码。如果抢锁失败。

3.代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。JVM会在当前线程的线程栈中开辟一块单独的空间,里面保存指向对象锁Mark Word的指针,同时在对象锁Mark Word中保存指向这片空间的指针。上述两个保存操作都是CAS操作,如果保存成功,代表线程抢到了同步锁,就把Mark Word中的锁标志位改成00,可以执行同步代码。如果保存失败,表示抢锁失败,竞争太激烈。

  1. 轻量级锁抢锁失败,JVM会使用自旋锁,自旋锁不是一个锁状态,只是代表不断的重试,尝试抢锁。从JDK1.7开始,自旋锁默认启用,自旋次数由JVM决定。如果抢锁成功则执行同步代码,如果失败则继续执行步骤5;

  2. 自旋锁重试之后如果抢锁依然失败,同步锁会升级至重量级锁,锁标志位改为10。在这个状态下,未抢到锁的线程都会被阻塞。

synchronized 的局限性:

1.粒度粗: synchronized 只能锁定代码块或方法,这意味着只能对整个方法或代码块进行同步。如果某个方法中只有一小部分需要同步,使用synchronized可能导致过多的线程阻塞。

2.性能开销: synchronized 在某些情况下可能引入性能开销,因为只有一个线程可以执行被锁定的代码块,其他线程必须等待。这可能导致性能下降,尤其在高并发情况下。

3.无法中断: 一旦线程获得synchronized锁,其他线程无法中断它,只能等待锁被释放。这可能导致线程在等待锁时无法响应中断信号。

4.无法设置超时: synchronized 也无法设置获取锁的超时时间,这使得在某些情况下可能导致线程无限期等待。

5.只支持互斥锁: synchronized 只提供了一种互斥锁,这意味着只有一个线程可以获取锁,其他线程必须等待。它不支持更复杂的同步模式,如读写锁或信号量。

6.局部性差: synchronized 不提供足够的工具来优化缓存局部性,这可能会导致内存访问效率降低。

与其他同步机制的对比

synchronized vs. ReentrantLock

synchronized 是内置锁,而 ReentrantLock 是Java提供的一个可重入锁。
ReentrantLock提供了更多的灵活性,如超时等待、可中断锁、公平性等。
synchronized 更简单易用,但ReentrantLock提供更多高级功能。

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

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

相关文章

怎么在抖音上引流?分享五个抖音引流推广必备的几个方法

大家好&#xff0c;我是 小刘今天为大家分享的是抖音引流知识分享&#xff0c;今天咱们聊一些干货知识&#xff0c;绝对会让你们有一个重新的认知。抖音的流量大&#xff0c;是毋庸置疑的&#xff0c;抖音也是最早一批短视频平台。抖音于2017年上线&#xff0c;一开始主要是通过…

Golang学习记录:基础知识篇(一)

Golang学习&#xff1a;基础知识篇&#xff08;一&#xff09; 前言什么是Golang&#xff1f;Go语言的基础语法语言结构基础语法数据类型基础使用 前言 很久之前就想学Go语言了&#xff0c;但是一直有其他东西要学&#xff0c;因为我学的是Java嘛&#xff0c;所以后面学的东西…

配置VScode开发环境-CUDA编程

如果觉得本篇文章对您的学习起到帮助作用&#xff0c;请 点赞 关注 评论 &#xff0c;留下您的足迹&#x1f4aa;&#x1f4aa;&#x1f4aa; 本文主要介绍VScode下的CUDA编程配置&#xff0c;因此记录以备日后查看&#xff0c;同时&#xff0c;如果能够帮助到更多人&#xf…

操作系统导论-第四章作业(待更)

一、进程 进程就是运行中的程序&#xff0c;程序本身是没有生命周期的&#xff0c;它只是存储在磁盘上的一些指令&#xff08;或者一些静态数据&#xff09;&#xff0c;操作系统将这些指令和数据加载到内存中&#xff0c;使其运行起来。 1.1 虚拟化CPU技术 根据我们平时使用…

基于Java的共享充电宝管理系统设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序&#xff08;小蔡coding&#xff09;有保障的售后福利 代码参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作…

AI时代助力程序员与项目经理的双翼飞翔:从开发到成长的秘诀

❤️作者主页&#xff1a;小虚竹 ❤️作者简介&#xff1a;大家好,我是小虚竹。2022年度博客之星评选TOP 10&#x1f3c6;&#xff0c;Java领域优质创作者&#x1f3c6;&#xff0c;CSDN博客专家&#x1f3c6;&#xff0c;华为云享专家&#x1f3c6;&#xff0c;掘金年度人气作…

企业网盘中支持在线编辑的有哪些选项?

企业网盘作为现代企业不可或缺的工具之一&#xff0c;为企业提供了便捷的文件存储和共享功能。而其中支持在线编辑的解决方案更是减少了对额外软件的依赖&#xff0c;使团队成员可以直接在浏览器中进行实时协作。 什么是在线编辑&#xff1f; 在线编辑是指用户无需下载文件&a…

3D 生成重建008-zero123让扩散模型了解空间信息zero-shot 单图生3d

3D 生成重建008-zero123让扩散模型了解空间信息zero-shot 单图生3d 文章目录 00 论文工作1 论文方法1.1 条件生成微调1.2 维护3d表示 2 效果 0 0 论文工作 之前分享的工作主要尝试是从一个pre-trained 文生图的diffusion模型中去蒸馏知识&#xff0c;从而去维护一个3d的表示…

数据结构上机实验——栈和队列的实现、栈和队列的应用、进制转换、约瑟夫环问题

文章目录 栈和队列上机实验1.要求2.栈的实现&#xff08;以顺序栈为例&#xff09;3.队列的实现&#xff08;以顺序队列为例&#xff09;4.利用栈实现进制转换5.利用队列解决约瑟夫环问题6.全部源码Stack.hQueue.htest.cpp 栈和队列上机实验 1.要求 1.利用栈的基本操作实现将任…

docker-compose部署elk(8.9.0)并开启ssl认证

docker部署elk并开启ssl认证 docker-compose部署elk部署所需yml文件 —— docker-compose-elk.yml部署配置elasticsearch和kibana并开启ssl配置基础数据认证配置elasticsearch和kibana开启https访问 配置logstash创建springboot项目进行测试kibana创建视图&#xff0c;查询日志…

李宏毅生成式AI课程笔记(持续更新

01 ChatGPT在做的事情 02 预训练&#xff08;Pre-train&#xff09; ChatGPT G-Generative P-Pre-trained T-Transformer GPT3 ----> InstructGPT&#xff08;经过预训练的GPT3&#xff09; 生成式学习的两种策略 我们在使用ChatGPT的时候会注意到&#xff0c;网站上…

2023/10/15

文章目录 1.uniapp之Vue2升Vue3值得注意的几点1.1 页面生命周期的使用1.2 引入资源的方式 2. 浏览器本地存储之Cookie和webStorage3. CSS变量 var()的用法4. CSS之实现线性渐变背景5. 图片无法和文字对齐的正确解决方案6. 使用正则处理接口返回的富文本内的图片7. transition实…

Java练习题-获取数组元素最大值

✅作者简介&#xff1a;CSDN内容合伙人、阿里云专家博主、51CTO专家博主、新星计划第三季python赛道Top1&#x1f3c6; &#x1f4c3;个人主页&#xff1a;hacker707的csdn博客 &#x1f525;系列专栏&#xff1a;Java练习题 &#x1f4ac;个人格言&#xff1a;不断的翻越一座又…

[cpp primer随笔] 11. 内联函数与constexpr函数

1. 内联函数 调用函数一般比对等价表达式求值要慢。因为调用函数除了对表达式求值外&#xff0c;还包含一系列过程&#xff0c;包括堆栈建立、拷贝实参、跳转执行等等。而在程序之中&#xff0c;通常存在一些优化规模较小、流程直接、却调用频率很高的函数&#xff0c;我们可以…

51系列—基于51单片机的集中抄表设计(代码+文档资料)

概述 自动抄表&#xff08;Automatic Meter Reading-AMR&#xff09;是指采用通讯和计算机网络等技术自动读取和处理表计数据。发展电能自动抄表技术是提高用电管理水平的需要&#xff0c;也是网络和计算机技术迅速发展的必然。在用电管理方面&#xff0c;采用自动抄表技术&am…

YOLOv5-QAT量化部署

目录 前言一、QAT量化浅析二、YOLOv5模型训练1. 项目的克隆和必要的环境依赖1.1 项目克隆1.2 项目代码结构整体介绍1.3 环境安装 2. 数据集和预训练权重的准备2.1 数据集2.2 预训练权重准备 3. 训练模型3.1 修改数据配置文件3.2 修改模型配置文件3.3 训练模型3.4 mAP测试 三、Y…

浅谈“智慧园区”

前言&#xff1a;国庆《中国智慧园区发展白皮书&#xff08;2022&#xff09;》&#xff0c;很全面的介绍智慧园区的起源、发展阶段、涉及内容、未来规划、竞争格局等。做了些笔记&#xff0c;这对在智慧园区工作的伙伴应该很有帮助&#xff0c;下面是笔记和一些公开资料的整合…

小谈设计模式(30)—Java设计模式总结

小谈设计模式&#xff08;30&#xff09;—Java设计模式总结 专栏介绍专栏地址专栏介绍 总括三个主要类别abc 创建型模式&#xff08;Creational Patterns&#xff09;常见的创建型模式单例模式&#xff08;Singleton Pattern&#xff09;工厂模式&#xff08;Factory Pattern&…

【计算机网络笔记】分组交换中的报文交付时间计算例题

系列文章目录 什么是计算机网络&#xff1f; 什么是网络协议&#xff1f; 计算机网络的结构 数据交换之电路交换 数据交换之报文交换和分组交换 系列文章目录题目解答 题目 在下图所示的采用“存储-转发”方式的分组交换网络中所有链路的数据传输速率为100 Mbps&#xff0c;分…

[开源]基于Vue+ElementUI+G2Plot+Echarts的仪表盘设计器

一、开源项目简介 基于SpringBoot、MyBatisPlus、ElementUI、G2Plot、Echarts等技术栈的仪表盘设计器&#xff0c;具备仪表盘目录管理、仪表盘设计、仪表盘预览能力&#xff0c;支持MySQL、Oracle、PostgreSQL、MSSQL、JSON等数据集接入&#xff0c;对于复杂数据处理还可以使用…