从根本上理解Synchronized的加锁过程

news/2024/4/25 9:04:14/文章来源:https://blog.csdn.net/Edwin_Hu/article/details/127625138

伸手摘星,即使一无所获,亦不致满手污泥

请关注公众号:星河之码

作为一个Java开发,对于Synchronized这个关键字并不会陌生,无论是并发编程,还是与面试官对线,Synchronized可以说是必不可少。

在JDK1.6之前,都认为Synchronized是一个非常笨重的锁,就是在之前的《谈谈Java中的锁》中提到的重量级锁。但是在JDK1.6对Synchronized进行优化后,Synchronized的性能已经得到了巨大提升,也算是脱下了重量级锁这一包袱。本文就来看看Synchronized的使用与原理。

JDK1.6后优化点:

锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等技术来减少锁操作的开销,synchronized的并发性能已经基本与J U C包提供的Lock持平

一、Synchronized的使用

在Java中,synchronized有:【修饰实例方法】、【修饰静态方法】、【修饰代码块】三种使用方式,分别锁住不同的对象。这三种方式,获取不同的锁,锁定共享资源代码段,达到互斥(mutualexclusion)效果,以此保证线程安全。

共享资源代码段又被称之为临界区,锁的作用就是保证临界区互斥,即同一时间临界区的只能有一个线程执行,其他线程阻塞等待,排队等待前一个线程释放锁

1.1 修饰实例方法

作用于当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁

public synchronized void methodA() {System.out.println("作用于当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁");
}

1.2 修饰静态方法

给当前类加锁,作用于当前类的所有对象实例,进入同步代码前要获得 当前 class 的锁

当被static修饰时,表明被修饰的代码块或者变量是整个类的一个静态资源,属于类成员,不属于任何一个实例对象,也就是说不管 new 了多少个对象,都只有一份,

public synchronized static void methodB() {System.out.println("给当前类加锁,作用于当前类的所有对象实例,进入同步代码前要获得 **当前 class 的锁**。");
}

如果一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,是不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。

1.3 修饰代码块

指定加锁对象,对给定对象/类加锁

  • synchronized(this|object) 表示进入同步代码库前要获得给定对象的锁

    public void methodc() {synchronized(this) {System.out.println("锁住的是当前对象,对象锁");}
    }
    
  • synchronized(类.class) 表示进入同步代码前要获得 当前 class 的锁

    javapublic void methodd() {synchronized(SynchronizedDome.class) {System.out.println("给当前类加锁,SynchronizedDome class 的锁");}
    }
    

二、Synchronized案例说明

了解了Synchronized的使用方法以后,接下来结合案例的方式,来详细看看Synchronized的加锁,多线程下是怎么执行的,我这里将按照上面三个使用方法来分别使用案例描述

2.1 修饰实例方法案例

  • 案例说明

    两个线程同时对一个共享变量sum进行累加3000,输出其最终结果,我们期望的结果最终应该是6000,接下来看看不加Synchronized修饰和加Synchronized修饰的情况下分别输出什么。

