4.从字节码分析synchronized的实现原理

news2024/12/28 15:59:23

文章目录

  • 0.前言
    • 章节回顾
  • 同步块,同步静态方法,同步实例方法的字节码区别
    • 解析`synchronized`代码块的字节码
  • 1. 基础知识
    • 1.1 `synchronized`关键字的作用和重要性
    • 1.2 Java对象监视器
      • 1.2.1 Java中的对象监视器概念
      • 1.2.2 对象监视器在多线程环境中的作用
  • 2. 基本原理
    • 2.1. Java层面的实现机制
    • 2.2. 操作系统层面的实现机制
  • 3. 字节码指令与`synchronized`关键字
    • `monitorenter`和`monitorexit`
    • Exception table 异常处理表
    • 3.1 `monitorenter`指令
    • 3.2 `monitorexit`指令
    • 3.3 指令在字节码层面上的作用和原理
  • 4. monitor和异常处理
    • 4.1 获取和释放对象监视器
    • 4.2 同步块的异常处理
  • 5. `synchronized` 优化
    • 5.1 JVM 在`synchronized`上的优化策略
    • 5.2 锁升级和锁消除等技术
  • 9. 参考文献

从字节码分析synchronized的实现原理
在这里插入图片描述

0.前言

前几个章节我们了解到Class文件的结构剖析,以及字节码的场景语句的底层原理,以及字节码中的指令的基本含义。本章节我们学以致用,来从字节码层面分析synchronized的实现原理。

章节回顾

1.《JVM之class文件结构剖析》
2.《JVM字节码指令详解》
3.《字节码之常见java语句的底层原理》
4.《字节码进阶之方法调用指令详解 》
5.《字节码之Lambda 表达式底层原理 》
6.《字节码进阶之Lombok底层原理》

首先,我们来看一个简单的Synchronized示例:

public class SynchronizedExample {
    public void method() {
        synchronized (this) {
            System.out.println("Synchronized Method");
        }
    }
}

我们可以通过javap来看一下这段代码的字节码:

$ javap -c -s -v -l SynchronizedExample

生成的字节码大致如下:

public class SynchronizedExample {
  public void method();
    Code:
       0: aload_0   // 将"this"加载到操作数栈顶
       1: dup       // 复制操作数栈顶的值
       2: astore_1  // 将操作数栈顶的值存储到局部变量表的第1个位置,也就是"this"
       3: monitorenter // 为对象(此处即"this")加锁
       4: getstatic     #2   // 获取静态字段,此处是 java/lang/System.out:Ljava/io/PrintStream;
       7: ldc           #3  // 从常量池中加载字符串常量 "Synchronized Method"
       9: invokevirtual #4  // 调用方法,此处是 java/io/PrintStream.println:(Ljava/lang/String;)V
      12: aload_1      // 把局部变量表的第1个位置的值(即"this")加载到操作数栈顶
      13: monitorexit  // 为对象(此处即"this")解锁
      14: goto          22
      17: astore_2    // 将操作数栈顶的异常存储到局部变量表的第2个位置
      18: aload_1     // 把局部变量表的第1个位置的值(即"this")加载到操作数栈顶
      19: monitorexit // 为对象(此处即"this")解锁
      20: aload_2     // 把局部变量表的第2个位置的值(即异常)加载到操作数栈顶
      21: athrow      // 抛出异常
      22: return      // 方法返回
    Exception table: // 异常处理表
       from    to  target type
           4    14    17   any
          17    20    17   any
}

此处我们注意到,在字节码指令中,synchronized关键字对应的是monitorentermonitorexit两个指令。后面章节我们后着重的讲解这两个字节码指令的原理。

当进入synchronized块的时候,就会执行monitorenter指令对对象加锁,而当退出synchronized块的时候,就会执行monitorexit指令解锁。

