Synchronized,我要一层一层剥开你的心

news2024/9/23 3:19:31

img

三种应用方式

  1. 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁。
  2. 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁。
  3. 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象。

修饰实例方法

所谓的实例对象锁就是用synchronized修饰实例对象中的实例方法,注意是实例方法不包括静态方法,如下

COPYpublic class AccountingSync implements Runnable{
    //共享资源(临界资源)
    static int i=0;

    /**
     * synchronized 修饰实例方法
     */
    public synchronized void increase(){
        i++;
    }
    @Override
    public void run() {
        for(int j=0;j<1000000;j++){
            increase();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        AccountingSync instance=new AccountingSync();
        Thread t1=new Thread(instance);
        Thread t2=new Thread(instance);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
    /**
     * 输出结果:
     * 2000000
     */
}

上述代码中,我们开启两个线程操作同一个共享资源即变量i,由于i++;操作并不具备原子性,该操作是先读取值,然后写回一个新值,相当于原来的值加上1,分两步完成,如果第二个线程在第一个线程读取旧值和写回新值期间读取i的域值,那么第二个线程就会与第一个线程一起看到同一个值,并执行相同值的加1操作,这也就造成了线程安全失败,因此对于increase方法必须使用synchronized修饰,以便保证线程安全。此时我们应该注意到synchronized修饰的是实例方法increase,在这样的情况下,当前线程的锁便是实例对象instance,注意Java中的线程同步锁可以是任意对象。从代码执行结果来看确实是正确的,倘若我们没有使用synchronized关键字,其最终输出结果就很可能小于2000000,这便是synchronized关键字的作用。这里我们还需要意识到,当一个线程正在访问一个对象的 synchronized 实例方法,那么其他线程不能访问该对象的其他 synchronized 方法,毕竟一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,所以无法访问该对象的其他synchronized实例方法,但是其他线程还是可以访问该实例对象的其他非synchronized方法,当然如果是一个线程 A 需要访问实例对象 obj1 的 synchronized 方法 f1(当前对象锁是obj1),另一个线程 B 需要访问实例对象 obj2 的 synchronized 方法 f2(当前对象锁是obj2),这样是允许的,因为两个实例对象锁并不同相同,此时如果两个线程操作数据并非共享的,线程安全是有保障的,遗憾的是如果两个线程操作的是共享数据,那么线程安全就有可能无法保证了,如下代码将演示出该现象

COPYpublic class AccountingSyncBad implements Runnable{
    static int i=0;
    public synchronized void increase(){
        i++;
    }
    @Override
    public void run() {
        for(int j=0;j<1000000;j++){
            increase();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        //new新实例
        Thread t1=new Thread(new AccountingSyncBad());
        //new新实例
        Thread t2=new Thread(new AccountingSyncBad());
        t1.start();
        t2.start();
        //join含义:当前线程A等待thread线程终止之后才能从thread.join()返回
        t1.join();
        t2.join();
        System.out.println(i);
    }
}

上述代码与前面不同的是我们同时创建了两个新实例AccountingSyncBad,然后启动两个不同的线程对共享变量i进行操作,但很遗憾操作结果是1452317而不是期望结果2000000,因为上述代码犯了严重的错误,虽然我们使用synchronized修饰了increase方法,但却new了两个不同的实例对象,这也就意味着存在着两个不同的实例对象锁,因此t1和t2都会进入各自的对象锁,也就是说t1和t2线程使用的是不同的锁,因此线程安全是无法保证的。解决这种困境的的方式是将synchronized作用于静态的increase方法,这样的话,对象锁就当前类对象,由于无论创建多少个实例对象,但对于的类对象拥有只有一个,所有在这样的情况下对象锁就是唯一的。下面我们看看如何使用将synchronized作用于静态的increase方法。

修饰静态方法

当synchronized作用于静态方法时,其锁就是当前类的class对象锁。由于静态成员不专属于任何一个实例对象,是类成员,因此通过class对象锁可以控制静态 成员的并发操作。需要注意的是如果一个线程A调用一个实例对象的非static synchronized方法,而线程B需要调用这个实例对象所属类的静态 synchronized方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的class对象,而访问非静态 synchronized 方法占用的锁是当前实例对象锁,看如下代码

COPYpublic class AccountingSyncClass implements Runnable{
    static int i=0;

    /**
     * 作用于静态方法,锁是当前class对象,也就是
     * AccountingSyncClass类对应的class对象
     */
    public static synchronized void increase(){
        i++;
    }

    /**
     * 非静态,访问时锁不一样不会发生互斥
     */
    public synchronized void increase4Obj(){
        i++;
    }

    @Override
    public void run() {
        for(int j=0;j<1000000;j++){
            increase();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        //new新实例
        Thread t1=new Thread(new AccountingSyncClass());
        //new心事了
        Thread t2=new Thread(new AccountingSyncClass());
        //启动线程
        t1.start();t2.start();

        t1.join();t2.join();
        System.out.println(i);
    }
}

由于synchronized关键字修饰的是静态increase方法,与修饰实例方法不同的是,其锁对象是当前类的class对象。注意代码中的increase4Obj方法是实例方法,其对象锁是当前实例对象,如果别的线程调用该方法,将不会产生互斥现象,毕竟锁对象不同,但我们应该意识到这种情况下可能会发现线程安全问题(操作了共享静态变量i)。

修饰代码块

除了使用关键字修饰实例方法和静态方法外,还可以使用同步代码块,在某些情况下,我们编写的方法体可能比较大,同时存在一些比较耗时的操作,而需要同步的代码又只有一小部分,如果直接对整个方法进行同步操作,可能会得不偿失,此时我们可以使用同步代码块的方式对需要同步的代码进行包裹,这样就无需对整个方法进行同步操作了,同步代码块的使用示例如下:

COPYpublic class AccountingSync implements Runnable{
    static AccountingSync instance=new AccountingSync();
    static int i=0;
    @Override
    public void run() {
        //省略其他耗时操作....
        //使用同步代码块对变量i进行同步操作,锁对象为instance
        synchronized(instance){
            for(int j=0;j<1000000;j++){
                    i++;
              }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(instance);
        Thread t2=new Thread(instance);
        t1.start();t2.start();
        t1.join();t2.join();
        System.out.println(i);
    }
}

从代码看出,将synchronized作用于一个给定的实例对象instance,即当前实例对象就是锁对象,每次当线程进入synchronized包裹的代码块时就会要求当前线程持有instance实例对象锁,如果当前有其他线程正持有该对象锁,那么新到的线程就必须等待,这样也就保证了每次只有一个线程执行i++;操作。当然除了instance作为对象外,我们还可以使用this对象(代表当前实例)或者当前类的class对象作为锁,如下代码:

COPY//this,当前实例对象锁
synchronized(this){
    for(int j=0;j<1000000;j++){
        i++;
    }
}

//class对象锁
synchronized(AccountingSync.class){
    for(int j=0;j<1000000;j++){
        i++;
    }
}

了解完synchronized的基本含义及其使用方式后,下面我们将进一步深入理解synchronized的底层实现原理。

特性

原子性

被 synchronized 修饰的代码在同一时间只能被一个线程访问,在锁未释放之前,无法被其他线程访问到。因此,在 Java 中可以使用 synchronized 来保证方法和代码块内的操作是原子性的。

可见性

对一个变量解锁之前,必须先把此变量同步回主存中。这样解锁后,后续线程就可以访问到被修改后的值。

有序性

synchronized 本身是无法禁止指令重排和处理器优化的,

as-if-serial 语义:不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果都不能被改变。

编译器和处理器无论如何优化,都必须遵守 as-if-serial 语义。

synchronized 修饰的代码,同一时间只能被同一线程执行。所以,可以保证其有序性。

可重入性

从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁,请求将会成功,在java中synchronized是基于原子性的内部锁机制,是可重入的,因此在一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的,这就是synchronized的可重入性。

原理

synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性

字节码指令

synchronized同步块使用了monitorenter和monitorexit指令实现同步,这两个指令,本质上都是对一个对象的监视器(monitor)进行获取,这个过程是排他的,也就是说同一时刻只能有一个线程获取到由synchronized所保护对象的监视器。

线程执行到monitorenter指令时,会尝试获取对象所对应的monitor所有权,也就是尝试获取对象的锁,而执行monitorexit,就是释放monitor的所有权。

锁的释放

获取建立的 happens before 关系

锁是 java 并发编程中最重要的同步机制。锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消息。

下面是锁释放 - 获取的示例代码:

COPYclass MonitorExample {
    int a = 0;

    public synchronized void writer() {  //1
        a++;                             //2
    }                                    //3

    public synchronized void reader() {  //4
        int i = a;                       //5
        ……
    }                                    //6
}

假设线程 A 执行 writer() 方法,随后线程 B 执行 reader() 方法。根据 happens before 规则,这个过程包含的 happens before 关系可以分为两类:

  1. 根据程序次序规则,1 happens before 2, 2 happens before 3; 4 happens before 5, 5 happens before 6。
  2. 根据监视器锁规则,3 happens before 4。
  3. 根据 happens before 的传递性,2 happens before 5。

上述 happens before 关系的图形化表现形式如下:

img

在上图中,每一个箭头链接的两个节点,代表了一个 happens before 关系。黑色箭头表示程序顺序规则;橙色箭头表示监视器锁规则;蓝色箭头表示组合这些规则后提供的 happens before 保证。

上图表示在线程 A 释放了锁之后,随后线程 B 获取同一个锁。在上图中,2 happens before 5。因此,线程 A 在释放锁之前所有可见的共享变量,在线程 B 获取同一个锁之后,将立刻变得对 B 线程可见。

内存布局

在Hotspot虚拟机中,对象在内存中的布局分为三块区域:

  • 对象头(Mark Word、Class Metadata Address)、
  • 实例数据
  • 对齐填充

img

实例数据

存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。

对齐填充

由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐,这点了解即可。

对象头

Java对象头是实现synchronized的锁对象的基础。一般而言,synchronized使用的锁对象是存储在Java对象头里。它是轻量级锁和偏向锁的关键。

它实现synchronized的锁对象的基础,这点我们重点分析它,一般而言,synchronized使用的锁对象是存储在Java对象头里的,jvm中采用2个字来存储对象头(如果对象是数组则会分配3个字,多出来的1个字记录的是数组长度),其主要结构是由Mark Word 和 Class Metadata Address 组成,其结构说明如下表:

虚拟机位数头对象结构说明
32/64bitMark Word存储对象的hashCode、锁信息或分代年龄或GC标志等信息
32/64bitClass Metadata Address类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的实例
Mark Word

Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。Java对象头一般占有两个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit)。

锁状态25bit4bit1bit是否是偏向锁2bit 锁标志位
无锁状态对象HashCode对象分代年龄001

由于对象头的信息是与对象自身定义的数据没有关系的额外存储成本,因此考虑到JVM的空间效率,Mark Word 被设计成为一个非固定的数据结构,以便存储更多有效的数据,它会根据对象本身的状态复用自己的存储空间,如32位JVM下,除了上述列出的Mark Word默认存储结构外,还有如下可能变化的结构:

img

其中轻量级锁和偏向锁是Java 6 对 synchronized 锁进行优化后新增加的,稍后我们会简要分析。这里我们主要分析一下重量级锁也就是通常说synchronized的对象锁,锁标识位为10,其中指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的)

COPYObjectMonitor() {
    _header       = NULL;
    _count        = 0; //记录个数
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSe t集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。如下图所示

img

由此看来,monitor对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时也是notify/notifyAll/wait等方法存在于顶级对象Object中的原因(关于这点稍后还会进行分析),ok~,有了上述知识基础后,下面我们将进一步分析synchronized在字节码层面的具体语义实现。

Class Metadata Address

类型指针,即是对象指向它的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

Array length

如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据。

底层原理

代码块底层原理

关于synchionized代码块底层原理和synchionized方法底层原理,这里通过利用javap直观的展现加了synchionized后,我们的代码到底出现了些什么指令来观察

反编译代码

现在我们重新定义一个synchronized修饰的同步代码块,在代码块中操作共享变量i,如下:

COPYpublic class SyncCodeBlock {

   public int i;

   public void syncTask(){
       //同步代码块
       synchronized (this){
           i++;
       }
   }
}

编译上述代码并使用javap反编译后得到字节码如下(这里我们省略一部分没有必要的信息):

COPYClassfile /***/src/main/java/com/zejian/concurrencys/SyncCodeBlock.class
  Last modified 2018-07-25; size 426 bytes
  MD5 checksum c80bc322c87b312de760942820b4fed5
  Compiled from "SyncCodeBlock.java"
public class com.hc.concurrencys.SyncCodeBlock
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
  //........省略常量池中数据
  //构造函数
  public com.hc.concurrencys.SyncCodeBlock();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 7: 0
  //===========主要看看syncTask方法实现================
  public void syncTask();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter  //注意此处,进入同步方法
         4: aload_0
         5: dup
         6: getfield      #2             // Field i:I
         9: iconst_1
        10: iadd
        11: putfield      #2            // Field i:I
        14: aload_1
        15: monitorexit   //注意此处,退出同步方法
        16: goto          24
        19: astore_2
        20: aload_1
        21: monitorexit //注意此处,退出同步方法
        22: aload_2
        23: athrow
        24: return
      Exception table:
      //省略其他字节码.......
}
SourceFile: "SyncCodeBlock.java"

我们主要关注字节码中的如下代码

COPY3: monitorenter  //进入同步方法
//..........省略其他  
15: monitorexit   //退出同步方法
16: goto          24
//省略其他.......
21: monitorexit //退出同步方法
锁的竞争模拟

通过反编译解读–同步代码块多线程下关于锁的竞争模拟

  1. 首先从字节码中可知同步语句块的实现使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置.
  2. 当执行monitorenter指令时,当前线程将试图获取 objectref(即对象锁) 所对应的 monitor 的持有权,当 objectref 的 monitor 的进入计数器为 0,那线程可以成功取得 monitor,并将计数器值设置为 1,取锁成功。
  3. 如果当前线程已经拥有 objectref 的 monitor 的持有权,那它可以重入这个 monitor (关于重入性稍后会分析),重入时计数器的值也会加 1。
  4. 倘若其他线程已经拥有 objectref 的 monitor 的所有权,那当前线程将被阻塞,直到正在执行线程执行完毕,即monitorexit指令被执行,执行线程将释放 monitor(锁)并设置计数器值为0 ,其他线程将有机会持有 monitor 。
  5. 值得注意的是编译器将会确保无论方法通过何种方式完成,方法中调用过的每条 monitorenter 指令都有执行其对应 monitorexit 指令,而无论这个方法是正常结束还是异常结束。为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令。从字节码中也可以看出多了一个monitorexit指令,它就是异常结束时被执行的释放monitor 的指令。

方法底层原理

方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。

JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。

当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词),然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。

在方法执行期间,执行线程持有了monitor,其他任何线程都无法再获得同一个monitor。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法之外时自动释放。

反编译代码

下面我们看看字节码层面如何实现:

COPYpublic class SyncMethod {

   public int i;

   public synchronized void syncTask(){
           i++;
   }
}

使用javap反编译后的字节码如下:

COPYClassfile /***/src/main/java/com/zejian/concurrencys/SyncMethod.class
  Last modified 2017-6-2; size 308 bytes
  MD5 checksum f34075a8c059ea65e4cc2fa610e0cd94
  Compiled from "SyncMethod.java"
public class com.hc.concurrencys.SyncMethod
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool;

   //省略没必要的字节码
  //==================syncTask方法======================
  public synchronized void syncTask();
    descriptor: ()V
    //方法标识ACC_PUBLIC代表public修饰,ACC_SYNCHRONIZED指明该方法为同步方法
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2                  // Field i:I
         5: iconst_1
         6: iadd
         7: putfield      #2                  // Field i:I
        10: return
      LineNumberTable:
        line 12: 0
        line 13: 10
}
SourceFile: "SyncMethod.java"

从字节码中可以看出,synchronized修饰的方法并没有monitorenter指令和monitorexit指令,取得代之的确实是ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法,JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

这便是synchronized锁在同步代码块和同步方法上实现的基本原理的区别。同时我们还必须注意到的是在Java早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的,而操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的synchronized效率低的原因。

本文由传智教育博学谷狂野架构师教研团队发布。

如果本文对您有帮助,欢迎关注点赞;如果您有任何建议也可留言评论私信,您的支持是我坚持创作的动力。

转载请注明出处!

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

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

相关文章

TypeScript 使用 ES6 解构骚操作

TypeScript 使用 ES6 解构骚操作 文章目录TypeScript 使用 ES6 解构骚操作一、TypeScript 对象解构二、TypeScript 函数参数解构四、参考资料&#x1f498;五、推荐博文&#x1f357;一、TypeScript 对象解构 我们都知道 ES6 的数据解构功能很强大&#xff0c;一行命令就能够声…

HTMLCSS常见问题解决

文章目录一、解决img图片底部空白问题1、出现问题的效果2、原理3、解决方式3.1、将图片变为盒子3.2、处理基线3.3、把上级元素的字体大小改成0像素二、解决给子元素设置margin-top父子盒子都向下移动问题1、出现问题的效果2、原理3、解决方式3.1、给父元素设置边框3.2、给父元素…

程序员多赚20k的接私活必备网站

为什么都是程序员&#xff0c;就有人能多赚20k&#xff1f;那是因为副业搞得那么溜啊&#xff01; 今天分享一些程序员搞钱必备的接私活网站&#xff0c;让更多程序员们在工作之余能有另外一份收入。 1.程序员客栈&#xff1a;http://proginn.com 专为程序员服务的软件外包对…

跨境电商代购系统演示说明

首先来看什么是淘宝代购淘宝代购是近年兴起的一种购物模式&#xff0c;是帮国外客户购买中国商品。主要是通过万邦 科技的外贸代购系统&#xff0c;把淘宝、天猫等电商平台的全站商品通过API 接入到你的网站 上&#xff0c;瞬间就可以架设一个有数亿产品的大型网上商城&#xf…

FATE联邦学习centos成功部署

官方文档&#xff1a;https://fate.readthedocs.io/en/latest/deploy/standalone-deploy/#1-description。 我用的文档中的Standalone的第二种安装方式&#xff0c;没用docker。 安装过程 文档上写着确定版本 export version1.7.0但是你别真的用1.7.0啊&#xff01; &#…

ATS认证教学

我用的版本是ATS7.11、系统版本是用最新的ios13.2.1 定义 ATS旨在分析通过UART、USB和蓝牙传输传输的iAP流量、通过USB和无线&#xff08;蓝牙和Wi-Fi&#xff09;传输的CarPlay流量、通过Wi-Fi传输的AirPlay 2流量以及闪电音频流量。 ATS是Apple’s Accessory Test System的…

Cookie、Session、JWT 那些事

文章目录前言一、概念1、Cookie&#xff1a;2、Session&#xff1a;3、JWT二、应用1. 基本使用2. 实现 “退出” 功能总结前言 目前 C/S 模式盛行&#xff0c;HTTP 是其中最常见的通信协议&#xff0c;我们知道 HTTP 协议是无状态的&#xff0c;但是这场景完全不够用。 比如&…

让物流园区可视可控,顺丰供应链与亚马逊云科技的供应链新解法

导读&#xff1a;物流园区如何破解供应链断点&#xff1f;在物流园区附近&#xff0c;我们经常看到周边道路停满了集装箱卡车。这是物流园区的一个典型痛点&#xff0c;由于园区内部业务情况的不可见性&#xff0c;司机们往往到了园区才被告知业务繁忙&#xff0c;需要长时间排…

字符串反转-课后程序(JAVA基础案例教程-黑马程序员编著-第九章-课后作业)

【案例9-2】 字符串反转 【案例介绍】 1.案例描述 在使用软件或浏览网页时&#xff0c;总会查询一些数据&#xff0c;查询数据的过程其实就是客户端与服务器交互的过程。用户&#xff08;客户端&#xff09;将查询信息发送给服务器&#xff0c;服务器接收到查询消息后进行处…

中国版ChatGPT高潮即将到来,解密ChatGPT底层网络架构

2022年11月30日人工智能研究实验室OpenAI发布全新聊天机器人ChatGPT&#xff0c;在中国用户无法访问的前提下&#xff0c;上线仅两个月月活用户就突破了1亿。ChatGPT如同重磅炸弹&#xff0c;一时间火遍全球。面对这一万亿级市场机遇&#xff0c;在国内&#xff0c;无论是资本方…

2.28性能测试概念

一.自动化测试的亮点1)使用注解:避免生成过多对象,造成资源和时间的浪费2)通过static修饰静态变量,全局只创建了一次驱动对象,在测试前再卸载驱动.避免重复创建驱动对象造成时间和资源的浪费3)通过使用参数化,保持用例的简洁,提高了代码的可读性.4)使用测试套件:一次性执行所有…

