Synchronized的锁升级过程是怎样的?

文章目录

  • 一、Synchronized的使用
    • 1、修饰实例方法
    • 2、修饰静态方法
    • 3、修饰代码块
    • 4、总结:
  • 二、Monitor
    • 1、Java对象头
      • 1.1 32 位虚拟机的对象头
      • 1.2 64位虚拟机的对象头
    • 2、Mark Word 结构
    • 3、Moniter
    • 4、Synchronized 字节码
    • 5、轻量级锁
    • 6、锁膨胀
    • 7、自旋优化
    • 8、偏向锁
    • 9、偏向锁的撤销
      • 9.1 hashcode
      • 9.2 其它线程使用对象
      • 9.3 调用 wait/notify
    • 10、批量重偏向、撤销
    • 11、锁消除

一、Synchronized的使用

Java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们是有区别的:

  • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
  • 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点

synchronized 关键字的使用方式主要有下面 3 种

  • 修饰实例方法

  • 修饰静态方法

  • 修饰代码块

1、修饰实例方法

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

synchronized void method() {//业务代码
}


2、修饰静态方法

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

这是因为静态成员不属于任何一个实例对象,归整个类所有,不依赖于类的特定实例,被类的所有实例共享。

synchronized static void method() {//业务代码
}

静态 synchronized 方法和非静态 synchronized 方法之间的调用不互斥

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


3、修饰代码块

对括号里指定的对象/类加锁:

  • synchronized(object) 表示进入同步代码库前要获得 给定对象的锁
  • synchronized(类.class) 表示进入同步代码前要获得 给定 Class 的锁
synchronized(this) {//业务代码
}


4、总结:

  • synchronized 关键字加到 static 静态方法和 synchronized(class) 代码块上都是是给 Class 类上锁;
  • synchronized 关键字加到实例方法上是给对象实例上锁;
  • 尽量不要使用 synchronized(String a) ,因为 JVM 中,字符串常量池具有缓存功能,多个线程使用相同的字符串值,实际使用的是同一个对象



二、Monitor

Java对象由三部分组成

  • 对象头
  • 对象体:对象体里放的是非静态的属性,也包括父类的所有非静态属性(private修饰的也在这里,不区分可见性修饰符),基本类型的属性存放的是具体的值,引用类型及数组类型存放的是引用指针。
  • 对齐填充

1、Java对象头

1.1 32 位虚拟机的对象头

普通对象

Mark Word :存储对象自身的运行时数据,hashCode、gc年龄以及锁信息等

Klass Word :指向Class对象

数组对象

相对于普通对象多了记录数组长度

所以对于一个int类型整数来说,它占用4字节,而一个Integer对象,在32位虚拟机中包含了8字节对象头,4字节数据,一共12字节,加上内存对齐,就是16字节



1.2 64位虚拟机的对象头

  • Markword:存储对象自身运行时数据如hashcode、gc分代年龄及锁信息等,64位系统总共占用8个字节。
  • 类型指针:对象指向类元数据地址的指针,jdk8默认开启指针压缩,64位系统占4个字节
  • 数组长度:若对象不是数组,则没有该部分,不分配空间大小,若是数组,则为4个字节长度



2、Mark Word 结构

32位虚拟机

64位虚拟机

  • 对象的hashCode占31位,重写类的hashCode方法返回int类型,只有在无锁情况下,在有调用的情况下会计算该值并写到对象头中,其他情况该值是空的。
  • 分代年龄占4位,最大值也就是15,在GC中,当survivor区中对象复制一次,年龄加1,默认是到15之后会移动到老年代。
  • 是否偏向锁占1位,无锁和偏向锁的最后两位都是01,使用这一位来标识区分是无锁还是偏向锁。
  • 锁标志位占2位,锁状态标记位,同是否偏向锁标志位标识对象处于什么锁状态。
  • 偏向线程ID占54位,只有偏向锁状态才有,这个ID是操作系统层面的线程唯一id,跟java中的线程id是不一致的


3、Moniter

Moniter称为监视器或者管程,是操作系统提供的对象