当发生异常的时候,无论是否有catch语句,JVM都会确保monitorexit指令的执行,这样可以保证在发生异常时,锁定的对象能够被正确释放。

我们看到了字节码指令中有,Java字节码中的异常处理表(Exception table)不仅仅是用来处理我们显式声明的try-catch块,也被用来处理一些隐式的运行时异常,以及一些Java语言特性的实现。

  Exception table: // 异常处理表
       from    to  target type
           4    14    17   any
          17    20    17   any

同步块,同步静态方法,同步实例方法的字节码区别

(1)普通同步方法(实例方法),锁是当前实例对象 ,进入同步代码前要获得当前实例的锁
(2)静态同步方法,锁是当前类的Class对象,进入同步代码前要获得当前类的Class对象的锁
(3)同步方法块,锁是括号里面的对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

我们写一个类

package com.icepip.project;

public class MyClass {
    public synchronized void synchronizedMethod() {
        System.out.println("sync");
    }
    
    public static synchronized void synchronizedStaticMethod() {
        System.out.println("sync");
    }
    
    public void synchronizedBlock() {
        synchronized (this) {
            System.out.println("sync");
        }
    }
    
    private int myPrivateMethod() {
        return 42;
    }
}

在这里插入图片描述
输出字节码
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  1. 可以看到在反编译代码块那里的字节码中可以看到其中有一个monitorenter指令和两个monitorexit指令。其实这就是synchronized的关键所在,synchronized修饰的代码块会带有monitorenter和monitorexit指令,用于jvm中控制同步代码块访问时必须获得对象锁。那么这里为啥会有两个monitorexit指令,获取对象锁执行完代码后不是释放对象锁就行了吗?按理来说monitorenter和monitorexit应该是一对一的?这里就是关键所在,JVM为了防止异常,获得锁的线程无法释放的情况,规定了synchronized锁修饰的代码块当线程执行异常时,会自动释放对象锁。因此这里有两个monitorexit指令,分别对应正常释放和异常释放。

  2. 可以看到在修饰静态方法和实例方法那里并没有monitorenter和monitorexit指令。但是可以观察到在方法的访问flags那里都有ACC_SYNCHRONIZED修饰。其实这里也是会用到monitorenter和monitorexit,只不过在修饰方法时用ACC_SYNCHRONIZED代替了。再加上ACC_STATIC就可以判断是静态方法还是实例方法了,后面拿到需要获取的所对象,实现方式就跟代码块那里一样了。

我再补充一点就是这三种不同的同步方式之间在字节码层的主要区别:总结一下

1)普通同步方法(实例方法):锁定的是实例对象本身,也就是说,线程要执行那个实例对象的同步方法,就必须先获得那个实例对象的锁。在解码层,这是通过ACC_SYNCHRONIZED标记来实现的。

2)静态同步方法:锁定的是类的Class对象。由于静态方法属于类,而不属于类的任何一个实例对象,所以通过锁定Class对象来控制静态方法的并发访问。在解码层,同样是通过ACC_SYNCHRONIZED和ACC_STATIC标记来实现的。

3)同步方法块:同步方法块可以锁定任何对象,这通过在字节码中插入monitorenter和monitorexit指令来实现。获取锁的对象是在括号内指定的对象。它提供了更大的灵活性,因为你可以锁定任何对象,不仅仅是实例对象或Class对象。

解析synchronized代码块的字节码

Java中的synchronized代码块在字节码层面上是通过monitorentermonitorexit指令来实现的。这两个指令都会和一个引用一起使用,该引用就是需要被同步的对象。

考虑一个简单的synchronized代码块:

Object lock = new Object();
synchronized(lock) {
  // do something
}

在字节码层面上,这段代码会被转换成如下的形式:

0: new    #2
3: dup
4: invokespecial  #3
7: astore_1
8: aload_1
9: dup
10: astore_2
11: monitorenter
12: // do something
   : // 异常处理
   : aload_2
   : monitorexit
   : goto end
   : // 异常处理
   : aload_2
   : monitorexit
   : athrow
   : end