2022年AI顶级论文 —生成模型之年(下)

CV - 计算机视觉 | ML - 机器学习 | RL - 强化学习 | NLP 自然语言处理 过去十年来&#xff0c;人工智能技术在持续提高和飞速发展&#xff0c;并不断冲击着人类的认知。 2012年&#xff0c;在ImageNet图像识别挑战赛中&#xff0c;一种神经网络模型&#xff08;AlexNet&am…

流媒体付服务器 ZLMediaKit 学习记录

1.官方github&#xff1a;ZLMediaKit 依赖于 media-server 库 #国内用户推荐从同步镜像网站gitee下载 git clone --depth 1 https://gitee.com/xia-chu/ZLMediaKit cd ZLMediaKit #千万不要忘记执行这句命令 git submodule update --init 之后 cd ZLMediaKit mkdir build…

嵌入式软件自动化测试的趋势

现在软件市场越来越推崇敏捷开发和持续交付&#xff0c;要在这样的环境下取得竞争优势&#xff0c;各个企业必须得开发出稳健的应用程序&#xff0c;为用户提供无可比拟的直观体验。而且,这些应用程序还要能够达到组织机构的业务目标。 自动化测试的重要意义 在这个竞争日益激…

和钟南山院士合影,被人民日报缅怀,吴孟达骨灰葬在马来西亚成谜