2.1.1 案例

  • 不加Synchronized修饰

    package com.upup.edwin.sync;public  class SynchronizedDome {//定义来了一个共享变量public static int sum = 0;// 进行累加3000次public void add() {for (int i = 0; i < 3000; i++) {sum = sum + 1;}}public static void main(String[] args) throws InterruptedException {SynchronizedDome dome = new SynchronizedDome();Thread thread1 = new Thread(() -> dome.add());Thread thread2 = new Thread(() -> dome.add());thread1.start();thread2.start();//join()  方法是让main线程等待线程 thread1 和thread2 都执行完成之后在继续执行下面的输出thread1.join();thread2.join();System.out.println("两个线程执行完成之后的累加结果:sum = " + sum);}}

    从结果上看,当我们不加synchronized修饰的时候,输出结果并不是我们锁期待的6000,这说明两个线程之间在执行的时候相互干扰了,也就是线程不安全

  • 加Synchronized修饰

    我们对上面的add方法进行改造,在方法上加上synchronized关键字,也就是加上了锁,来看看它的执行结果

    // 进行累加3000次
    public synchronized void add() {for (int i = 0; i < 3000; i++) {sum = sum + 1;}
    }
    

    加上synchronized修饰后,发现输出结果与我们预期的是一致的,说明加上锁,两个线程是排队顺序执行的

2.1.2 案例执行过程

通过以上的两个案例对比,可以发现在synchronized修饰方法的时候,能够让结果正常输出,保证了线程安全,那么它是怎么做到的吗,两个线程的执行过程是怎么样的呢?

在前面我们提到了,当synchronized修饰实例方法的时候获取的是当前对象实例的锁,我们在代码中new出了一个SynchronizedDome对象,因此本质上锁住的是这个对象

SynchronizedDome dome = new SynchronizedDome();

所有线程要执行同步函数都要先获取锁(synchronized里面叫做监视器锁),获取到锁的线程才能执行同步函数,没有获取到的线程只能等待,抢到锁的线程执行完同步函数后会释放锁并通知唤醒其他等待的线程再次获取锁。流程如下

2.2 修饰静态方法案例

修饰静态方法与修饰实例方法基本一致,唯一的区别就是锁的不是当前对象,而是整个Class对象。我们只需要把上述案例中同步函数改成静态的就可以了

package com.upup.edwin.sync;public  class SynchronizedDome {//定义来了一个共享变量public static int sum = 0;// 进行累加3000次public synchronized static void add() {for (int i = 0; i < 3000; i++) {sum = sum + 1;}}public static void main(String[] args) throws InterruptedException {Thread thread1 = new Thread(() -> add());Thread thread2 = new Thread(() -> add());thread1.start();thread2.start();//join()  方法是让main线程等待线程 thread1 和thread2 都执行完成之后在继续执行下面的输出thread1.join();thread2.join();System.out.println("两个线程执行完成之后的累加结果:sum = " + sum);}}

可以看到当我们改成静态方法之后,就不需要在main方法中new SynchronizedDome()了,直接调用add即可,这也说明锁的不是当前对象了,

我们知道在Java中静态资源是属于Class的,不属于任何一个实例对象,而每个Class对象在Jvm中都是唯一的,所以我们锁住Class对象后,其他线程无法获取其静态资源了,从而进入等待阶段,本质上,锁住静态资源的执行过程与锁住实例方法的执行过程是一致的,只是锁的对象不一样而已

2.3 修饰代码块案例

静态资源锁Class,实例方法锁对象,还有一种就是锁住一个方法的某一段代码,也就是代码块。比如我们在上述的add 方法中调用了一个print方法

 public static void print(){try {Thread.sleep(5000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("这是一个不需要加锁的方法,当前执行的线程是 : " + Thread.currentThread().getName());}

如上,print就是睡眠了5秒钟后输出一句话,不涉及到线程安全问题,如果使用synchronized修饰整个Add方法,并且在add中调用 print(),如下

public synchronized static void add() {print();for (int i = 0; i < 3000; i++) {sum = sum + 1;}
}

这种方式synchronized就会把add()整个包裹,使整个程序执行时间变长,完整案例如下

package com.upup.edwin.sync;public  class SynchronizedDome {//定义来了一个共享变量public static int sum = 0;// 进行累加3000次public synchronized static void add() {print();for (int i = 0; i < 3000; i++) {sum = sum + 1;}}// 可以异步执行的方法public static void print(){try {Thread.sleep(5000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("这是一个不需要加锁的方法,当前执行的线程是 : " + Thread.currentThread().getName());}public static void main(String[] args) throws InterruptedException {long l1 = System.currentTimeMillis();Thread thread1 = new Thread(() -> add());Thread thread2 = new Thread(() -> add());thread1.start();thread2.start();//join()  方法是让main线程等待线程 thread1 和thread2 都执行完成之后在继续执行下面的输出thread1.join();thread2.join();long l2 = System.currentTimeMillis();System.out.println("两个线程执行完成的时间是:l2 - l1 = " + (l2 - l1) + " 毫秒");System.out.println("两个线程执行完成之后的累加结果:sum = " + sum);}}

以上案例执行结果如下

很明显,两个线程在排队执行Add方法时,连print方法一起等待,但是实际上print是一个线程安全的方法,不需要获取锁,并且print方法还比较耗时,这就拖慢了整个程序的执行总时长,其执行过程如下

这种方式会将线程安全的方法也锁住,导致排队执行代码变多,时间变长,其本质就是synchronized锁住的是整个Add方法,粒度比较大,我们可以对add进行改造一下,让它只锁累计的那一段代码

public static void add() {print();synchronized(SynchronizedDome.class){for (int i = 0; i < 3000; i++) {sum = sum + 1;}}
}

如上,synchronized只锁了这for循环段代码,print()是可以并行执行的,这样就可以提升整个方法的执行效率,完整代码如下

package com.upup.edwin.sync;public  class SynchronizedDome {//定义来了一个共享变量public static int sum = 0;// 进行累加3000次public static void add() {print();synchronized(SynchronizedDome.class){for (int i = 0; i < 3000; i++) {sum = sum + 1;}}}// 可以异步执行的方法public static void print(){try {Thread.sleep(5000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("这是一个不需要加锁的方法,当前执行的线程是 : " + Thread.currentThread().getName());}public static void main(String[] args) throws InterruptedException {long l1 = System.currentTimeMillis();Thread thread1 = new Thread(() -> add());Thread thread2 = new Thread(() -> add());thread1.start();thread2.start();//join()  方法是让main线程等待线程 thread1 和thread2 都执行完成之后在继续执行下面的输出thread1.join();thread2.join();long l2 = System.currentTimeMillis();System.out.println("两个线程执行完成的时间是:l2 - l1 = " + (l2 - l1) + " 毫秒");System.out.println("两个线程执行完成之后的累加结果:sum = " + sum);}}

修改之后,整个方法的执行时间只有5秒多,我们休眠的时间也是5秒,说明两个线程是一起进入的休眠,并不是排队的,其执行过程如下

在上述案例中我使用的是锁住整个class的方法:‘synchronized(SynchronizedDome.class)’,如果要改成锁住对象只需要改成’synchronized(this)'即可。其他执行流程都是一样的,只是获取的锁不一样

三、Synchronized原理剖析

以Hotspot(是Jvm的一种实现)为例,在Jvm中每个Class都有一个对象,对象又由 【对象头 + 实例数据 + 对齐填充(java对象必须是8byte的倍数)】三部分组成,每个对象都有一个对象头,synchronized的锁就是存在对象头中的

3.1 对象头

既然synchronized的锁是存在对象头中的,那就先来了解一下对象头,Hotspot 有两种对象头:

  • 数组类型:如果对象是数组类型,则虚拟机用3字节存储对象头
  • 非数组类型:如果对象是非数组类型,则用2字节存储对象头

一般对象头由两部分组成

  • Mark Word

    存储自身的运行时数据,比如:对象的HashCode,分代年龄和锁标志位信息

    Mark Word存储的信息与对象自身定义无关,所以Mark Word是一个一个非固定的数据结构,Mark Word里存储的数据会在运行期间随着锁标志位的变化而变化。

  • Klass Pointer

    类型指针指向它的类元数据的指针。

Mark Word在不同的虚拟机下的bit位不一样,以下是32位与64位虚拟机的对比图


3.2 Monitor

在了解Monitor之前,先思考一个问题,前面我们说synchronized修饰方法和代码块的时候,加锁业务流程的执行过程是一样的,那么他们内部加锁实现是不是一样的呢?

其实加锁过程肯定是不一样的,不然加锁过程一样,锁一样,加锁业务流程的执行过程是一样,那就没必要分成方法和代码块了

我们可以找到上述案例中的SynchronizedDome类的Class文件,然后再命令行中执行javap -c -s -v -l SynchronizedDome.class就可以看到编译后指令集。可以分别查看synchronized修饰方法和代码块指令集的区别

  • synchronized代码块

    上图中的指令集中有monitorenter、monitorexit两个指令,当synchronized修饰代码块时,JVM就是使用monitorenter和monitorexit两个指令实现同步的,当线程执行到monitorenter的时候要先获得monitor锁,才能执行后面的方法。当线程执到monitorexit的时候则要释放锁

  • synchronized修饰方法

上图中的指令集中有一个ACCSYNCHRONIZED标记,当synchronized修饰方法时,JVM通过在方法访问标识符(flags)中加入ACCSYNCHRONIZED来实现同步功能,当线程执行有ACCSYNCHRONIZED标志的方法,需要获得monitor锁。每个对象都与一个monitor相关联,线程可以占有或者释放monitor


3.1什么是Monitor锁

从上面的描述无论是修饰代码块还是修饰方法,都要获取一个Monitor锁,那么什么是Monitor锁呢?

Monitor即监视器,可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个Java对象就有一把看不见的锁,称为内部锁或者Monitor锁

Monitor锁与对象的关系图:

任何一个对象都有一个Monitor与之关联,当且一个Monitor被持有后,它将处于锁定状态。Synchronized在JVM中基于进入和退出Monitor对象,通过成对的MonitorEnter和MonitorExit指令来实现方法同步和代码块同步。

  • MonitorEnter

    插入在同步代码块的开始位置,当代码执行到该指令时,将会尝试获取该对象Monitor锁;

  • MonitorExit

    插入在方法结束处和异常处,JVM保证每个MonitorEnter必须有对应的MonitorExit,释放Monitor锁;

3.2 Monitor锁的工作原理

每一个对象都会有一个monitor锁,Monitor锁的MarkWord锁标识位为10,其中指针指向的是Monitor对象的起始地址

在Java虚拟机(HotSpot)中,Monitor是由ObjectMonitor实现的,ObjectMonitor中维护了一个锁池(EntryList)和等待池(WaitSet)

ObjectMonitor工作模型图如下:

ObjectMonitor工作模型图大致描述了以下几个步骤

  • 所有新的线程都会进入(①号入口)EntryList中去竞争锁

  • 当有线程通过CAS把monitor的owner字段设置为自己时,说明这个线程获取到了锁,也就是进入图中的(②号入口)owner区域,其他线程进入阻塞状态

    如果当前线程是第一次进入该monitor,将recursions由0设置为1,_owner为当前线程,该线程成功获得锁并返回

    如果当前线程不是第一次进入该monitor,说明当前线程再次进入monitor,即重入锁,执行recursions ++ ,记录重入的次数

  • 如果获取到锁的线程(owner)执行了wait等方法,就会释放锁,并进入(③号入口)waitset中,于此同时通知waitset中其他线程重新竞争锁,获取到锁(④号入口)进入owner区域

  • 当线程执行完同步代码,会释放锁(由⑤号口出),于此同时通知waitset和EntryList中其他线程重新竞争锁

    释放锁线程执行monitorexitmonitor的进入数-1,执行过多少次monitorenter,最终要执行对应次数的monitorexit

四、Synchronized锁优化

在之前的《谈谈Java中的锁》中介绍了锁优化的一些基本原理, 接下来看看Synchronized的锁优化。锁优化主要包含:锁粗化、锁消除、锁升级三部分。

4.1 锁粗化

同步代码块要求我们将同步代码的范围尽量缩小,这样可以使同步的操作数量尽可能缩小,缩短阻塞时间,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁

比如上述案例add的循环中,如果将Synchronized防止for循环里面不是范围更小吗?

for (int i = 0; i < 3000; i++) {synchronized(SynchronizedDome.class){sum = sum + 1;}
}

这样虽然缩小了范围,但是未必缩短了时间,因为在加锁过程中也会消耗资源,如果频繁的加锁释放锁,可能会导致性能损耗

基于此,JVM会对这种情况进行锁粗化,锁粗化就是将【多个连续的加锁、解锁操作连接在一起】,扩展成一个范围更大的锁,避免频繁的加锁解锁操作

J V M在检测到上述for循环再频繁获取同一把锁的到时候,就会将加锁的范围粗化到循环操作的外部,使其只需要获取一次锁就可以,减小加锁释放锁的开销。

4.2 锁消除

Java虚拟机在JIT编译时,会进行逃逸分析(对象在函数中被使用,也可能被外部函数所引用,称为函数逃逸),通过对运行上下文的扫描,分析synchronized锁对象是不是只被一个线程加锁,不存在其他线程来竞争加锁的情况。这样就可以消除该锁了,提升执行效率。

锁消除的经典案例就是StringBuffer 了,StringBuffer 是线程安全的,其内部的append方法就是通过synchronized加锁的,源码如下

@Override
public synchronized StringBuffer append(String str) {toStringCache = null;super.append(str);return this;
}

当我们调用StringBuffer 的append时,就会加锁,但是当我们使用的对象经过逃逸分析后,认为该对象不会被其他线程共享的时候,就会将append方法的synchronized去掉,编译不加入monitorenter和monitorexit指令。比如下面这个方法

public static String appendStr(String str, int i) {StringBuffer sb= new StringBuffer();sb.append(str);sb.append(i);return sb.toString();
}

StringBuffer的append虽然是同步方法。但appendStr中的sb对象没有传递到方法外,不会被其他线程引用,不存在锁竞争的情况,因此可以进行锁消除

五、Synchronized锁升级

在之前的《谈谈Java中的锁》中介绍了锁的类型,其中介绍了无锁、偏向锁 、轻量级锁、 重量级锁等几种锁的实现。而我们常说的锁升级其实就是这几种锁的升级跃迁。锁升级过程:【无锁】—>【偏向锁】—>【轻量级锁】—>【 重量级锁】。

而锁的变化其实就是一个标志位的变化,在前面提到的对象头中Mark World时有提到它存储的就是对象的HashCode,分代年龄和锁标志位信息。因此锁的升级变化,本质上就是Mark World中锁标志位的变化。以上几种锁的标志位信息如下

锁状态存储内容存储内容
无锁对象的hashCode、对象分代年龄、是否是偏向锁(0)01
偏向锁偏向线程ID、偏向时间戳、对象分代年龄、是否是偏向锁(1)01
轻量级锁指向栈中锁记录的指针00
重量级锁指向互斥量(重量级锁)的指针10

注意:

锁可以升级不可以降级,但是偏向锁状态可以被重置为无锁状态!!!

锁可以升级不可以降级,但是偏向锁状态可以被重置为无锁状态!!!

锁可以升级不可以降级,但是偏向锁状态可以被重置为无锁状态!!!

5.1 无锁升级为偏向锁

  • 为啥要有偏向锁

    大多数情况下是一个线程多次获得同一个锁,不存在锁竞争的,而竞争锁会增大资源消耗,,为了降低获取锁的代价,才引入的偏向锁


当线程第一次执行到同步代码块的时候,锁对象变成就会偏向锁(通过CAS修改对象头里的锁标志位),其目标就是在只有一个线程执行同步代码块时,降低获取锁带来的消耗,提高性能

偏向锁是默认开启的,而且开始时间一般是比应用程序启动慢几秒,可以通过JVM配置成没有延迟

-XX:BiasedLockingStartUpDelay=0

可以通过J V M参数关闭偏向锁,关闭之后程序默认会进入轻量级锁状态

-XX:-UseBiasedLocking=false

无锁升级为偏向锁,其本质是判断对象头的Mark Word中线程ID与当前线程ID是否一致以及偏向锁标识,如果一致直接执行同步代码或方法,具体流程如下

  • 无锁状态,存储内容「是否为偏向锁(0)」,锁标识位01

    • CAS设置当前线程ID到Mark Word存储内容中,并且将是否为偏向锁0 修改为 是否为偏向锁1
    • Mark Word和栈帧中记录获取到偏向的锁的threadID
    • 执行同步代码或方法
  • 偏向锁状态,存储内容「是否为偏向锁(1)、线程ID」,锁标识位01

    • 对比线程ID是否一致,如果一致无需使用CAS来加锁、解锁,直接执行同步代码或方法

      因为偏向锁不会自动释放锁,因此后续线程A再次获取锁的时候,需要比较当前线程的threadID和Java对象头中的threadID是否一致

    • 如果不一致,CASMark Word的线程ID设置为当前线程ID,设置成功,执行同步代码或方法

      其他线程,如线程B要竞争锁对象,而偏向锁不会主动释放,因此Mark Word还是存储的线程A的threadID

      此时会检查Mark Word的线程A是否存活,如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程B)可以竞争将其设置为偏向锁;

    • CAS设置失败,证明存在多线程竞争情况,触发撤销偏向锁,当到达全局安全点,偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后在安全点的位置恢复继续往下执行。

      如果Mark Word的线程A是存活,则线程B的CAS会失败,此时会暂停线程A,撤销偏向锁,升级为轻量级锁,

5.2 偏向锁升级为轻量级锁

轻量级锁又称自旋锁,一般在竞争锁对象的线程比较少,持有锁时间也不长的场景中,由于阻塞线程、唤醒线程需要C P U从用户态转到内核态,时间比较长,如果同步代码块执行的时间比这更时间短,那就本末倒置了,所以这种情况一般不阻塞线程,让其自旋一段时间等待锁其他线程释放锁,通过自旋换取线程在用户态和内核态之间切换的开销

  • 锁竞争

    如果多个线程轮流获取一个锁,但是每次获取锁的时候没有发生阻塞,就不存在锁竞争。只有当某线程尝试获取锁的时候,发现该锁已经被占用,只能等待其释放,这才发生了锁竞争。


当前线程持有的锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能

升级为轻量级锁有两种情况:

  • 当关闭偏向锁功能时,会由无锁直接升级为轻量级锁
  • 多个线程竞争偏向锁导致偏向锁升级为轻量级锁

这两种情况下偏向锁升级为轻量级锁过程如下

  • 无锁状态:存储内容「是否为偏向锁(0)」,锁标识位01

    关闭偏向锁功能时

    • CAS设置当前线程栈中锁记录的指针到Mark Word存储内容
    • 锁标识位设置为00
    • 执行同步代码或方法
  • 轻量级锁状态:存储内容「线程栈中锁记录的指针」,锁标识位00

    • CAS设置当前线程栈中锁记录的指针到Mark Word存储内容,设置成功获取轻量级锁,执行同步块代码或方法
    • 设置失败,证明多线程存在一定竞争,线程自旋上一步的操作,自旋一定次数后还是失败,轻量级锁升级为重量级锁
    • Mark Word存储内容替换成重量级锁指针,锁标记位10

5.3 轻量级锁升级为重量级锁

轻量级锁在自旋一定次数之后还没获取到锁,就升级为重量级锁,重量级锁是依赖操作系统的MutexLock(互斥锁)来实现的,需要从用户态转到内核态,成本非常高,等待锁的线程都会进入阻塞状态,防止CPU空转

计数器记录自旋次数,默认允许循环10次,可以通过虚拟机参数更改

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

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

计算机视觉与图形学-神经渲染专题-非刚体NeRF II

《TiNeuVox:Fast Dynamic Radiance Fields with Time-Aware Neural Voxel》链接&#xff1a;https://jaminfong.cn/tineuvox/摘要作者通过表示具有时间感知体素特征的场景提出了一个辐射场框架&#xff0c;并将其命名为 TiNeuVox。作者引入了一个微小的坐标变形网络来模拟粗略的…

Day 04 - Composition API_ref reactive 函数

1.ref函数 作用: 定义一个响应式的数据&#xff1b; 语法: const xxx ref(initValue) 创建一个包含响应式数据的引用对象&#xff08;reference对象&#xff0c;简称ref对象&#xff09;。 JS中操作数据&#xff1a; xxx.value 模板中读取数据: 不需要.value&#xff0c;直…

Vue CLI系列之生成打包报告

文章の目录一、通过命令行参数的形式生成报告二、通过可视化的UI面板直接查看报告写在最后打包时&#xff0c;为了直观地发现项目中存在的问题&#xff0c;可以在打包时生成报告。生成报告的方式有两种&#xff1a; 一、通过命令行参数的形式生成报告 "scripts": {…

【Axure高保真原型】移动端钱包原型模板

今天和大家分享移动端钱包的原型模板&#xff0c;里面包含了11大模块&#xff0c;各个模块都是高保真高交互的原型模板&#xff0c;大家可以在演示地址里体验哦 【原型预览及下载地址】 https://axhub.im/ax9/4c3757a85d201a4c/#c1 这个原型还可以在手机上演示哦&#xff0c…

【精准三点定位求解汇总】利用Python或JavaScript高德地图开放平台实现精准三点定位(经纬度坐标与平面坐标转换法求解、几何绘图法求解)

【精准三点定位求解汇总】利用Python或JavaScript高德地图开放平台实现精准三点定位&#xff08;经纬度坐标与平面坐标转换法求解、几何绘图法求解&#xff09; 众所周知&#xff0c;如果已知三个点的坐标&#xff0c;到一个未知点的距离&#xff0c;则可以利用以距离为半径画…

JavaScript的原型链

JavaScript的原型链 JavaScript的继承主要是通过原型链实现的&#xff0c;所以理解原型链是掌握JavaScript继承的关键一环。原型链的继承的基本思想是通过原型链继承多个引用类型的属性和方法。 理解原型链 关于原型链的定义与理解&#xff1a; 每个构造函数都有一个原型对…

Sentinel流控

Sentinel 随着微服务的流行&#xff0c;服务和服务之间的稳定性变得越来越重要。 Sentinel 以流量为切入点&#xff0c;从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。 1.sentinel特性 Sentinel 具有以下特征: 丰富的应用场景&#xff1a; Sentinel 承接了…

【折腾服务器 3】群晖学习版中安装 Active Backup for Business 及相关配置 =)

Catch UP 书接上回&#xff0c;在 ESXi 中安装了群晖系统&#xff0c;这个系统主要是用来给 Windows 物理机做备份的&#xff0c;因此在本片主要讲解如何配置 Active Backup for Business 软件。 Chapter 1 设置存储空间 上一篇博客中&#xff0c;安装群晖时分配了一个 32GB…

【高精度定位】关于GPS、RTK、PPK三种定位技术的探讨

高精度定位通常是指亚米级、厘米级以及毫米级的定位&#xff0c;从市场需求来看&#xff0c;定位的精度越高往往越好。“高精度、低成本”的定位方案无疑将是未来市场的趋势。 在物联网时代&#xff0c;大多数的应用或多或少都与位置服务相关联&#xff0c;尤其是对于移动物体而…

「 理财与风险控制|养老系列」你想象中的高端养老社区是什么样?

本文主要介绍为什么养老规划需要考虑养老社区的部分&#xff0c;当前市场上养老社区的各种现状&#xff0c;养老社区从各个角度分类&#xff0c;选择养老社区需要关注的要素以及保险保单能够提供的养老权益是怎样的 文章目录01 为什么要关注养老社区&#xff1f;02 为什么关注高…

【docker常用命令】

一、帮助启动类命令 &#xff08;1&#xff09;启动docker systemctl start docker&#xff08;2&#xff09;停止docker systemctl stop docker&#xff08;3&#xff09;重启docker systemctl restart docker&#xff08;4&#xff09;查看docker状态 systemctl status…

移动端测试必备技能: adb命令和抓包

移动端测试 是指对移动应用进行的测试&#xff0c;即实体的特性满足需求的程度&#xff0c;进行测试前需要搭建测试环境。 1 移动端自动化环境搭建 1.1 java安装 java JDK 安装jdk-8u181-windows-x64.exe 配置环境变量&#xff1a; JAVA_HOME&#xff1a;D:\developer to…

【c++】STL--vector

前言 想必大家已经对string有所了解了&#xff0c;string是专门用于字符串的。今天讲到的vector则是表示可变大小数组的序列容器。就像数组一样&#xff0c;vectoer也采用的连续存储空间来存储元素。也就是意味着可以采用下标对vector的元素进行访问&#xff0c;和数组…

SpringBoot系列之自动装配原理详解

文章目录前言一、SpringBoot自动配置-Condition-11、观察spring自动创建bean过程2、创建自定义bean对象3、根据条件创建自定义bean二、 SpringBoot自动配置-Condition-2三、SpringBoot自动配置-切换内置web服务器1、查看继承关系图2、shiftdelete 排除Tomcat四、SpringBoot自动…

12.20工作学习记录 力扣 罗马文转数字

每日一题:罗马文转数字 定义两个指针 不断后移 每一次让前一个指针的值累加为sum 最后返回sum 力扣https://leetcode.cn/problems/roman-to-integer/ 最长公共前缀 主要是subString方法 力扣https://leetcode.cn/problems/longest-common-prefix/solutions/现在分词与形容…

圣诞的荒诞小故事并记录互联网协议-五层模型

今天敲代码敲着敲着灵光乍现&#xff0c;突然一个荒诞的故事&#x1f4a1;映入脑海。 1.未来和过去&#xff1a; 人高度发达&#xff08;以下称之为渡&#xff09; 渡可以打开时空穿越过去&#xff08;以下称之为旧迹&#xff09;&#xff0c;并且可以进随心所欲的来去自如&a…

基于yolov5s实践国际象棋目标检测模型开发

在我前面的一篇文章中讲解实现了基于改进的yolov5s-spd模型实现了五子棋目标对象检测模型系统的设计开发&#xff0c;这里紧接前文&#xff0c;突发奇想&#xff0c;是否可以借鉴同样的思路实现象棋的检测模型开发呢&#xff1f;理论上面肯定是可以的&#xff0c;但是实际效果如…

详细介绍关于自定义类型:结构体、枚举、联合【c语言】

文章目录结构体结构体的声名特殊的声明结构成员的类型结构的自引用结构体变量的定义和初始化结构体内存对齐修改默认对齐数结构体变量访问成员结构体传参结构体实现位段&#xff08;位段的填充&可移植性&#xff09;位段的内存分配位段的跨平台问题枚举枚举类型的定义枚举的…

微信小程序入门

目录 一&#xff0c;简介 二&#xff0c;小程序开发环境搭建 1.申请账号 2.安装开发工具 3.小程序工具使用 三&#xff0c;目录结构以及json配置 1.目录结果 2.json配置 3.JSON 语法 4.WXML 5.wxss 6.JS 逻辑交互 四&#xff0c;小程序宿主环境 1.程序与页面 2.组件…

String 字符串

String 基本介绍 String 应该是 Java 中最常用的一个对象&#xff0c;他不是八种基本数据类型的其中之一&#xff0c;但是随便翻了一下项目代码&#xff0c;用 String 定义的变量超过百分之八十。 public final class Stringimplements java.io.Serializable, Comparable<…