【多线程与高并发】- 锁的机制与底层优化原理

news2024/11/23 21:11:38

锁的机制与底层优化原理

😄生命不息,写作不止
🔥 继续踏上学习之路,学之分享笔记
👊 总有一天我也能像各位大佬一样
🏆 一个有梦有戏的人 @怒放吧德德
🌝分享学习心得,欢迎指正,大家一起学习成长!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rADoNDiD-1686655850724)(D:\Documents\锁与线程并发系列\锁的机制与底层优化原理.jpg)]

文章目录

  • 锁的机制与底层优化原理
    • 前言
    • 简单例子
    • 锁的机制
      • 1、jdk1.6之前
      • 2、CAS机制
    • Synchronized底层的锁优化机制
      • 1、锁的状态升级变迁
        • (1)、锁状态mark word结构
        • (2)、锁升级流程
        • (3)、轻量级锁一定比重量级锁性能高吗?
      • 2、synchronized锁升级状态变化原理
    • 文章资料

前言

最近经常研究一些关于线程并发的问题,再开发中也实实在在遇到过许多的并发问题,之前所学的是如何解决这些问题,然而接下来就得理解一下底层原理。

简单例子

首先用一个简单的例子来进行对锁的开篇认知。

如下代码,我们定义一个类,在这个类中提供了一个自增的方法。我们通过多线程的方式去执行自增,并且主线程也加入进行自增,最后输出这个值。这段代码都知道在自增的时候会出现并发问题,我们在通过加锁,控制对互斥资源的访问,最后就能得到期望的值。

public class Number {
    int num = 0;
    public int getNum() {
        return num;
    }
    public void autoAccretion() {
        synchronized(this) {
            num++;
        }
    }
}
public class TestNum {
    public static void main(String[] args) throws InterruptedException {
        Number number = new Number();
        long startTime = System.currentTimeMillis();
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 10000000; i++) {
                number.autoAccretion(); // 自增
            }
        });
        thread.start();
        // 主线程也执行
        for (int i = 0; i < 10000000; i++) {
            number.autoAccretion();
        }
        thread.join();
        long endTime = System.currentTimeMillis();
        System.out.println(String.format("%sms", endTime - startTime));
        System.out.println(number.num);
    }
}

我们通过执行会发现,这个值并不是我们期望所得到的,这是因为这里面出现了并发的问题。
在这里插入图片描述

当然,我们都知道要想解决这个并发问题,只需要在调用对象的方法上加上synchronized 就行。

锁的机制

通过以上代码,我们使用synchronized锁来控制对num++的执行,当多个线程进来的时候,只有一个锁能拿到synchronized这把锁。

1、jdk1.6之前

在jdk1.6以前,真正加锁的对象是synchronized内部的monitor对象[1];那么,如果拿不到synchronized锁的线程最后会是怎样的呢?他会放到一个队列中(即重量级锁),直到锁被释放后才能让下个线程拿到锁,这是jdk1.6以前的做法,如果一直不释放锁,那么就会导致这些等待线程一直处于等待,很明显这样会导致性能的问题。

在来说一下重量级锁,在底层可能出现线程阻塞,上下文切换,等待得到锁的线程将锁释放掉,通过操作系统对线程的调度,将阻塞态的线程唤醒。操作系统实现线程的切换还需要从用户态切换到核心态,成本非常高。

[1]在操作系统和并发编程领域中,Monitor(监视器)是一种同步机制,用于控制对共享资源的访问。它可以用于确保在任何时刻只有一个线程能够进入临界区(Critical Section)并执行相关操作,从而实现线程安全。

在这里插入图片描述

2、CAS机制

CAS(Compare and Swap)简单说就是比较并交换,它是一种并发编程中常用的原子操作,用于实现无锁的线程安全操作。它通常用于解决多个线程同时对同一个共享变量进行修改的竞争问题。我们通常将cas称为无锁、自旋锁、乐观锁以及轻量级锁。

CAS操作包含三个操作数:内存位置(或称为期望值),当前值和新值。CAS操作会比较内存位置的当前值与期望值是否相等,如果相等,则将内存位置的值更新为新值;如果不相等,则不进行任何操作。CAS操作是原子的,即在执行过程中不会被其他线程中断。它通过比较当前值和期望值来确定内存位置是否被修改,从而避免了传统的锁机制带来的竞争和阻塞。

