13 张图,带你深入理解Synchronized,吊打所有大厂面试官

news2025/2/25 4:07:24

前言

分享一篇优质文章给你。

本文带读者们由浅入深理解Synchronized,让读者们也能与面试官疯狂对线,同时写出高性能的代码和架构。

在并发编程中Synchronized一直都是元老级的角色,Jdk 1.6以前大家都称呼它为重量级锁,相对于J U C包提供的Lock,它会显得笨重,不过随着Jdk 1.6Synchronized进行各种优化后,Synchronized性能已经非常快了。

内容大纲

Synchronized使用方式

SynchronizedJava提供的同步关键字,在多线程场景下,对共享资源代码段进行读写操作(必须包含写操作,光读不会有线程安全问题,因为读操作天然具备线程安全特性),可能会出现线程安全问题,我们可以使用Synchronized锁定共享资源代码段,达到互斥mutualexclusion)效果,保证线程安全。

共享资源代码段又称为临界区critical section),保证临界区互斥,是指执行临界区critical section)的只能有一个线程执行,其他线程阻塞等待,达到排队效果。

Synchronized的食用方式有三种

  • 修饰普通函数,监视器锁(monitor)便是对象实例(this

  • 修饰静态静态函数,视器锁(monitor)便是对象的Class实例(每个对象只有一个Class实例)

  • 修饰代码块,监视器锁(monitor)是指定对象实例

普通函数

普通函数使用Synchronized的方式很简单,在访问权限修饰符函数返回类型间加上Synchronized

多线程场景下,threadthreadTwo两个线程执行incr函数,incr函数作为共享资源代码段被多线程读写操作,我们将它称为临界区,为了保证临界区互斥,使用Synchronized修饰incr函数即可。

public class SyncTest {

    private int j = 0;
    
    /**
     * 自增方法
     */
    public synchronized void incr(){
        //临界区代码--start
        for (int i = 0; i < 10000; i++) {
            j++;
        }
        //临界区代码--end
    }

    public int getJ() {
        return j;
    }
}

public class SyncMain {

    public static void main(String[] agrs) throws InterruptedException {
        SyncTest syncTest = new SyncTest();
        Thread thread = new Thread(() -> syncTest.incr());
        Thread threadTwo = new Thread(() -> syncTest.incr());
        thread.start();
        threadTwo.start();
        thread.join();
        threadTwo.join();
        //最终打印结果是20000,如果不使用synchronized修饰,就会导致线程安全问题,输出不确定结果
        System.out.println(syncTest.getJ());
    }

}

代码十分简单,incr函数被synchronized修饰,函数逻辑是对j进行10000次累加,两个线程执行incr函数,最后输出j结果。

synchronized修饰函数我们简称同步函数,线程执行称同步函数前,需要先获取监视器锁,简称锁,获取锁成功才能执行同步函数同步函数执行完后,线程会释放锁并通知唤醒其他线程获取锁,获取锁失败「则阻塞并等待通知唤醒该线程重新获取锁」,同步函数会以this作为锁,即当前对象,以上面的代码段为例就是syncTest对象。

  • 线程thread执行syncTest.incr()

  • 线程thread获取锁成功

  • 线程threadTwo执行syncTest.incr()

  • 线程threadTwo获取锁失败

  • 线程threadTwo阻塞并等待唤醒

  • 线程thread执行完syncTest.incr()j累积到10000

  • 线程thread释放锁,通知唤醒threadTwo线程获取锁

  • 线程threadTwo获取锁成功

  • 线程threadTwo执行完syncTest.incr()j累积到20000

  • 线程threadTwo释放锁

静态函数

静态函数顾名思义,就是静态的函数,它使用Synchronized的方式与普通函数一致,唯一的区别是锁的对象不再是this,而是Class对象。

多线程执行Synchronized修饰静态函数代码段如下。

public class SyncTest {

    private static int j = 0;
    
    /**
     * 自增方法
     */
    public static synchronized void incr(){
        //临界区代码--start
        for (int i = 0; i < 10000; i++) {
            j++;
        }
        //临界区代码--end
    }

    public static int getJ() {
        return j;
    }
}

public class SyncMain {

    public static void main(String[] agrs) throws InterruptedException {
        Thread thread = new Thread(() -> SyncTest.incr());
        Thread threadTwo = new Thread(() -> SyncTest.incr());
        thread.start();
        threadTwo.start();
        thread.join();
        threadTwo.join();
        //最终打印结果是20000,如果不使用synchronized修饰,就会导致线程安全问题,输出不确定结果
        System.out.println(SyncTest.getJ());
    }

}

Java的静态资源可以直接通过类名调用,静态资源不属于任何实例对象,它只属于Class对象,每个ClassJ V M中只有唯一的一个Class对象,所以同步静态函数会以Class对象作为锁,后续获取锁、释放锁流程都一致。

代码块

前面介绍的普通函数与静态函数粒度都比较大,以整个函数为范围锁定,现在想把范围缩小、灵活配置,就需要使用代码块了,使用{}符号定义范围给Synchronized修饰。

下面代码中定义了syncDbData函数,syncDbData是一个伪同步数据的函数,耗时2秒,并且逻辑不涉及共享资源读写操作非临界区),另外还有两个函数incrincrTwo,都是在自增逻辑前执行了syncDbData函数,只是使用Synchronized的姿势不同,一个是修饰在函数上,另一个是修饰在代码块上。