不知不觉当中&#xff0c;香港著名演员吴孟达&#xff0c;已经离开人世间两年了&#xff0c;可是关于他的话题却依然没有停息。在影迷的记忆里&#xff0c;吴孟达是星爷的黄金搭档&#xff0c;两个人曾经一起携手&#xff0c;拍摄出来很多部优秀的影视剧。 时光荏苒&#xff0c…

Revit中复合墙图层的规则和CAD识别翻模墙

一、Revit中用于指定复合墙图层的规则&#xff0c;具体内容? 在编辑复合墙的结构时&#xff0c;请使用“指定图层”工具将“编辑部件”对话框中的行指定给图层或预览窗格中的区域&#xff0c;并遵循这些原则。 在预览窗格中&#xff0c;样本墙的各个行必须保持从左到右的顺序显…

面试了1个自动化测试,开口40W年薪,只能说痴人做梦...

公司前段缺人&#xff0c;也面了不少测试&#xff0c;结果竟然没有一个合适的。一开始瞄准的就是中级的水准&#xff0c;也没指望来大牛&#xff0c;提供的薪资在10-20k&#xff0c;面试的人很多&#xff0c;但平均水平很让人失望。看简历很多都是3年工作经验&#xff0c;但面试…

Docker前端工程npm平滑过渡到pnpm v7的姿势

前言 pnpm挺多优点的&#xff0c;比如安装依赖速度很快&#xff0c;命令行也可以少打几个字符&#xff1b; 标题为啥说平滑&#xff0c;就是尽可能的少破坏性的迁移【针对现有的工程的改造】&#xff0c; 但是又能受用到pnpm的部分特性&#xff0c;使其效益符合我们的期望即可…

RK3588平台开发系列讲解(系统篇)init.d介绍

平台内核版本安卓版本RK3588Linux 5.10Android 12文章目录 一、Linux启动简介二、sysvinit配置三、inid.d介绍沉淀、分享、成长,让自己和他人都能有所收获!😄 📢本篇介绍init.d相关知识。 一、Linux启动简介 Linux用户空间启动时,第一个会启动init进程,用来引导启动其…

0101基础-认证授权-springsecurity

文章目录1 基础概念1.1 认证1.2 会话1.3 jwt1.4 授权2 授权的数据模型3 RBAC3.1 基于角色的访问控制3.2 基于资源的访问控制4 名词解析4.1 SSO4.2 CAS4.3 联合登陆4.4 多端登录&#xff1a;同一账号不同终端登录4.5 OAuth1 基础概念 1.1 认证 认证是为了保护系统的隐私数据和…