每个Java对象都可以关联一个Moniter对象,如果使用synchronized给对象上锁(重量级),该对象的Mark Word中就被设置指向Moniter对象的指针


Moniter结构

  • 刚开始Moniter中Owner为null
  • 当Thread-2执行synchronized(obj)后,就会将Moniter的所有者Owner置位Thread-2,Moniter只能有一个Owner
    • obj对象的MarkWord中最初保存的是对象的hashcode、gc年龄等信息,同时锁标志位为01,表示无锁。当获取锁后,会将这些信息保存在Moniter对象中,然后MarkWord存储的就是指向Moniter的指针,锁标志位为10(重量级锁)
  • 在Thread-2上锁的过程中,如果Thread-1、Thread-3也来执行synchronized(obj),就会进入EntryList,处于BLOCKED状态
  • Thread-2执行完同步代码块的内容后,唤醒EntryList中等待的线程来竞争锁,竞争是非公平的


4、Synchronized 字节码

static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {synchronized (lock) {counter++;}
}

对应的字节码为

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=3, args_size=10: getstatic #2 // <- lock引用 (synchronized开始)3: dup4: astore_1 // lock引用 -> slot 15: monitorenter // 将 lock对象 MarkWord 置为 Monitor 指针6: getstatic #3 // <- i9: iconst_1 // 准备常数 110: iadd // +111: putstatic #3 // -> i14: aload_1 // <- lock引用15: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList16: goto 2419: astore_2 // e -> slot 2 20: aload_1 // <- lock引用21: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList22: aload_2 // <- slot 2 (e)23: athrow // throw e24: returnException table:from to target type6    16  19    any19   22  19    anyLineNumberTable:line 8: 0line 9: 6line 10: 14line 11: 24LocalVariableTable:Start Length Slot Name Signature0     25     0    args [Ljava/lang/String;StackMapTable: number_of_entries = 2frame_type = 255 /* full_frame */offset_delta = 19locals = [ class "[Ljava/lang/String;", class java/lang/Object ]stack = [ class java/lang/Throwable ]frame_type = 250 /* chop */offset_delta = 4

0:拿到lock的引用

4:将lock的引用存储到 slot1 中

5:将lock对象的 MarkWord 置为 Monitor 指针,原本存储的信息就存储到 Monitor 中

6-11:执行counter++操作

14:从 slot1 中获取lock对象的引用

15:将 lock 对象的 MarkWord 重置,原本MarkWord 存储的是hashcode、gc年龄等信息,当 lock 获取锁后,将MarkWord 置位 Monitor 指针。重置就是将这些信息重新写到 MarkWord 中,同时唤醒 EntryList

16:goto 24 执行24行,退出

在Exception table中设置了监控异常的行数,如果6-16行有异常,就去执行19行

19:将异常信息 e 存储到slot2中

20:从 slot1 中获取lock对象的引用

21:将 lock对象 MarkWord 重置, 唤醒 EntryList

22:从 slot2 中获取异常信息

23:打印异常信息

注意

  • 通过异常 try-catch 机制,确保一定会被解锁
  • 方法级别的 synchronized 不会在字节码指令中有所体现


5、轻量级锁

如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。

轻量级锁对使用者是透明的,即语法仍然是 synchronized

如下,method1method2方法都对obj对象加锁