CAS的大致流程如下:

​ ①获取内存中的原始值,即备份数据。

​ ②进行比较,将当前值与期望值进行比对看是否相等。

​ ③如果相等,就将当前值覆盖旧的值,反之通过循环,重复操作,直到获得到对的值。

我们最常见的AtomicInteger类,他就是属于原子性操作的,它可以在并发环境下进行原子操作,确保对整数的操作是线程安全的。如下代码,我们可以通过这个类的自增方法来替换用synchronized锁包围的自增运算。

public void autoAccretion() {
    //        synchronized (this) {
    //            num++;
    //        }
    atomicInteger.incrementAndGet();
}

我们可以看它的底层代码,通过unsafe类调用自增方法,实际底层原理也是进行比较交换的规则来保证原子性。会先获取内部原始的值,在将这个值自增1,在进行比较,如果当前值等于期望值,则自动将值设置为给定的更新值。但是,如果比较不相等,可能是在获取原始值之后做自增的时候,原始值已经被其他线程给操作成功覆盖了,则这个新的值是错误,需要刷新备份数据,再去循环尝试,直到得到对的数据才会去刷新旧值。大量的线程过来执行这个compareAndSet方法,如果在执行的时候没有被其他线程执行,那就能够将新的值将旧的值替换掉,就算说是失败了,能够通过循环继续执行,在多线程的执行能够确保数据的正确性,至于线程的先后执行也只是看运气。

public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1; 
}

//unsafe.class
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); // 最后的底层原理通过循环自备份,自增与比较

    return var5;
}

我们可以看到内部代码是看不到的,它底层是通过c++编写的。Java中的CAS操作都是通过sun包下Unsafe类实现,而Unsafe类中的方法都是native方法,表示该方法的实现是由外部的本地代码,这里我下载了lookaside_java-1.8.0-openjdk的源码(可以从github上拉取),可以从host底层来看源码。
在这里插入图片描述

就AtomicInteger内的compareAndSwapInt方法,我们通过jdk1.8的 hotspot/src/share/vm/prims/unsafe.cpp 下的源码可以看到底层由c++编写。它使用原子比较和交换操作来比较和替换指定内存地址上的整数值,并返回比较结果。

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  // We are about to write to this entry so check to see if we need to copy it.
  // 执行了一个写屏障操作(write barrier),用于保证在修改对象之前进行必要的处理。
  // JNIHandles::resolve(obj)将obj从JNI句柄解析为Java对象,并使用oopDesc::bs()执行写屏障操作。
  oop p = oopDesc::bs()->write_barrier(JNIHandles::resolve(obj));
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);// 计算出要进行原子比较和交换操作的内存地址
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e; // 核心代码 执行原子的比较和交换操作
UNSAFE_END

(jint)(Atomic::cmpxchg(x, addr, e)) == e;: 这一行代码是主要的核心代码,使用Atomic::cmpxchg函数执行原子的比较和交换操作。它尝试将addr指向的内存地址上的值与e进行比较,如果相等,则将其替换为x。最后,它将比较结果与e进行比较,如果相等,则返回true,否则返回false。

接下来在jdk1.8的源码:atomic_linux_x86.inline.hpp中看一下这个核心代码的底层逻辑,这段代码会先通过操作系统的内核方法判断是否为多处理器系统,在通过LOCK_IF_MP获取lock指令,实际上拿到的汇编指令lock与cmpxchgl来实现原子性。当拿到lock指令的时候就能给进行比较并交换,没有得到锁的情况需要等待锁被释放,这就达到了原子性问题。当缓存不是很大的情况是使用缓存行锁,但如果超过了缓存行大小,就会使用总线锁。

inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
  int mp = os::is_MP(); // 调用操作系统内核方法用于判断当前的系统是否为多处理器系统。
  // LOCK_IF_MP获取lock指令,他是判断是否位多处理器
  __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)" // 汇编指令,前面已经定义#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "
                    : "=a" (exchange_value)
                    : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
                    : "cc", "memory");
  return exchange_value;
}

Synchronized底层的锁优化机制

1、锁的状态升级变迁

在jdk1.6以后Synchronized锁引用了许多状态切换:无状态、偏向锁、轻量级锁、重量级锁,根据不同的条件进行状态的切换升级,能够在一定的程度中使性能提升。