在这段字节码中:

  • 0: new3: dup4: invokespecial7: astore_1这四条指令创建了一个新的Object对象,并将其引用存储在局部变量表的第一个位置(即lock)。

  • 8: aload_1lock的引用压入到操作数栈顶,9: dup将栈顶的引用复制一份并压入栈顶,11: monitorenter获取lock上的监视器锁,开始同步块。

  • 在同步块中,如果没有异常发生,会执行到aload_2monitorexit,释放lock上的监视器锁,结束同步块。

  • 如果在同步块中有异常发生,控制流会转到异常处理代码,aload_2monitorexit会释放lock上的监视器锁,并重新抛出异常。

因此,无论同步块是否正常结束,都会确保lock上的监视器锁被释放。这就是synchronized代码块在字节码层面上的实现。

1. 基础知识

1.1 synchronized关键字的作用和重要性

在Java中,synchronized是一个关键字,用于控制多个线程对共享资源的访问。synchronized能够保证一个线程在执行同步代码块的过程中,不会被其他线程打断,从而避免造成数据不一致的问题。

这就是所谓的“互斥”,即在同一时间内,只有一个线程能够执行某一段代码。这种机制可以防止多个线程同时修改一个数据,避免出现数据不一致的问题。这对于多线程编程来说极其重要,因为在没有适当的同步措施下,多个线程同时修改一个数据可能会导致程序的结果不可预知。

还有一种情况就是“可见性”,考虑这样一种情况,当一个线程修改了一份数据,而另一个线程需要读取这份数据,如果修改后的数据不能立即对其他线程可见,那么其他线程读取的就可能是旧的数据,这同样会导致数据不一致的问题。synchronized关键字也能够保证可见性,确保修改后的数据对所有线程立即可见。

1.2 Java对象监视器

1.2.1 Java中的对象监视器概念

在Java中,每一个对象都可以作为一个监视器。这个监视器通过内部的一个监视器锁来实现的。当线程试图获取这个监视器的所有权时,它首先会试图去获取这个监视器锁。

当一个线程获取到对象的监视器锁时,这个线程便成为了这个监视器的所有者,其他线程如果也想要获取这个监视器的所有权,就必须等待当前所有者线程释放监视器锁。

Java中的synchronized关键字,就是通过这种方式获取和释放对象监视器的。

1.2.2 对象监视器在多线程环境中的作用

对象监视器在多线程环境中起到同步和互斥的作用。

  1. 同步:Java中的synchronized关键字可以用来修饰方法或者代码块,当它修饰的是静态方法时,线程在调用这个方法的时候会尝试获取调用该方法的类的类对象的监视器锁;当它修饰的是实例方法时,线程在调用这个方法的时候会尝试获取调用该方法的对象的监视器锁;当它修饰的是代码块时,线程在执行这段代码的时候会尝试获取synchronized后面括号里面对象的监视器锁。

  2. 互斥:当一个线程获取到了对象的监视器锁,其他线程就无法再获取到这个锁,只能在当前线程释放锁之后才有机会获取。这就实现了线程间的互斥,保证了线程安全。

通过对象监视器,Java可以在多线程环境中实现线程间的同步和互斥,保证了线程安全。

2. 基本原理

理解了synchronized的作用和重要性后。它是如何保证线程间的互斥和可见性的?这背后的机制是什么?

实际上,当我们用synchronized修饰方法或者代码块的时候,JVM会自动在这段代码前后插入特殊的指令(monitorentermonitorexit)这个指令就是我们上面所说的对象监视器。来管理和控制线程的执行,从而实现线程同步。后面章节,我们着重了解这些指令的。

但是,synchronized的实现原理远不止于此。它还涉及到底层操作系统的内存管理和线程调度等复杂的机制。例如,synchronized会与Java内存模型(JMM)、操作系统内核的互斥锁、CPU的缓存一致性协议等概念密切相关。