static final Object obj = new Object();public static void method1() {synchronized( obj ) {// 同步块 Amethod2();}
}public static void method2() {synchronized( obj ) {// 同步块 B}
}

1、创建 锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word

锁记录有两个属性

  • 锁记录地址,同时用00标记表示轻量级锁
  • Object referenct指向锁的对象

锁的对象obj中有对象头、对象体,对象头中Mark Word存储的是hashcode、gc年龄等信息

2、让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 ObjectMark Word,将 Mark Word 的值存入锁记录,然后将锁记录的信息存到ObjectMark Word

3、如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁

4、如果 cas 失败,有两种情况

  • 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
  • 如果是自己执行了synchronized锁重入,那么再添加一条 Lock Record 作为重入的计数
    • 此时锁记录中Object reference指向Object,但是由于ObjectMark World位置已经是00轻量级锁状态,因此这条锁记录存储为null

5、当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一

6、当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头

  • 成功,则解锁成功

  • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程


6、锁膨胀

锁膨胀:轻量级锁升级为重量级锁

如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁


static Object obj = new Object();
public static void method1() {synchronized( obj ) {// 同步块}
}

1、当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁

2、这时 Thread-1 加轻量级锁失败,进入锁膨胀流程

  • 为 Object 对象申请 Monitor 锁,让 ObjectMARK WORLD指向Moniter锁地址
  • 在堆区创建一个锁记录【Lock Record】对象,该对象包含了持有该锁的线程信息,然后ObjectMARK WORLD也会记录这个对象的地址
  • 然后 Thread-1进入 MonitorEntryList,处于BLOCKED状态

3、当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,会失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程


7、自旋优化

重量级锁竞争的时候,还可以使用自旋(循环尝试获取重量级锁)来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。 (进入阻塞再恢复,会发生上下文切换,比较耗费性能)


自旋重试成功的情况

自旋重试失败的情况

  • 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
  • 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
  • Java 7 之后不能控制是否开启自旋功能

8、偏向锁

  • 轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。

  • Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有

这里的线程id是操作系统赋予的id 和 Thread的id是不同的

static final Object obj = new Object();
public static void m1() {synchronized( obj ) {// 同步块 Am2();}
}
public static void m2() {synchronized( obj ) {// 同步块 Bm3();}
}
public static void m3() {synchronized( obj ) {// 同步块 C}
}

没有开启偏向锁,会使用轻量级锁 ,每次重入都会执行CAS操作 开启偏向锁,每次锁重入仅判断当前ThreadID是否是自己


对象头格式

一个对象创建时:

  • 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的 thread、epoch、age 都为 0,不保存hashcode信息
  • 偏向锁默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟
  • 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、age 都为 0,第一次用到 hashcode 时才会赋值

测试

利用 jol 第三方工具来查看对象头信息

public static void main(String[] args) throws IOException {Dog d = new Dog();ClassLayout classLayout = ClassLayout.parseInstance(d);new Thread(() -> {log.debug("synchronized 前");System.out.println(classLayout.toPrintableSimple(true));synchronized (d) {log.debug("synchronized 中");System.out.println(classLayout.toPrintableSimple(true));}log.debug("synchronized 后");System.out.println(classLayout.toPrintableSimple(true));}, "t1").start();
}
11:08:58.117 c.TestBiased [t1] - synchronized 前
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101 
11:08:58.121 c.TestBiased [t1] - synchronized 中
00000000 00000000 00000000 00000000 00011111 11101011 11010000 00000101 
11:08:58.121 c.TestBiased [t1] - synchronized 后
00000000 00000000 00000000 00000000 00011111 11101011 11010000 00000101

注意:处于偏向锁的对象解锁后,线程 id 仍存储于对象头中,也就是偏(心)向某个线程了


禁用偏向锁

禁用偏向锁后,创建对象后,最后3位是001,无锁状态。加锁后,变为000,轻量级锁,同时保存了锁记录地址。释放锁后,变回001无锁状态,同时清除锁记录地址

11:13:10.018 c.TestBiased [t1] - synchronized 前
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
11:13:10.021 c.TestBiased [t1] - synchronized 中
00000000 00000000 00000000 00000000 00100000 00010100 11110011 10001000 
11:13:10.021 c.TestBiased [t1] - synchronized 后
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001

9、偏向锁的撤销

9.1 hashcode

Dog d = new Dog(); 后加上一句 d.hashCode();

  • 正常状态对象一开始是没有 hashCode 的,第一次调用才生成
  • 调用了 hashCode() 后会撤销该对象的偏向锁
11:22:10.386 c.TestBiased [main] - 调用 hashCode:1778535015 
11:22:10.391 c.TestBiased [t1] - synchronized 前
00000000 00000000 00000000 01101010 00000010 01001010 01100111 00000001 
11:22:10.393 c.TestBiased [t1] - synchronized 中
00000000 00000000 00000000 00000000 00100000 11000011 11110011 01101000 
11:22:10.393 c.TestBiased [t1] - synchronized 后
00000000 00000000 00000000 01101010 00000010 01001010 01100111 00000001

因为调用了hashcode(),但是默认是偏向锁,存储的是线程id,没有内存去存储hashcode,因此会撤销偏向锁,用来存储hashcode

  • 轻量级锁会在锁记录中记录 hashCode
  • 重量级锁会在 Monitor 中记录 hashCode

9.2 其它线程使用对象

当有其它线程使用偏向锁对象时【没有发生锁竞争】,会将偏向锁升级为轻量级锁

private static void test2() throws InterruptedException {Dog d = new Dog();Thread t1 = new Thread(() -> {log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));synchronized (d) {log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));}log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));synchronized (TestBiased.class) {TestBiased.class.notify();}}, "t1");t1.start();Thread t2 = new Thread(() -> {synchronized (TestBiased.class) {try {TestBiased.class.wait();} catch (InterruptedException e) {e.printStackTrace();}}log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));synchronized (d) {log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));}log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));}, "t2");t2.start();
}