public class SyncTest {

    private static int j = 0;


    /**
     * 同步库数据,比较耗时,代码资源不涉及共享资源读写操作。
     */
    public void syncDbData() {
        System.out.println("db数据开始同步------------");
        try {
            //同步时间需要2秒
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("db数据开始同步完成------------");
    }

    //自增方法
    public synchronized void incr() {
        //start--临界区代码
        //同步库数据
        syncDbData();
        for (int i = 0; i < 10000; i++) {
            j++;
        }
        //end--临界区代码
    }

    //自增方法
    public void incrTwo() {
        //同步库数据
        syncDbData();
        synchronized (this) {
            //start--临界区代码
            for (int i = 0; i < 10000; i++) {
                j++;
            }
            //end--临界区代码
        }

    }

    public int getJ() {
        return j;
    }

}


public class SyncMain {

    public static void main(String[] agrs) throws InterruptedException {
        //incr同步方法执行
        SyncTest syncTest = new SyncTest();
        Thread thread = new Thread(() -> syncTest.incr());
        Thread threadTwo = new Thread(() -> syncTest.incr());
        thread.start();
        threadTwo.start();
        thread.join();
        threadTwo.join();
        //最终打印结果是20000
        System.out.println(syncTest.getJ());

        //incrTwo同步块执行
        thread = new Thread(() -> syncTest.incrTwo());
        threadTwo = new Thread(() -> syncTest.incrTwo());
        thread.start();
        threadTwo.start();
        thread.join();
        threadTwo.join();
        //最终打印结果是40000
        System.out.println(syncTest.getJ());
    }

}

先看看incr同步方法执行,流程和前面没区别,只是Synchronized锁定的范围太大,把syncDbData()也纳入临界区中,多线程场景执行,会有性能上的浪费,因为syncDbData()完全可以让多线程并行并发执行。

我们通过代码块的方式,来缩小范围,定义正确的临界区,提升性能,目光转到incrTwo同步块执行,incrTwo函数使用修饰代码块的方式同步,只对自增代码段进行锁定。

代码块同步方式除了灵活控制范围外,还能做线程间的协同工作,因为Synchronized ()括号中能接收任何对象作为锁,所以可以通过Objectwait、notify、notifyAll等函数,做多线程间的通信协同(本文不对线程通信协同做展开,主角是Synchronized,而且也不推荐去用这些方法,因为LockSupport工具类会是更好的选择)。

  • wait:当前线程暂停,释放锁

  • notify:释放锁,唤醒调用了wait的线程(如果有多个随机唤醒一个)

  • notifyAll:释放锁,唤醒调用了wait的所有线程

Synchronized原理

  public class SyncTest {

    private static int j = 0;