2.1. Java层面的实现机制

在Java层面,synchronized的实现主要依赖于JVM。当我们用synchronized修饰方法或者代码块时,JVM会自动在这段代码前后插入特殊的指令(monitorenter,monitorexit)来管理和控制线程的执行。

  • monitorenter:位于同步代码块的前端,表示当前线程尝试获取锁。如果获取成功,线程就可以执行同步代码块;如果失败,线程就会被阻塞,直到获取到锁。
  • monitorexit:位于同步代码块的后端,表示当前线程释放锁。

这样,synchronized就可以实现线程间的互斥,保证同一时间只有一个线程能够执行某一段代码。

2.2. 操作系统层面的实现机制

在操作系统层面,synchronized的实现主要依赖于操作系统的内存管理和线程调度机制。

当线程试图获取对象的锁时,如果该锁已经被其他线程持有,操作系统会让当前线程进入阻塞状态,并将其放入等待队列。当持有锁的线程执行完同步代码块并释放锁之后,操作系统会从等待队列中唤醒一个线程,并将锁分配给这个线程,这个线程就可以开始执行同步代码块了。

此外,操作系统还可以通过内存屏障来保证线程间的可见性。内存屏障是一种处理器指令,用于阻止特定类型的内存操作的重排序。通过插入内存屏障,可以确保某些操作的执行顺序,从而保证线程间的可见性

通过以上两个层面的实现,synchronized实现了线程间的同步,确保了线程安全。那接来我们聊聊从字节码层面聊聊synchronized关键字

3. 字节码指令与synchronized关键字

我们先看一个synchronized代码块的Java示例并解析出其字节码

public class SynchronizedBlockExample {
    public void method() {
        synchronized (this) {
            System.out.println("Synchronized Block");
        }
    }
}

编译后的字节码:

public class SynchronizedBlockExample {
    public void method();
    Code:
        0: aload_0      // 将"this"加载到操作数栈顶
        1: dup          // 复制操作数栈顶的值
        2: astore_1     // 将操作数栈顶的值存储到局部变量表的第1个位置,也就是"this"
        3: monitorenter // 为对象(此处即"this")加锁
        4: getstatic    #2  // 获取静态字段,此处是 java/lang/System.out:Ljava/io/PrintStream;
        7: ldc          #3  // 从常量池中加载字符串常量 "Synchronized Block"
        9: invokevirtual #4 // 调用方法,此处是 java/io/PrintStream.println:(Ljava/lang/String;)V
        12: aload_1     // 把局部变量表的第1个位置的值(即"this")加载到操作数栈顶
        13: monitorexit // 为对象(此处即"this")解锁
        14: goto         22
        17: astore_2    // 将操作数栈顶的异常存储到局部变量表的第2个位置
        18: aload_1     // 把局部变量表的第1个位置的值(即"this")加载到操作数栈顶
        19: monitorexit // 为对象(此处即"this")解锁
        20: aload_2     // 把局部变量表的第2个位置的值(即异常)加载到操作数栈顶
        21: athrow      // 抛出异常
        22: return      // 方法返回
    Exception table:  // 异常处理表
        from    to  target type
            4    14    17   any
            17    20    17   any
}

monitorentermonitorexit

无论是synchronized方法还是synchronized代码块,字节码层面的处理方式都是类似的,即通过monitorentermonitorexit指令来实现加锁和解锁的操作。同时,无论异常是否被处理,JVM都会确保monitorexit指令的执行,以确保锁定的对象能被正确释放。