9.3 调用 wait/notify

重量级锁才支持 wait/notify,调用后,锁直接升级为重量级锁

public static void main(String[] args) throws InterruptedException {Dog d = new Dog();Thread t1 = new Thread(() -> {log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));synchronized (d) {log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));try {d.wait();} catch (InterruptedException e) {e.printStackTrace();}log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));}}, "t1");t1.start();new Thread(() -> {try {Thread.sleep(6000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (d) {log.debug("notify");d.notify();}}, "t2").start();
}
[t1] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101 
[t1] - 00000000 00000000 00000000 00000000 00011111 10110011 11111000 00000101 
[t2] - notify 
[t1] - 00000000 00000000 00000000 00000000 00011100 11010100 00001101 11001010

总结

  • 默认情况下,偏向锁是开启的,即这个锁归这个对象所拥有

  • 如果有其他线程获取锁或者调用hashcode,那么升级为轻量级锁

  • 如果发生锁竞争或者调用wait/notify,那么升级为重量级锁


10、批量重偏向、撤销

  • 对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID 。当(某类型对象)撤销偏向锁超过阈值 20 次后,jvm 会在给(所有这种类型的状态为偏向锁的)对象加锁时重新偏向至新的加锁线程
  • 当撤销偏向锁阈值超过 40 次后,jvm 会将整个类的所有对象都会变为不可偏向的,新建的该类型对象也是不可偏向的
    • 例如:当前有40个锁对象,刚开始都偏向t1线程。现在t2线程获取这40个锁对象,1-19个锁对象会撤销偏向锁,第20个锁对象往后,会撤销t1的偏向锁,将偏向锁设置为t2【达到20阈值】。然后t3线程获取这40个锁对象,由于前19个锁对象已经是非偏向锁了,从第20个开始,又会撤销偏向锁,最后撤销次数达到40阈值后,会将所有的锁变为不可偏向的,即使新创建的对象也是不可偏向的。

演示批量重偏向

private static void test3() throws InterruptedException {Vector<Dog> list = new Vector<>();Thread t1 = new Thread(() -> {for (int i = 0; i < 30; i++) {Dog d = new Dog();list.add(d);synchronized (d) {log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));}}synchronized (list) {list.notify();}}, "t1");t1.start();Thread t2 = new Thread(() -> {synchronized (list) {try {list.wait();} catch (InterruptedException e) {e.printStackTrace();}}log.debug("===============> ");for (int i = 0; i < 30; i++) {Dog d = list.get(i);log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));synchronized (d) {log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));}log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));}}, "t2");t2.start();
}
[t1] - 0 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 1 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
...
[t1] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - ===============> 
[t2] - 0 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101  // 原始轻量级锁偏向t1
[t2] - 0 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000  // t2获取锁,将锁升级为轻量级锁
[t2] - 0 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001  // 释放锁后,轻量级锁被撤销
...
[t2] - 18 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 18 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 18 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
[t2] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 // 第20次,初始轻量级锁偏向t1
[t2] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 // 第20次撤销锁,达到阈值,jvm将后边所有锁偏向t2
[t2] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
...
[t2] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101