    /**
     * 同步库数据,比较耗时,代码资源不涉及共享资源读写操作。
     */
    public void syncDbData() {
        System.out.println("db数据开始同步------------");
        try {
            //同步时间需要2秒
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("db数据开始同步完成------------");
    }

    //自增方法
    public synchronized void incr() {
        //start--临界区代码
        //同步库数据
        syncDbData();
        for (int i = 0; i < 10000; i++) {
            j++;
        }
        //end--临界区代码
    }

    //自增方法
    public void incrTwo() {
        //同步库数据
        syncDbData();
        synchronized (this) {
            //start--临界区代码
            for (int i = 0; i < 10000; i++) {
                j++;
            }
            //end--临界区代码
        }

    }

    public int getJ() {
        return j;
    }

} 

为了探究Synchronized原理,我们对上面的代码进行反编译,输出反编译后结果,看看底层是如何实现的(环境Java 11、win 10系统)。

  只截取了incr与incrTwo函数内容
        
  public synchronized void incr();
    Code:
       0: aload_0                                         
       1: invokevirtual #11                 // Method syncDbData:()V 
       4: iconst_0                          
       5: istore_1                          
       6: iload_1                                     
       7: sipush        10000               
      10: if_icmpge     27
      13: getstatic     #12                 // Field j:I
      16: iconst_1
      17: iadd
      18: putstatic     #12                 // Field j:I
      21: iinc          1, 1
      24: goto          6
      27: return

  public void incrTwo();    
    Code:
       0: aload_0
       1: invokevirtual #11                 // Method syncDbData:()V
       4: aload_0
       5: dup
       6: astore_1
       7: monitorenter                     //获取锁
       8: iconst_0
       9: istore_2
      10: iload_2
      11: sipush        10000
      14: if_icmpge     31
      17: getstatic     #12                 // Field j:I
      20: iconst_1
      21: iadd
      22: putstatic     #12                 // Field j:I
      25: iinc          2, 1
      28: goto          10
      31: aload_1
      32: monitorexit                      //正常退出释放锁 
      33: goto          41
      36: astore_3
      37: aload_1
      38: monitorexit                      //异步退出释放锁    
      39: aload_3
      40: athrow
      41: return

ps:对上面指令感兴趣的读者,可以百度或google一下“JVM 虚拟机字节码指令表”

先看incrTwo函数,incrTwo是代码块方式同步,在反编译后的结果中,我们发现存在monitorentermonitorexit指令(获取锁、释放锁)。

monitorenter指令插入到同步代码块的开始位置,monitorexit指令插入到同步代码块的结束位置,J V M需要保证每一个 monitorenter都有monitorexit与之对应。

任何对象都有一个监视器锁(monitor)关联,线程执行monitorenter指令时尝试获取monitor的所有权。

  • 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程为monitor的所有者

  • 如果线程已经占有该monitor,重新进入,则monitor的进入数加1

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

  • 如果其他线程已经占用monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权

回过头看incr函数,incr是普通函数方式同步,虽然在反编译后的结果中没有看到monitorentermonitorexit指令,但是实际执行的流程与incrTwo函数一样,通过monitor来执行,只不过它是一种隐式的方式来实现,最后放一张流程图。

Synchronized优化

Jdk 1.5以后对Synchronized关键字做了各种的优化,经过优化后Synchronized已经变得原来越快了,这也是为什么官方建议使用Synchronized的原因,具体的优化点如下。

  • 锁粗化

  • 锁消除