在这个例子中,synchronized (this)块被编译为:

  1. 首先获取this对象的引用
  2. 执行monitorenter指令,获取this对象的锁
  3. 执行synchronized块中的内容(即System.out.println("Synchronized Method")
  4. 执行monitorexit指令,释放this对象的锁

Exception table 异常处理表

我们前两个章节学习的时候,Exception table当时说的是是用来处理我们显式声明的try-catch块,指定异常的时候程序的跳转位置。但是在本示例中,我们并没有看到try-catch块,但是在字节码中出现了Exception table 。其实这也是synchronized关键字字节码指令的一个特性。所以Exception table 异常处理表 之前的描述相对来说比较局限。它也被用来处理一些隐式的运行时异常,以及一些Java语言特性的实现。

这个过程中,如果monitorenter指令成功执行,但是synchronized块的内容执行中发生了异常,monitorexit指令就不会被正常执行。为了确保即使有异常发生,锁依然能被正确释放,Java编译器会在字节码中加入一个异常处理表,用来在发生异常时跳转到一个monitorexit指令以释放锁,然后再重新抛出该异常。这就是为什么在这个例子的字节码中会有异常处理表的原因。

3.1 monitorenter指令

monitorenter是Java虚拟机的一个字节码指令,用于获取对象的锁。当代码块或方法前使用synchronized修饰后,在编译成的字节码中就会出现这个指令。

在Java中,每个对象都有一个内置锁(也称为监视器锁或互斥锁)。当一个线程需要访问一个被synchronized修饰的代码块或方法时,需要先获取这个对象的内置锁。

  1. 如果锁的计数器是0,表示这个锁没有被任何线程持有。这时,Java虚拟机会让请求的线程获取这个锁,并把锁的计数器设为1。然后线程就可以进入synchronized代码块或方法进行操作。

  2. 如果当前线程已经持有这个锁,比如在一个已经获取锁的synchronized方法或代码块中,再次请求获取同一个锁。这时monitorenter会使锁的计数器增加1,这被称为锁的重入。

  3. 如果锁已经被其他线程持有,这时当前线程的monitorenter请求会失败,线程就会进入阻塞状态,等待锁的释放。

  4. 当锁被释放后(即其他线程执行了monitorexit),系统会从等待队列中唤醒一个或多个线程,让它们再次尝试获取锁。

monitorentermonitorexit必须配对使用,每一个monitorenter操作都需要对应一个monitorexit操作来释放锁。

这种锁机制可以保证同一时间内,只有一个线程可以执行synchronized修饰的代码块或方法,从而避免了线程间的数据竞争,达到了线程同步的效果。

3.2 monitorexit指令

monitorexit是Java虚拟机的一个字节码指令,用于释放对象的锁,它与monitorenter指令相对。当synchronized修饰的代码块或方法执行完后,在编译生成的字节码中就会出现这个指令。

在Java中,每个对象都有一个内置锁(也称为监视器锁或互斥锁)。当一个线程已经获取了一个对象的内置锁,执行了synchronized修饰的代码块或方法后,需要释放这个锁,以让其他线程也能获取这个锁来访问这个代码块或方法。

当一个线程执行到monitorexit指令时,Java虚拟机会检查这个线程是否是这个对象的锁的持有者。如果是,那么它会释放这个锁,并将锁的计数器减1。如果计数器变为0,表示锁完全被释放。

如果这个线程不是锁的持有者,那么monitorexit指令会导致Java虚拟机抛出IllegalMonitorStateException异常。

monitorentermonitorexit必须配对使用,每一个monitorenter操作都需要对应一个monitorexit操作来释放锁。这种机制可以保证同一时间内,只有一个线程可以执行synchronized修饰的代码块或方法,从而避免了线程间的数据竞争,达到了线程同步的效果。

3.3 指令在字节码层面上的作用和原理

在字节码层面上,monitorentermonitorexit指令配合实现了synchronized的语义。

当线程执行到monitorenter指令时,它尝试获取对象的监视器锁。如果监视器锁未被其他线程持有,则该线程获取该锁并将锁的计数器设为1;如果当前线程已经持有该锁,那么它将仅仅将锁的计数器增加1;如果锁被其他线程持有,那么当前线程将被阻塞,进入等待状态,直到获取到锁为止。

当线程执行到monitorexit指令时,如果当前线程持有该锁,它将锁的计数器减一,如果计数器的值变为0,那么锁就会被释放;如果当前线程并不持有该锁,那么将会抛出java.lang.IllegalMonitorStateException异常。

所以,每一个monitorenter必须要有相对应的monitorexit,它们之间的执行逻辑必须完整,这样就能保证获取到的锁能够被正确释放。

4. monitor和异常处理

在这里插入图片描述

4.1 获取和释放对象监视器

在Java中,每个对象都有一个与之关联的监视器(monitor)。当线程需要进入synchronized代码块时,它需要先获取这个对象的监视器。如果该监视器已经被另一个线程持有,那么当前线程就会阻塞,直到监视器被释放。

获得对象监视器实际上就是获得一个“锁”。这是通过monitorenter指令实现的。当线程达到monitorenter指令时,它会试图获取锁。如果锁已经被其他线程持有,则当前线程会进入阻塞状态,直到锁被释放。

当线程完成synchronized代码块的执行或者遇到异常需要退出时,它需要释放对象监视器。这是通过monitorexit指令实现的。释放监视器会唤醒等待该监视器的其他线程。

4.2 同步块的异常处理

如前面的字节码解析中所展示的,当发生异常时,控制流会转向异常处理部分,执行monitorexit指令来释放监视器,然后重新抛出异常。这确保了即使在synchronized代码块中发生异常,监视器仍然会被释放,避免了线程因无法获取到监视器而陷入无限等待的情况。

在Java中,异常处理是通过异常表来实现的。每个方法都可以有一个异常表,表中每项包括:开始PC、结束PC、处理异常的代码的起始PC、需要捕获的异常类型。当发生异常时,JVM会查找异常表,找到一个范围包含当前PC并且可以处理该类型异常的项,将PC设为该项的处理代码的起始PC,开始执行异常处理代码。如果找不到,异常会继续向上抛出。

因此,synchronized代码块在字节码层面上的实现确保了无论代码块是否正常执行完毕,监视器锁都会被释放,这是通过JVM的异常处理机制和monitorexit指令共同实现的。这样可以避免因为异常导致的死锁情况,提高了程序的健壮性。

5. synchronized 优化

5.1 JVM 在synchronized上的优化策略

Java虚拟机为了提高synchronized的性能,使用了一系列的优化策略,其中包括适应性自旋、锁消除、锁粗化、轻量级锁和偏向锁等。

  1. 适应性自旋:自旋等待是指当一个线程尝试获取锁时,如果锁被其他线程占用,那么该线程不会立即挂起,而是选择进行循环等待,看是否能在短时间内获取锁。适应性自旋就是自旋等待的一种优化方式。自旋的时间不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

  2. 锁消除:锁消除是指编译器在运行期间,对一些代码要求同步,但是对象只会被一个线程使用,不存在多线程竞争的情况,此时JVM会取消对这部分代码的同步。

  3. 锁粗化:锁粗化是将多个连续的加锁、解锁操作合并为一次,扩大了锁的范围。

5.2 锁升级和锁消除等技术

Java虚拟机还使用了锁升级的技术来优化synchronized

  1. 偏向锁:偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。

  2. 轻量级锁:当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋形式尝试获取锁,不会阻塞,提高性能。

  3. 重量级锁:当锁是轻量级锁的时候,另一个线程尝试获取锁,则会膨胀为重量级锁,线程进入阻塞状态。重量级锁会让所有请求的线程进入阻塞,效率较低。

锁的升级过程是单向的,也就是说,偏向锁只能升级为轻量级锁,轻量级锁只能升级为重量级锁,不会发生降级。

9. 参考文献

https://www.cnblogs.com/wy697495/p/11607925.html

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

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

相关文章

Vue3.0性能提升主要是通过哪几方面体现的?

一、编译阶段 回顾Vue2,我们知道每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把用到的数据property记录为依赖,当依赖发生改变,触发setter,则会通知watcher,从而使关联的组件重新渲染 试想…

Linux系统编程_进程间通信第2天: 共享内存(全双工)、信号(类似半双工)、信号量

1. 共享内存概述(433.10)(全双工) 2. 共享内存编程实现(434.11) 共享内存(Shared Memory),指两个或多个进程共享一个给定的存储区 特点 共享内存是最快的一种 IPC&…

docker 安装 Centos7

1. 从docker 安装 Centos7 查看有哪些 centos7 系统:docker search centos72. 安装 centos7 docker pull docker.io/ansible/centos7-ansible3.使用镜像创建容器 docker run -itd -p 8022:22 --namevm01 -v /bodata:/bodata -h vm01 --privilegedtrue 688353a31…

Python---while循环的执行流程 解释

使用Debug调试工具&#xff0c;查看while循环的运行流程 代码 # ① 初始化计数器 i 1 # ② 编写循环条件&#xff08;判断计数器是否达到了100&#xff09; while i < 100:print(f{i 1}、老婆大人&#xff0c;我错了)# ③ 在循环体内部更新计数器i 1 ① 代码都是顺序执行…

【四:Unittest框架】

day1 总结&#xff1a; from selenium import webdriver 页面及元素常用操作&#xff1a;四大操作元素操作三大等待 三大切换 句柄/frame/alert 键盘操作 keys 鼠标操作 ActionChains JS操作 日期控件/滚动条 下拉列表 文件上传 input/非input类型文件 pytest有inittest的区别…

Cesium 学习教程 - 地球以及渲染数据导出(打印)图片

Cesium 学习教程 - 地球以及渲染数据导出&#xff08;打印&#xff09;图片 地球导出核心代码完整代码&#xff1a;在线示例 本文包括地球导出核心代码、完整代码以及在线示例。 地球导出核心代码 这里放上核心代码&#xff1a; /*** todo canvas 导出图片* param {string} da…

MT8766核心板详细参数_MTK联发科4G安卓核心板智能通讯模块

MT8766安卓核心板采用四核2.0GHz主频芯片方案&#xff0c;国内4G全网通。12nm先进工艺&#xff0c;支持Android 9.0系统。GPU采用超强 IMG GE8300 。 可流畅适配大数据运算、人脸识别算法、多种识别模式。支持高速LPDDR4/X&#xff0c;主频高达1600MHz。支持EMMC5.1。标配 WIF…

bug记录——The bean ‘xxx.FeignClientSpecification‘ could not be registered.

问题描述 在一个项目中&#xff0c;用了两个feign&#xff0c;这两个feign都走了网关&#xff0c;当启动项目时&#xff0c;启动失败&#xff0c;并且报错&#xff1a; The bean ‘petCF-gateway.FeignClientSpecification’ could not be registered. A bean with that name …

java--自动类型转换

1.什么是自动类型转换&#xff0c;为什么要进行自动类型转换&#xff1f; 类型范围小的变量&#xff0c;可以直接赋值给类型范围大的变量 2.自动类型转换在计算机中执行原理 解释说明&#xff1a;byte类型变量在内存中只占一个字节&#xff0c;因此会把12转为八进制放到内存中…

Butterworth型IIR滤波器

本文参考 https://zhuanlan.zhihu.com/p/580458091 1、数字角频率ω与模拟角频率Ω的理解 我们之前的课程接触最多的是模拟频率f&#xff0c;包括在模拟电路、高频电路以及传感器课程上&#xff0c;都是以f作为频率响应函数的横坐标。使用f的好处是其真实反映了实际系统的工作…

android 指针动画转动

记录一种简单动画 效果图&#xff1a; 都是直接使用图片资源FrameLayout布局实现&#xff0c;布局如下&#xff1a; <LinearLayout xmlns:android"http://schemas.android.com/apk/res/android"xmlns:app"http://schemas.android.com/apk/res-auto"…

Android13 实现有线网络和wifi共存

Android13 实现有线网络和wifi共存 文章目录 Android13 实现有线网络和wifi共存一、前言二、修改代码&#xff1a;1、ConnectivityService.java2、NetworkFactoryImpl.java3、Android11 和Android13 修改代码目录对比&#xff1a;4、如果只修改部分代码的后果只修改 Connectivi…

短视频矩阵系统源码---开发

一、智能剪辑、矩阵分发、无人直播、爆款文案于一体独立应用开发 抖去推----主要针对本地生活的----移动端(小程序软件系统&#xff0c;目前是全国源头独立开发)&#xff0c;开发功能大拆解分享&#xff0c;功能大拆解&#xff1a; 7大模型剪辑法&#xff08;数学阶乘&#x…

图的匹配相关学习笔记

二分图最大匹配 二分图是一种奇妙的图&#xff0c;它满足可以把其内部的节点划分成两个集合&#xff0c;且这每个集合内部的的点没有边相连。 二分图的判定 二分图的判定定理&#xff1a;一张无向图是二分图&#xff0c;当且仅当图中不存在奇环&#xff08;长度为奇数的环&am…

VisualStudio安装VSIX离线包的方法

一、安装的原理 1、使用的window工具&#xff1a;CMD或者powerShell 2、vs中用于安装vsix的exe&#xff1a; 二、安装过程 1、在VSIXInstaller.exe同级目录启动PowerShell &#xff08;1&#xff09;鼠标移到空白处 &#xff08;2&#xff09;Shift 鼠标右键 &#xff08;…

物联网_00_物理网介绍

1.物联网为什么会出现? 一句话-----追求更高品质的生活, 随着科技大爆炸, 人类当然会越来越追求衣来伸手饭来张口的懒惰高品质生活, 最早的物联网设备可以追溯到19世纪末的"在线可乐售卖机"和"特洛伊咖啡壶"(懒惰的技术人员为了能够实时看到物品的情况而设…

BAT032:批量替换当前目录下文件的部分字符

引严&#xff1a;编写批处理程序&#xff0c;实现批量替换当前目录下文件的部分字符。 一、新建Windows批处理文件 参考博客&#xff1a; CSDNhttps://mp.csdn.net/mp_blog/creation/editor/132137544 二、写入批处理代码 1.右键新建的批处理文件&#xff0c;点击【编辑】。…

飞速(FS)MTP®光纤跳线系列——数据中心布线理想选择

数据中心的重要定位要求其使用的光纤跳线具有高性能和高可靠性。飞速&#xff08;FS&#xff09;MTP光纤产品系列能够以简单的安装方式快速部署高密度链路&#xff0c;优化线缆管理&#xff0c;确保充分利用通道空间&#xff0c;显著减少安装时间和成本。 飞速&#xff08;FS&…

线性代数-Python-02:矩阵的基本运算 - 手写Matrix及numpy中的用法

文章目录 一、代码仓库二、矩阵的基本运算2.1 矩阵的加法2.2 矩阵的数量乘法2.3 矩阵和向量的乘法2.4 矩阵和矩阵的乘法2.5 矩阵的转置 三、手写Matrix代码Matrix.pymain_matrix.pymain_numpy_matrix.py 一、代码仓库 https://github.com/Chufeng-Jiang/Python-Linear-Algebra-…

uni-app:实现时钟自走(动态时钟效果)

效果 核心代码 使用钩子函数 mounted()&#xff0c;设置定时器&#xff0c;是指每秒都要去执行时间的获取&#xff0c;以至于实现时间自走的效果 mounted() { this.updateTime(); // 初始化时间 setInterval(this.updateTime, 1000); // 每秒更新时间 }, 自定义方法…