(1)、锁状态mark word结构

synchronized锁在线程第一次访问的时候,实际上是没有加锁的,只是在mark word中记录了线程ID,这种就是偏向锁,默认是认为不会有多个线程抢着用,mark word是通过64bit来表示的,通过最低2位也就是锁标志位,偏向锁与无锁的值是01,轻量级锁用00表示,重量级锁用10表示,标记了GC的用11表示,无锁与偏向锁低2位是一致的,在倒数第3位有1位来表示偏向锁位:值为1表示偏向锁。
在这里插入图片描述

(2)、锁升级流程

在Java中,synchronized锁的状态可以根据竞争情况进行升级和降级,结合上图,我们就可以清晰的了解synchronized底层锁的状态变化过程。

初始状态下,对象没有被任何线程锁定,此时是无状态锁;当有一个线程第一次进入synchronized代码块时,JVM会偏向该线程,将锁的对象头标记为偏向锁,此时还会记录这个线程ID,能够直接进入同步块,标记偏向线程id是为了等下次线程过来访问的时候,会进行线程id比较,如果相同,就能够获取这把锁;然而,当多个线程来争抢这把锁,这时候就会进行锁升级,会将偏向锁升级为轻量级锁,它会使用CAS操作来尝试将锁的对象头设置为指向锁记录(Lock Record)的指针,如果CAS成功,就能够获得这把锁,如果获得不到,会通过自旋;当轻量级锁竞争失败时,锁会升级为重量级锁。此时,JVM会使用操作系统的互斥量(Mutex)来实现锁的互斥操作。重量级锁涉及到用户态和内核态之间的切换,开销较大。
在这里插入图片描述

(3)、轻量级锁一定比重量级锁性能高吗?

当线程足够多的时候,如果使用轻量级锁,很多个线程会自旋,没有成功将会一直自旋,这样还会消耗cpu,此时还不如直接放在队列中使用重量级锁。总的来说如果竞争不激烈,轻量级锁可以提供更好的性能。而在高度竞争的情况下,重量级锁可能更适合,避免了自旋和不断重试的开销。在实际使用中,需要根据具体情况进行评估和测试,选择适当的锁机制。

2、synchronized锁升级状态变化原理

接下来我们从代码的形式来了解synchronized锁的升级状态变化。

因为使用了 java对象的内存布局以及使用ClassLayout查看布局,首先需要导入依赖, 0.13是显示二进制,0.17最新版本是显示十六进制。

<!--  java对象的内存布局以及使用ClassLayout查看布局  -->
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.17</version>
</dependency>

我们在main方法中,通过ClassLayout查看布局,这里User只是定义的一个对象实体,里面包含id和name属性。

User userTemp = new User();
/*java对象的内存布局以及使用ClassLayout查看布局*/
System.out.println("无状态(001):" + ClassLayout.parseInstance(userTemp).toPrintable());

我们看一下这段代码的输出,可以对照锁的状态图来看,这些信息包含对象头mark,class,还有对象属性,最后4字节是对齐位,因为位数是8的整数倍。mark由8个字节,64bit组成,以下是十六进制,我们转换成二进制:0…001对比锁状态图来看是无锁状态。