11、锁消除

锁消除 :JIT即时编译器会对字节码做进一步优化,下边代码中o是一个局部变量,不会共享,所以编译后,不会执行加锁操作,而是直接执行x++

public class MyBenchmark {static int x = 0;public void b() throws Exception {//这里的o是局部变量,不会被共享,JIT做热点代码优化时会做锁消除Object o = new Object();synchronized (o) {x++;}}
}

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

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

相关文章

命令行使用ADB,不用root,完美卸载小米预装软件

ADB安装与运行 install java 下载安装 注意选择JDK17以上版本 https://www.oracle.com/java/technologies/downloads/#jdk22-windows 选择中间的安装文件下载 编辑系统变量 C:\Program Files (x86)\Java\jdk-22 C:\Program Files (x86)\Java\jdk-22\bin 把C:\Progra…

K210视觉识别模块学习笔记7:多线程多模型编程识别

今日开始学习K210视觉识别模块: 图形化操作函数 亚博智能 K210视觉识别模块...... 固件库: canmv_yahboom_v2.1.1.bin 训练网站: 嘉楠开发者社区 今日学习使用多线程、多模型来识别各种物体 这里先提前说一下本文这次测试实验的结果吧&#xff1a;结果是不太成…

视频去水印免费电脑版 pdf压缩在线免费网页版 pdf压缩在线免费 简单工具软件详细方法步骤分享

消除视频中的恼人水印&#xff0c;是许多视频编辑爱好者的常见需求。在这篇文章中&#xff0c;我们将探讨几种视频去水印的技巧&#xff0c;在数字化时代&#xff0c;视频和图片的传播越来越方便&#xff0c;但随之而来的水印问题也让人头疼。本文将为您详细介绍视频剪辑去水印…

捕获会自动消失的消息提示弹窗

如上图&#xff0c;我们会在一些场景碰到会自动消失的消息提示弹窗&#xff0c;一般存在个3-5秒&#xff0c;我们在做UI断言时&#xff0c;需要监测这个弹窗是否会出现&#xff0c;就需要去捕获这个弹窗的位置 我们打开浏览器的开发者模式(F12)&#xff0c;找到源码(Sources) …

探索 Redis 不同集群架构的性能与应用

1. 引言 Redis的集群配置成为了提高数据可靠性和服务可用性的关键。本文将带领大家了解Redis的四种主要集群架构&#xff0c;并重点分析哨兵模式和Redis Cluster架构和优势。 2. Redis的四种集群架构 2.1 单实例Redis 使用单个 Redis 实例提供服务。适用于小规模应用&#…

MiniExcel:.NET中处理Excel的高效方案

在.NET开发环境中&#xff0c;处理Excel文件是一项常见的任务&#xff0c;无论是数据导入、导出还是报表生成。传统的解决方案可能存在性能瓶颈或功能限制。MiniExcel作为一个现代、高效的库&#xff0c;为.NET开发者提供了一个强大的工具来简化Excel操作。本文将介绍MiniExcel…

爬虫程序在采集亚马逊站点数据时如何绕过验证码限制?

引言 在电商数据分析中&#xff0c;爬虫技术的应用日益广泛。通过爬虫技术&#xff0c;我们可以高效地获取大量的电商平台数据&#xff0c;这些数据对于市场分析、竞争情报、价格监控等有着极其重要的意义。亚马逊作为全球最大的电商平台之一&#xff0c;是数据采集的重要目标…

【技术升级】Docker环境下Nacos平滑升级攻略,安全配置一步到位

目前项目当中使用的Nacos版本为2.0.2&#xff0c;该版本可能存在一定的安全风险。软件的安全性是一个持续关注的问题&#xff0c;尤其是对于像Nacos这样的服务发现与配置管理平台&#xff0c;它在微服务架构中扮演着核心角色。随着新版本的发布&#xff0c;开发团队会修复已知的…

【解决】ubuntu20.04 root用户无法SSH登陆问题

Ubuntu root用户无法登录的问题通常可以通过修改‌SSH配置文件和系统登录配置来解决。 修改SSH配置文件 sudo vim /etc/ssh/sshd_config 找到 PermitRootLogin 设置&#xff0c;并将其值更改为 yes 以允许root用户通过SSH登录 保存并关闭文件之后&#xff0c;需要重启SSH服务…

【HarmonyOS】实现矩形上下拖动、动态拖拽修改高度

简介 实现一个矩形块上下拖动&#xff0c;并且可以拖动边缘定位点改变矩形块高度。实现效果如下&#xff1a; 代码 Entry Component struct Rec_Page {State penOffsetY: number 0;State offsetX: number 0State offsetY: number 0State positionX: number 0State posi…

Microsoft 官网免费下载安装正版官方增强版 office LSTC (长期支持版) 包含 visio , access

1.进入下方网址&#xff1a; https://www.microsoft.com/en-us/download/details.aspx?id49117 下载文件&#xff1a; officedeploymenttool_17126-20132.exe 现在看到下载链接可能失效了&#xff0c;但是下述步骤任然正确。需要下载文件的可以私信发送。 2.进入下方网址…

家具购物小程序的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;用户管理&#xff0c;家具分类管理&#xff0c;家具新品管理&#xff0c;订单管理&#xff0c;系统管理 微信端账号功能包括&#xff1a;系统首页&#xff0c;家具新品&#xff0c;家具公告&#xff0…

filament 初使用记录

安装初始化 一、环境准备 官网要的 我安装的 二、下载安装 安装laravel composer create-project --prefer-dist laravel/laravel 项目名称 10.*导入 filament composer require filament/filament注册 filament 管理面板 php artisan filament:install --panels初始化…

vue3前端开发-小兔鲜项目-登录组件的开发表单验证

vue3前端开发-小兔鲜项目-登录组件的开发表单验证&#xff01;现在开始写登录页面的内容。首先这一次完成基础的首页按钮点击跳转&#xff0c;以及初始化一些简单的表单的输入验证。后期还会继续完善内容。 1&#xff1a;首先还是准备好login页面的组件代码内容。 <script …

MySQL基础练习题7-销售分析

题目&#xff1a;报告 2019年春季 才售出的产品。即 仅 在 2019-01-01 &#xff08;含&#xff09;至 2019-03-31 &#xff08;含&#xff09;之间出售的商品。 准备数据 分析数据 方法一&#xff1a;group by having 第一步&#xff1a;先找到要求的列 第二步&#xff1…

CAN转PROFINET网关

型号&#xff1a;TCA-152 &#xff08;上海泗博自动化技术有限公司产品&#xff09; 基本说明&#xff1a;TCA-152可实现 PROFINET网络与CAN网络之间的数据通信。网关在PROFINET网络作为从站&#xff0c;CAN端支持CAN2.0A/CAN2.0B协议&#xff0c;支持对CAN帧进行过滤处理。 …

python 图片转文字、语音转文字、文字转语音保存音频并朗读

一、python图片转文字 1、引言 pytesseract是基于Python的OCR工具&#xff0c; 底层使用的是Google的Tesseract-OCR 引擎&#xff0c;支持识别图片中的文字&#xff0c;支持jpeg, png, gif, bmp, tiff等图片格式 2、环境配置 python3.6PIL库安装Google Tesseract OCR 3、安…

谷粒商城实战笔记-65-商品服务-API-品牌管理-表单校验自定义校验器

文章目录 1&#xff0c;el-form品牌logo图片自定义显示2&#xff0c;重新导入和注册element-ui组件3&#xff0c;修改brand-add-or-update.vue控件的表单校验规则firstLetter 校验规则sort 校验规则 1&#xff0c;el-form品牌logo图片自定义显示 为了在品牌列表中自定义显示品…

最新源支付系统源码 V7版全开源 免授权 附搭建教程

本文来自&#xff1a;最新源支付系统源码 V7版全开源 免授权 附搭建教程 - 源码1688 简介&#xff1a; 最新源支付系统源码_V7版全开源_免授权_附详细搭建教程_站长亲测 YPay是专为个人站长打造的聚合免签系统&#xff0c;拥有卓越的性能和丰富的功能。它采用全新轻量化的界面…