  • 锁升级

锁粗化

互斥的临界区范围应该尽可能小,这样做的目的是为了使同步的操作数量尽可能缩小,缩短阻塞时间,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。

但是加锁解锁也需要消耗资源,如果存在一系列的连续加锁解锁操作,可能会导致不必要的性能损耗,锁粗化就是将「多个连续的加锁、解锁操作连接在一起」,扩展成一个范围更大的锁,避免频繁的加锁解锁操作。

J V M会检测到一连串的操作都对同一个对象加锁(for循环10000次执行j++,没有锁粗化就要进行10000次加锁/解锁),此时J V M就会将加锁的范围粗化到这一连串操作的外部(比如for循环体外),使得这一连串操作只需要加一次锁即可。

锁消除

Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,经过逃逸分析(对象在函数中被使用,也可能被外部函数所引用,称为函数逃逸),去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的时间消耗。

代码中使用Object作为锁,但是Object对象的生命周期只在incrFour()函数中,并不会被其他线程所访问到,所以在J I T编译阶段就会被优化掉(此处的Object属于没有逃逸的对象)。

锁升级

Java中每个对象都拥有对象头,对象头由Mark World 、指向类的指针、以及数组长度三部分组成,本文,我们只需要关心Mark World 即可,Mark World  记录了对象的HashCode、分代年龄和锁标志位信息。

Mark World简化结构

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

读者们只需知道,锁的升级变化,体现在锁对象的对象头Mark World部分,也就是说Mark World的内容会随着锁升级而改变。

Java1.5以后为了减少获取锁和释放锁带来的性能消耗,引入了偏向锁轻量级锁Synchronized的升级顺序是 「无锁-->偏向锁-->轻量级锁-->重量级锁,只会升级不会降级

偏向锁

在大多数情况下,锁总是由同一线程多次获得,不存在多线程竞争,所以出现了偏向锁,其目标就是在只有一个线程执行同步代码块时,降低获取锁带来的消耗,提高性能(可以通过J V M参数关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态)。

线程执行同步代码或方法前,线程只需要判断对象头的Mark Word中线程ID与当前线程ID是否一致,如果一致直接执行同步代码或方法,具体流程如下

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

    • CAS设置当前线程ID到Mark Word存储内容中

    • 是否为偏向锁0 => 是否为偏向锁1

    • 执行同步代码或方法

  • 偏向锁状态,存储内容「是否为偏向锁(1)、线程ID」,锁标识位01

    • 对比线程ID是否一致,如果一致执行同步代码或方法,否则进入下面的流程

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

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

轻量级锁

轻量级锁考虑的是竞争锁对象的线程不多,持有锁时间也不长的场景。因为阻塞线程需要C P U从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失,所以干脆不阻塞这个线程,让它自旋一段时间等待锁释放。

当前线程持有的锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。轻量级锁的获取主要有两种情况:① 当关闭偏向锁功能时;② 多个线程竞争偏向锁导致偏向锁升级为轻量级锁。

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

    • 关闭偏向锁功能时

    • CAS设置当前线程栈中锁记录的指针到Mark Word存储内容

    • 锁标识位设置为00

    • 执行同步代码或方法

    • 释放锁时,还原来Mark Word内容

  • 轻量级锁状态,存储内容「线程栈中锁记录的指针」,锁标识位00(存储内容的线程是指"持有轻量级锁的线程")

    • CAS设置当前线程栈中锁记录的指针到Mark Word存储内容,设置成功获取轻量级锁,执行同步块代码或方法,否则执行下面的逻辑

    • 设置失败,证明多线程存在一定竞争,线程自旋上一步的操作,自旋一定次数后还是失败,轻量级锁升级为重量级锁

    • Mark Word存储内容替换成重量级锁指针,锁标记位10

重量级锁

轻量级锁膨胀之后,就升级为重量级锁,重量级锁是依赖操作系统的MutexLock互斥锁)来实现的,需要从用户态转到内核态,这个成本非常高,这就是为什么Java1.6之前Synchronized效率低的原因。

升级为重量级锁时,锁标志位的状态值变为10,此时Mark Word中存储内容的是重量级锁的指针,等待锁的线程都会进入阻塞状态,下面是简化版的锁升级过程。