无状态(001):cn.lyd.test.service.user.dto.User object internals:
OFF  SZ               TYPE DESCRIPTION               VALUE
  0   8                    (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4                    (object header: class)    0xf800c143
 12   4                int User.id                   0
 16   4   java.lang.String User.name                 null
 20   4                    (object alignment gap)    
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

先给出全部代码,后面针对代码进行解析,通过这段代码,就能够清晰看见锁是如何升级的。

/**
 * @Author: lyd
 * @Description: synchronized锁升级状态变化
 * @Date: 2023/6/11
 */
public class LockUpgrade {
    public static void main(String[] args) throws InterruptedException { // 主线程
        User userTemp = new User();
        /*java对象的内存布局以及使用ClassLayout查看布局*/
        System.out.println("无状态(001):" + ClassLayout.parseInstance(userTemp).toPrintable());
        // jvm默认延迟4s自动开机偏向锁,可以通过-XX:BiasedLockingStartupDelay = 0 取消延迟
        // 如果不需要偏向锁,使用-XX:- UseBiasedLocking = false 关闭
        // -> 一开始是不会自动使用偏向锁的,如果一开始就使用synchronized锁,就会直接使用重量级锁,jvm中需要延迟4s才能够开启偏向锁。所以这里延迟了5s
        Thread.sleep(5000);
        // 偏向锁认为一开始只有一个线程来访问
        User user = new User(); // 重新new一个对象
        System.out.println("启用偏向锁(101):" + ClassLayout.parseInstance(user).toPrintable());
        for (int i = 0; i < 2; i++) {
            synchronized (user) { // 加上偏向锁
                System.out.println("偏向锁(101)(带线程ID):" + ClassLayout.parseInstance(user).toPrintable());
            }
            // 虽然这里会释放偏向锁,但实际上不会主动释放,头部(高54位)是不会做修改的,这些数值代表属于哪个线程
            // 在偏向锁中,高54位来标识线程id,主要是如果同个线程过来的,就直接走这个偏向锁
            System.out.println("释放偏向锁(101)(带线程ID):" + ClassLayout.parseInstance(user).toPrintable());
        }
        new Thread(() -> { // 多个线程竞争,可能升级为轻量级锁
            synchronized (user) {
                System.out.println("轻量级锁(00)(带线程ID):" + ClassLayout.parseInstance(user).toPrintable());
                try {
                    System.out.println("睡眠3秒钟==========================================");
                    Thread.sleep(3000);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
                System.out.println("轻量级锁升级重量级锁(10):" + ClassLayout.parseInstance(user).toPrintable());
            }
        }).start();
        Thread.sleep(1000);
        // 开启新的线程, 第一个线程还在睡眠中,意味着线程还没有结束,此时第二个线程就执行了,这样就会导致多个线程的访问,这是就会升级位重量级锁
        new Thread(() -> {
            synchronized (user) {
                System.out.println("重量级锁(10)(带线程ID):" + ClassLayout.parseInstance(user).toPrintable());
            }
        }).start();
    }
}

代码解析:

首先是无锁的状态,这个在前面已经介绍了,现在就不再继续赘述。我们要知道,jvm默认是延迟4s才自动开机偏向锁,我们可以通过-XX:BiasedLockingStartupDelay = 0 取消延迟。

一开始是不会自动使用偏向锁的,如果一开始就使用synchronized锁,就会直接使用重量级锁,jvm中需要延迟4s才能够开启偏向锁。所以这里延迟了5s。

重新new一个对象,此时我们可以看到下启用了偏向锁,注意,这里虽然还没有上锁,但是锁的使用是需要先开启锁的。

User user = new User(); // 重新new一个对象
System.out.println("启用偏向锁(101):" + ClassLayout.parseInstance(user).toPrintable());

看一下控制台的输出,可以看到头部mark信息,转换二进制就是000…0101,这可以看到偏向锁就已经启用了,但是我们观察到偏向线程id的54bit都是0,显然这时候还没有上锁,只是开启了偏向锁而已。

启用偏向锁(101):cn.lyd.test.service.user.dto.User object internals:
OFF  SZ               TYPE DESCRIPTION               VALUE
  0   8                    (object header: mark)     0x0000000000000005 (biasable; age: 0)
  8   4                    (object header: class)    0xf800c143
 12   4                int User.id                   0
 16   4   java.lang.String User.name                 null
 20   4                    (object alignment gap)    
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

通过for循环,主线程两次访问user这把锁,此时启动的是偏向锁,这里是携带了线程id,在偏向锁中,高54位来标识线程id,主要是如果同个线程过来的,就直接走这个偏向锁。输出完后释放偏向锁,实际上不会主动释放,头部高54位并没有做修改。

for (int i = 0; i < 2; i++) {
    synchronized (user) { // 加上偏向锁
        System.out.println("偏向锁(101)(带线程ID):" + ClassLayout.parseInstance(user).toPrintable());
    }
    // 虽然这里会释放偏向锁,但实际上不会主动释放,头部(高54位)是不会做修改的,这些数值代表属于哪个线程
    // 在偏向锁中,高54位来标识线程id,主要是如果同个线程过来的,就直接走这个偏向锁
    System.out.println("释放偏向锁(101)(带线程ID):" + ClassLayout.parseInstance(user).toPrintable());
}

因为是主线程来两次访问,线程id都是相同的,根据头mark的二进制可以解出线程id:000…001100111111010100这是将0x00000000033f5005转换成二进制,取高54位的来。

偏向锁(101)(带线程ID):cn.lyd.test.service.user.dto.User object internals:
OFF  SZ               TYPE DESCRIPTION               VALUE
  0   8                    (object header: mark)     0x00000000033f5005 (biased: 0x000000000000cfd4; epoch: 0; age: 0)
  8   4                    (object header: class)    0xf800c143
 12   4                int User.id                   0
 16   4   java.lang.String User.name                 null
 20   4                    (object alignment gap)    
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

释放偏向锁(101)(带线程ID):cn.lyd.test.service.user.dto.User object internals:
OFF  SZ               TYPE DESCRIPTION               VALUE
  0   8                    (object header: mark)     0x00000000033f5005 (biased: 0x000000000000cfd4; epoch: 0; age: 0)
  8   4                    (object header: class)    0xf800c143
 12   4                int User.id                   0
 16   4   java.lang.String User.name                 null
 20   4                    (object alignment gap)    
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

偏向锁(101)(带线程ID):cn.lyd.test.service.user.dto.User object internals:
OFF  SZ               TYPE DESCRIPTION               VALUE
  0   8                    (object header: mark)     0x00000000033f5005 (biased: 0x000000000000cfd4; epoch: 0; age: 0)
  8   4                    (object header: class)    0xf800c143
 12   4                int User.id                   0
 16   4   java.lang.String User.name                 null
 20   4                    (object alignment gap)    
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

释放偏向锁(101)(带线程ID):cn.lyd.test.service.user.dto.User object internals:
OFF  SZ               TYPE DESCRIPTION               VALUE
  0   8                    (object header: mark)     0x00000000033f5005 (biased: 0x000000000000cfd4; epoch: 0; age: 0)
  8   4                    (object header: class)    0xf800c143
 12   4                int User.id                   0
 16   4   java.lang.String User.name                 null
 20   4                    (object alignment gap)    
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

刚刚跑的是主线程,我们可以创建一个新的线程,使得多个线程竞争,这就可能将偏向锁升级为轻量级锁。当线程获取到轻量级锁,其他线程进来会拿不到,此时会自旋。这里我们睡眠3s,来模拟线程紧握锁不放,这时候我们在开一个新的线程,还是来争抢user这把锁,这就导致CAS自旋失败,锁膨胀就会将轻量级锁升级为重量级锁。

new Thread(() -> { // 多个线程竞争,可能升级为轻量级锁
    synchronized (user) {
        System.out.println("轻量级锁(00)(带线程ID):" + ClassLayout.parseInstance(user).toPrintable());
        try {
            System.out.println("睡眠3秒钟==========================================");
            Thread.sleep(3000);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        System.out.println("轻量级锁升级重量级锁(10):" + ClassLayout.parseInstance(user).toPrintable());
    }
}).start();
Thread.sleep(1000);
// 开启新的线程, 第一个线程还在睡眠中,意味着线程还没有结束,此时第二个线程就执行了,这样就会导致多个线程的访问,这是就会升级位重量级锁
new Thread(() -> {
    synchronized (user) {
        System.out.println("重量级锁(10)(带线程ID):" + ClassLayout.parseInstance(user).toPrintable());
    }
}).start();

通过以下日志,我们可以将头mark转换成二进制,取最后两位锁标记位,能够清晰看到轻量级锁升级到重量级锁。

轻量级锁(00)(带线程ID):cn.lyd.test.service.user.dto.User object internals:
OFF  SZ               TYPE DESCRIPTION               VALUE
  0   8                    (object header: mark)     0x000000002a11ef08 (thin lock: 0x000000002a11ef08)
  8   4                    (object header: class)    0xf800c143
 12   4                int User.id                   0
 16   4   java.lang.String User.name                 null
 20   4                    (object alignment gap)    
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

睡眠3秒钟==========================================
轻量级锁升级重量级锁(10):cn.lyd.test.service.user.dto.User object internals:
OFF  SZ               TYPE DESCRIPTION               VALUE
  0   8                    (object header: mark)     0x0000000024232f6a (fat lock: 0x0000000024232f6a)
  8   4                    (object header: class)    0xf800c143
 12   4                int User.id                   0
 16   4   java.lang.String User.name                 null
 20   4                    (object alignment gap)    
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    
重量级锁(10)(带线程ID):cn.lyd.test.service.user.dto.User object internals:
OFF  SZ               TYPE DESCRIPTION               VALUE
  0   8                    (object header: mark)     0x0000000024232f6a (fat lock: 0x0000000024232f6a)
  8   4                    (object header: class)    0xf800c143
 12   4                int User.id                   0
 16   4   java.lang.String User.name                 null
 20   4                    (object alignment gap)    
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

文章资料

深入理解CAS操作

Java 对象、对象头mark word、锁升级、对象占内存大小

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

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

相关文章

c语言第二课------地基打牢,基础打紧,不可放弃

作者前言: 欢迎小可爱们前来借鉴我的gtieehttps://gitee.com/qin-laoda 目录 变量的作用域和生命周期 常量 字符串转义字符注释 选择语句 循环语句 函数 _________________________________________________ 这次我们接着上一篇博客,我们接着来继续C语言的学习 变量的作用…

四十六、B+树

这一次我们来介绍B树。 一个m阶的B树具有如下几个特征&#xff1a; 1.根结点至少有两个子女。 2.每个中间节点都包含k-1个元素和k个孩子&#xff0c;其中 m/2 < k < m 3.每一个叶子节点都包含k-1个元素&#xff0c;其中 m/2 < k < m 4.所有的叶子结点都位于同一层。…

Python学习ing

Python中 字符串不能通过 &#xff0c;把 数字等非字符串&#xff0c;进行拼接&#xff0c; 但Javascript中是可以的。 会把数字自动转换成字符串&#xff0c; 但python 提供了占位符 %s % 占位符 city "徐州" age 20 message "我是%s人,今天%s岁" …

面向对象继承

2. 面向对象特征二&#xff1a;继承(Inheritance) 2.1 继承的概述 说到继承我们会想到什么 在Java面向对象程序设计中的继承&#xff0c;多个类中存在相同属性和行为时&#xff0c;将这些相同的内容抽取到单独一个类&#xff08;父类&#xff09;中&#xff0c;然后所有的类继…

新能源汽车保养vr仿真教学软件为职业培训带来新的思路和方法

电动车电池更换VR虚拟体验是一种利用VR虚拟现实技术实现对电动车电池更换进行模拟仿真演示和实操训练的虚拟仿真实验教学课件&#xff0c;相比传统教学模式&#xff0c;有效提高学生的实践能力和技能水平。 通过VR技术模拟现场&#xff0c;使培训人员可以身临其境滴观摩操作过程…

(二)Liunx下ElasticSearch快速搭建

1.下载安装 1).环境准备&#xff1a; 操作系统&#xff1a;centos7 es版本&#xff1a;8.8.1 jdk:17 es与jdk等兼容支持查看 2).下载安装包上传到服务器&#xff0c;官网地址 https://www.elastic.co/cn/downloads/elasticsearch 3).解压文件 tar -zxvf elasticsearch-8.8.…

信息差永远存在,聪明的人也永远能利用信息差赚钱

信息的差异永远存在&#xff01; 例子一&#xff1a; 在2018年&#xff0c;某宝在央视春晚中成功地获得了广告名额&#xff0c;但与此同时&#xff0c;上百位工程师没想到的是&#xff0c;这是一个恶梦的开始&#xff01; 由于春晚当晚的访问人数过多&#xff0c;服务器立刻发…

模块化相关知识点

这里写目录标题 模块化概述基本使用&#xff08;包的跨模块使用&#xff09;介绍代码演示 模块服务的使用&#xff08;跨模块接口的实现&#xff09;简介具体代码 模块化 概述 基本使用&#xff08;包的跨模块使用&#xff09; 介绍 代码演示 首先 要在第一个模块里&#xf…

路径规划算法:基于算术优化优化的路径规划算法- 附代码

路径规划算法&#xff1a;基于算术优化优化的路径规划算法- 附代码 文章目录 路径规划算法&#xff1a;基于算术优化优化的路径规划算法- 附代码1.算法原理1.1 环境设定1.2 约束条件1.3 适应度函数 2.算法结果3.MATLAB代码4.参考文献 摘要&#xff1a;本文主要介绍利用智能优化…

SpringCloud公共模块项目搭建(二)

我们新建module公共模块项目 microservice-common&#xff0c;主要是放一些其他项目公用的东西&#xff0c;比如实体类&#xff0c;工具类等等&#xff1b; 新建module 勾选 create a simple project 取下module 名称 mircoservice-common 这里用默认的jar pom.xml 加下 jpa…

SpringBatch从入门到实战(四):表结构

batch_job_instance -> batch_job_execution/batch_job_execution_params -> batch_job_execution_context 一&#xff1a;batch_job_instance 作业实例 同一个作业&#xff0c;同一套作业参数 唯一标识一条记录&#xff0c;首次启动时插入一条数据。 字段描述JOB_IN…

MySQL 数据库基础

这里写目录标题 一、Mysql的基本概念数据库管理系统&#xff08;DBMS&#xff09;数据库系统 二、数据库的发展史三、 主流的数据库介绍数据库分为关系型数据库与非关系型数据库关系型数据库非关系型数据库介绍 四、 操作Mysql常用的数据类型&#xff1a;常看数据库结构查看当前…

共探数字化时代安全新思路,腾讯安全联合FreeBuf举办安全运营高峰论坛

近日&#xff0c;由网络安全行业门户FreeBuf主办&#xff0c;腾讯安全协办的「安全运营高峰论坛深圳站」在深圳益田威斯汀酒店圆满举行。在这场网络安全行业盛会上&#xff0c;数百名来自全国各地的网安人共聚一堂&#xff0c;围绕“安全运营”、“新时代下威胁情报对企业的价值…

为什么推荐使用线程池?

为什么推荐使用线程池&#xff1f; 更多优秀文章&#xff0c;请扫码关注个人微信公众号或搜索“程序猿小杨”添加。 背景&#xff1a; 若并发的线程数量很多&#xff0c;且每个线程都是执行一个时间很短的任务就结束了&#xff0c;这样频繁创建线程就会大大降低系统的效率&…

如何使用Leangoo领歌scrum工具自动生成燃尽图

在上一篇&#xff0c;我为大家介绍了如何使用Leangoo领歌敏捷工具管理Sprint Backlog_哆啦B梦_的博客-CSDN博客&#xff0c;今天我们一起来看看Leangoo领歌敏捷工具是如何自动生成Scrum燃尽图的。 什么是scrum燃尽图&#xff1f; 燃尽图是在项目完成之前&#xff0c;对需要完…

什么是大数据?

现在大数据发展的如火如荼&#xff0c;也有不少小伙伴对于什么是大数据比较感兴趣&#xff0c;那么大数据在比较官方的定义是指无法在一定时间范围内用常规软件工具进行捕捉、管理和处理的数据集合&#xff0c;是需要新处理模式才能具有更强的决策力、洞察发现力和流程优化能力…

文字表达的力量:如何通过编辑和校对优化内容

有效的文字表达能够为读者传递清晰的信息&#xff0c;产生深远的影响。通过编辑和校对优化内容&#xff0c;你可以提升文字表达的力量。以下是一些建议&#xff0c;帮助你在编辑和校对过程中优化内容&#xff0c;提高文字表达的力量。 1.确保内容的逻辑性和连贯性 一个具有说服…

15款好用的效果图设计制作软件推荐

无论你是设计师、工程师、室内设计师还是3D艺术家&#xff0c;你都必须熟悉效果图设计制作软件。 在这篇文章中&#xff0c;我们将向您介绍15个值得尝试的效果图设计制作软件&#xff01; 1.即时设计 即时设计是一款「专业UI设计工具」&#xff0c;不受平台限制&#xff0c;…

Spark学习笔记(一):Spark 基本原理

文章目录 1、Spark 整体架构1.1、Spark 集群角色1.1.1、Cluster Manager1.1.2、Worker Node1.1.3、Executor1.1.4、Application1.1.5、Driver1.1.6、Executor 2、Spark 运行基本流程2.1、RDD2.2、DAG2.3、DAGScheduler2.4、TaskScheduler2.5、Job2.6、Stage2.7、TaskSet Task2.…

宠物托管APP小程序系统开发 让专业的人帮你照顾宠物

宠物是人类的好朋友&#xff0c;为很多家庭带去了温馨与陪伴。但是对于很多养宠人士来说&#xff0c;在工作出差或者节假日出门旅游的时候&#xff0c;宠物的去处成为困扰他们的一大难题。那么有没有一个专业的机构可以在主人不在家的时候&#xff0c;为爱宠提供专业化的管护呢…