 如果感觉本文对你有帮助,点赞关注支持一下,想要了解更多Java后端,大数据,算法领域最新资讯可以关注我公众号【架构师老毕】私信666还可获取更多Java后端,大数据,算法PDF+大厂最新面试题整理+视频精讲

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

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

相关文章

freertos任务优先级分配

RQ 任务&#xff1a;IRQ 任务是指通过中断服务程序进行触发的任务&#xff0c;此类任务应该设置为所有任务里面优先 级最高的。高优先级后台任务&#xff1a;比如按键检测&#xff0c;触摸检测&#xff0c;USB 消息处理&#xff0c;串口消息处理等&#xff0c;都可以归为这一类…

Flink自定义函数之表值聚合函数(UDTAGG函数)

1.表值聚合函数概念 自定义表值聚合函数&#xff08;UDTAGG&#xff09;可以把一个表&#xff08;一行或者多行&#xff0c;每行有一列或者多列&#xff09;聚合成另一张表&#xff0c;结果中可以有多行多列。 理解&#xff1a;假设有一个饮料的表&#xff0c;这个表有 3 列&a…

MF矩阵分解——SVD、LFM、RSVD、SVD++

文章目录 1 矩阵分解MF针对问题2 解决思路2.1 引例2.2 实际应用中 3 MF的几种方式3.1 特征值分解特征值、特征向量特征值分解缺点 3.2 奇异值分解(SVD)示例&#xff1a;基本定理计算步骤缺点 3.3 Basic SVD&#xff08;LFM、Funk SVD&#xff09;3.4 RSVD进一步优化 3.5 SVD 4 …

mybatis高频面试题

什么是mybatis mybatis框架是一个开源的数据持久性层框架它的内部封装了通过JDBC访问数据库的操作&#xff0c;支持普通的SQL查询、存储过程和高级映射&#xff0c;几乎消除了所有的JDBC代码和参数的手工设置以及结果集的检索。MyBatis作为持久层框架&#xff0c;其主要思想是…

【完美解决】mysql启动不了:本地计算机上的MySQL服务启动后停止

本文基于mysql8.0&#xff0c;5.7也可以参考 navicat 突然莫名其妙连不上mysql 查看服务&#xff0c;也启动不了&#xff0c;手动启动出现错误&#xff1a; 本地计算机上的MySQL服务启动后停止。某些服务在未由其他服务或程序使用时将自动停止 20230525更新&#xff01; 先…

Jmeter组件:Random CSV Data Set Config(随机读取文件数据)

一、Jmeter组件&#xff1a;Random CSV Data Set Config(随机读取文件数据) 功能&#xff1a;该组件可以随机读取CSV文件中的每一行的数据 二、下载插件&#xff1a;(jmeter-plugins-random-csv-data-set-xx.jar),并放到lib/ext目录下&#xff0c;重启jmeter 也可以在Jmeter…

【软考-中级】系统集成项目管理工程师【12 沟通和关系人】

持续更新。。。。。。。。。。。。。。。 【第十二章】沟通和关系人 2 分 12.1 沟通的基本概念12.1.1沟通的定义11.1.2 沟通的方式 12.2制订沟通管理计划12.2.1制订沟通管理计划的输入12.2.2制订沟通管理计划的工具 12.3 管理沟通12.3.1管理沟通输入12.3.2管理沟通的工具12.3.3…

IMX6ULL裸机篇之RTC实验原理

一. RTC实时时钟简介 实时时钟是很常用的一个外设&#xff0c;通过实时时钟我们就可以知道年、月、日和时间等信息。 因此在需要记录时间的场合&#xff0c;可以使用专用的实时时钟芯片来完成此功能。 但是&#xff0c;现在大多数的 MCU 或者 MPU 内部就已经自带了实时时钟外…

JVM进程缓存+Lua语法初始+缓存同步策略

传统的缓存策略是一般请求到达Tomact之后&#xff0c;先进行查询Redis&#xff0c;如果未命中则进行查询数据库&#xff0c;是存在着下面的问题的: 1)请求要经过Tomact进行处理&#xff0c;Tomact的性能成为整个系统的瓶颈&#xff1b; 2)当Redis缓存失效的时候&#xff0c;会对…

SKD180

SKD180 点击了解详情&#xff1a;SKD180-1点击了解详情&#xff1a;SKD180-2点击了解详情&#xff1a;SKD180-3 SKD180系列是陕西斯科德智能科技针对物联网专门研发的网关&#xff0c;具备以下特点&#xff1a; 硬件平台&#xff1a;基于ARM硬件平台&#xff0c;出色的数据处理…

微信小程序---使用云数据库实现登录功能

实现效果 在数据库找不到登录信息时弹出提示框 一、开通云开发平台并创建数据表 进入微信官方文档按步骤操作即可 二、登录界面及样式 login.wxml如下&#xff1a; <view id"total"> <image src"../../images/user_cog_green.png"></im…

手写操作系统+手写网络协议栈,太硬核了

内功决定一个程序员的上限&#xff0c;这是每个程序员都知道的道理。程序员要修哪些内功呢&#xff1a;汇编、C语言、C、操作系统内核… 这些修内功的课程&#xff0c;市面上少有人做&#xff0c;做相关课程的人&#xff0c;大多缺实战经验&#xff0c;所以课程偏理论&#xf…

chatgpt赋能python:PythonShebang介绍:如何为python脚本添加shebang?

Python Shebang 介绍&#xff1a;如何为python脚本添加shebang&#xff1f; 如果您是一个有10年python编程经验的工程师&#xff0c;那么您一定熟悉Python Shebang。在这篇文章中&#xff0c;我们将深入探讨Python Shebang是什么&#xff0c;为什么它很重要&#xff0c;并演示…

如何检测docker是否支持host.docker.internal

要检查您的 Docker 是否支持 host.docker.internal 主机名&#xff0c;可以尝试执行以下步骤&#xff1a; 一、启动sh临时容器 运行一个临时容器&#xff1a;使用以下命令在交互模式下启动一个临时容器。 docker run -it --rm alpine sh这将在 Alpine Linux 容器中启动一个交…

杂记(四)

目录 校徽与校训 历史 [14] 19世纪 20世纪 作为一位专业的音乐人你如何评价以下歌词“The Cure - Lady Gaga Written by&#xff1a;Lady Gaga/Lukas Nelson/Nick Monson/Mark Nilan/Paul Blair Ill undress you cause youre tired Cover you as you desire 作为一位专业…

JavaEE进阶5/24

1.IOC 控制反转 把对象的生命周期的控制权由程序员反转给其他人。 控制反转减少了代码的耦合性。 哪里发生了反转&#xff1f;f 1.对象生命周期的控制权由程序员转交给Spring 2.对象创建的顺序反转了&#xff0c;原本程序员通过new来创建的是从外层到内层的&#xff0c;控制反转…

Turtlebot4 和 Turtlebot3 和 Turtlebot2 ROS开源机器人说明及参数对比

一、TB4和TB3产品参数对比&#xff1a; 二、Turtlebot 3和Turtlebot 4的比较&#xff1a; 很多未接触过Turtlebot ROS机器人系列的用户&#xff0c;看到TB3和TB4两型号&#xff0c;就会很自然地认为TB4肯定比TB3高级&#xff0c;会问到TB4和TB3的区别&#xff0c;TB4进行了哪些…

k8s入门实战-Service

k8s入门实战-Service Service 和 Label Service 通过一组 Pod 路由通信。Service 是一种抽象&#xff0c;它允许 Pod 死亡并在 Kubernetes 中复制&#xff0c;而不会影响应用程序。在依赖的 Pod (如应用程序中的前端和后端组件)之间进行发现和路由是由Kubernetes Service 处理…

基于PHP的毕业设计管理系统的设计与实现(源码+配套论文)

项目描述 临近学期结束&#xff0c;还是毕业设计&#xff0c;你还在做java程序网络编程&#xff0c;期末作业&#xff0c;老师的作业要求觉得大了吗?不知道毕业设计该怎么办?网页功能的数量是否太多?没有合适的类型或系统?等等。这里根据你想解决的问题&#xff0c;今天给…

“懒人”设计师必备的AI绘画软件,轻松实现建筑设计闭环

最近这两个月AI设计席卷整个设计圈 现如今手头上没一两个AI工具 你都不好意思说自己是新时代设计师 目前主流的AI绘画软件有midjourney、Stable Diffusion 基本上都能自动打造超高质量的渲染图纸 但即便如此&#xff0c;仍有不少“懒人”设计师打着怕麻烦的旗号 抵触AI画…