Synchronized与Java线程的关系

前言

​ Java多线程处理任务时,为了线程安全,通常会对共享资源进行加锁,拿到锁的线程才能进行访问共享资源。而加锁方式通过都是Synchronized锁或者Lock锁。

​ 那么多线程在协同工作的时候,线程状态的变化都与锁对象有关系。

Synchronized锁

​ Java采用synchronized关键字、以互斥同步的方式的解决线程安全问题。一般Synchronized主要用于同步代码块、实例方法、静态方法。

一、使用方式 字节码分析

// 使用
public synchronized void test1(){
}
public void test2(){synchronized(new Test()){}
}
public static synchronized void test3(){
}//反编译
public synchronized void test1();descriptor: ()Vflags: ACC_PUBLIC, ACC_SYNCHRONIZED    // herepublic void test2();descriptor: ()flags: ACC_PUBLICCode:stack=2, locals=3, args_size=10: new           #2                  // class com/easy/helloworld/Test3: dup4: invokespecial #3                  // Method "<init>":()V7: dup8: astore_19: monitorenter                   // here10: aload_111: monitorexit                    // here12: goto          2015: astore_216: aload_117: monitorexit                    // here18: aload_219: athrow20: returnpublic static synchronized void test3();descriptor: ()Vflags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED   // here

​ 由上可知,同步代码:通过moniterenter、moniterexit 关联到到一个monitor对象,进入时设置Owner为当前线程,计数+1、退出-1。除了正常出口的 monitorexit,还在异常处理代码里插入了 monitorexit。

​ 而实例方法、静态方式是隐式调用moniterenter、moniterexit。

二、Moniterenter、Moniterexit

​ monitorenter和monitorexit这两个jvm指令,主要是基于 Mark WordObject monitor来实现的。

​ 在 JVM 中,对象在内存中分为三块区域:

  • 对象头:由Mark WordKlass Point构成。

    • Mark Word(标记字段):用于存储对象自身的运行时数据,例如存储对象的HashCode,分代年龄、锁标志位等信息,是synchronized实现轻量级锁和偏向锁的关键。 64位JVM的Mark Word组成如下:

      img

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

  • 实例数据:这部分主要是存放类的数据信息,父类的信息。

  • 字节对齐:为了内存的IO性能,JVM要求对象起始地址必须是8字节的整数倍。对于不对齐的对象,需要填充数据进行对齐。

​ 在JDK 1.6之前,synchronized只有传统的锁机制,直接关联到monitor对象,存在性能上的瓶颈。在JDK 1.6后,为了提高锁的获取与释放效率,JVM引入了两种锁机制:偏向锁和轻量级锁。它们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下因使用传统锁机制带来的性能开销问题。这几种锁的实现和转换正是依靠对象头中的Mark Word

Synchronized锁机制

1、偏向锁

1)引入偏向锁的初衷

​ 在没有多线程竞争的情况下,尽量减少不必要的轻量级锁的执行。轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只依赖一次CAS原子指令。

​ 但在多线程竞争时,需要进行偏向锁撤销步骤,因此其撤销的开销必须小于节省下来的CAS开销,否则偏向锁并不能带来收益。JDK 1.6中默认开启偏向锁,可以通过-XX:-UseBiasedLocking来禁用偏向锁。在JDK15中,已经默认禁用偏向锁了。

2)关键字

prototype_header:JVM中的每个类有一个类似mark wordprototype_header用来标记该class的epoch和偏向开关等信息

匿名偏向状态:锁对象mark word标志位为101,且存储的Thread ID为空时的状态(即锁对象为偏向锁,且没有线程偏向于这个锁对象)。

Atomic::cmpxchg_ptrCAS函数。这个方法有三个参数,依次为exchange_valuedestcompare_value。如果dest的值为compare_value则更新为exchange_value,并返回compare_value。否则,不更新并返回实际原值

3)偏向锁流程

步骤 1、从当前线程的栈中找到一个空闲的Lock Record,并指向当前锁对象。

步骤 2、获取对象的markOop数据mark,即对象头的Mark Word;

步骤 3、判断锁对象的mark word是否是偏向模式,即低3位是否为101。若不是,进入步骤4。若是,计算anticipated_bias_locking_value,判断偏向状态:

步骤 3.1anticipated_bias_locking_value若为0,代表**偏向的线程是当前线程,且mark word的epoch等于class的epoch,**这种情况下直接执行同步代码块,什么都不用做。

步骤 3.2判断class的prototype_header是否为非偏向模式。若为非偏向模式,CAS尝试将对象恢复为无锁状态。无论cas是否成功都会进入轻量级锁逻辑。

步骤 3.3、**如果epoch偏向时间戳已过期,则需要重偏向。**利用CAS指令将锁对象的mark word替换为一个偏向当前线程且epoch为类的epoch的新的mark word

步骤 3.4CAS将偏向线程改为当前线程后,如果当前是匿名偏向(即对象头中的bit field存储的Thread ID为空)且无并发冲突,则能修改成功获取偏向锁,否则进入锁升级的逻辑。

步骤 4、走到一步会进行轻量级锁逻辑。构造一个无锁状态的mark word,然后存储到Lock Record

设置为无锁状态的原因是:轻量级锁解锁时是将对象头的mark wordcas替换为Lock Record中的Displaced Mark Word,所以设置为无锁状态。如果是锁重入,则将Lock RecordDisplaced Mark Word设置为null,放到栈帧中,起到计数作用。

注意

只有匿名偏向的对象才能进入偏向锁模式。JVM启动时会延时初始化偏向锁,默认是4000ms。初始化后会将所有加载的Klass的prototype header修改为匿名偏向样式。当创建一个对象时,会通过Klass的prototype_header来初始化该对象的对象头。

​ 简单的说,偏向锁初始化结束后,后续所有对象的对象头都为匿名偏向样式,在此之前创建的对象则为无锁状态。而对于无锁状态的锁对象,如果有竞争,会直接进入到轻量级锁。这也是为什么JVM启动前4秒对象会直接进入到轻量级锁的原因。

​ 那么为什么要延迟初始化?JVM启动时必不可免会有大量sync的操作,而偏向锁并不是都有利。如果开启了偏向锁,会发生大量锁撤销和锁升级操作,大大降低JVM启动效率。

4)偏向锁的撤销

偏向锁的 撤销(revoke)是一个很特殊的操作,为了执行撤销操作,需要等待全局安全点,此时所有的工作线程都停止了执行。

偏向锁的撤销操作并不是将对象恢复到无锁可偏向的状态,而是在偏向锁的获取过程中,发现可能存在竞争时,直接将一个被偏向的对象升级到被加了轻量级锁的状态。

场景说明:

​ 锁已经偏向线程A,此时线程B尝试获取锁。这种情况下会走到Mark标记的分支。

​ 如果需要撤销的是当前线程,只要遍历当前线程的栈就能拿到lock record,可以直接调用revoke_bias,不需要等到safe point再撤销。**在调用Object#hashcode时,也会走到该分支将为偏向锁的锁对象直接恢复为无锁状态。**若不是当前线程,会被push到VM Thread中等到safe point的时候再执行。

​ VMThread内部维护了一个VMOperationQueue类型的队列,用于保存内部提交的VM线程操作VM_operation。GC、偏向锁的撤销等操作都是在这里被执行。

撤销流程:

**步骤 1、查看偏向的线程是否存活,如果已经死亡,则直接撤销偏向锁。**JVM维护了一个集合存放所有存活的线程,通过遍历该集合判断某个线程是否存活。

步骤 2、偏向的线程是否还在同步块中,如果不在,则撤销偏向锁。如果在同步块中,执行步骤3。

​ 而是否在同步块的判断依据偏向锁的重入计数方式:在偏向锁的获取中,每次进入同步块的时候都会在栈中找到第一个可用(即栈中最高的)的Lock Record,将其obj字段指向锁对象。每次解锁的时候都会把最低的Lock Record移除掉,所以可以通过遍历线程栈中的Lock Record来判断是否还在同步块中。轻量级锁的重入也是基于Lock Record的计数来判断。

步骤 3、升级为轻量级锁。将偏向线程所有相关Lock RecordDisplaced Mark Word设置为null,再将最高位的Lock RecordDisplaced Mark Word 设置为无锁状态,然后将对象头指向最高位的Lock Record。这里没有用到CAS指令,因为是在safepoint,可以直接升级成轻量级锁。

小结

​ 当只有一个线程反复进入同步块时,偏向锁带来的性能开销基本可以忽略,但是当有其他线程尝试获得锁时,就需要等到safe point时将偏向锁撤销为无锁状态或升级为轻量级/重量级锁。

​ 因此,JVM中增加了一种批量重偏向/撤销的机制以减少锁撤销的开销,而mark word中的epoch也是在这里被大量应用。但无论怎么优化,偏向锁的撤销仍有一定不可避免的成本。如果业务场景存在大量多线程竞争,那偏向锁的存在不仅不能提高性能,而且会导致性能下降。

总结

如果当前锁已偏向其他线程||epoch值过期||class偏向模式关闭||获取偏向锁的过程中存在并发冲突,都会进入到锁膨胀InterpreterRuntime::monitorenter方法, 在该方法中会进行偏向锁撤销和升级。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2dzNki4f-1692344710003)(https://tech.youzan.com/content/images/2021/07/—.svg)]

2)轻量级锁

目的:

​ 在多线程交替执行同步块的情况下,尽量避免重量级锁使用的操作系统互斥量带来的开销。但是如果多个线程在同一时刻进入临界区,会导致轻量级锁膨胀升级重量级锁,所以轻量级锁的出现并非是要替代重量级锁。

​ 在偏向锁逻辑中,cas失败会执行到InterpreterRuntime::monitorenter。在轻量级锁逻辑中,如果当前线程不是轻量级锁的重入,也会执行到InterpreterRuntime::monitorenter

​ 其内部函数fast_enter的流程,主要逻辑为revoke_and_rebias:如果当前是偏向模式且偏向的线程还在使用锁,会将锁的mark word改为轻量级锁的状态,并将偏向的线程栈中的Lock Record修改为轻量级锁对应的形式(此时Lock Record是无锁状态),且返回值不是BIAS_REVOKED_AND_REBIASED,会继续执行slow_enter

slow_enter流程步骤

步骤 1markOop mark = obj->mark()方法获取对象的markOop数据mark;

步骤 2mark->is_neutral()方法判断mark是否为无锁状态,标识位001

步骤 3、如果mark处于无锁状态,把mark保存到BasicLock对象(Lock Record的属性)的displaced_header字段;

步骤 3.1、通过CAS尝试将Mark Word更新为指向BasicLock对象的指针,如果更新成功,表示竞争到锁,则执行同步代码,否则执行步骤4;

步骤 4、如果是重入,则设置Displaced Mark Word为null。

步骤 5、到这说明有多个线程竞争轻量级锁,轻量级锁需要膨胀升级为重量级锁;

轻量级锁的释放

​ 轻量级锁释放时需要将Displaced Mark Word替换回对象头的mark word中。如果CAS失败或者是重量级锁则进入到InterpreterRuntime::monitorexit方法中。monitorexit直接调用slow_exit方法释放Lock Record

最后执行的是如果是fast_exit方法,且是轻量级锁,尝试cas替换mark word。若解锁时有竞争,会调用inflate方法进行重量级锁膨胀,升级到到重量级锁后再执行exit方法。

3)重量级锁

​ 重量级锁通过对象内部的监视器(monitor)实现,其依赖于底层操作系统的Mutex Lock实现,需要额外的用户态到内核态切换的开销。由上文分析,slow_enter获取轻量级锁未成功时,会在inflate中完成锁膨胀。

​ 而inflate其中是一个for循环,主要是为了处理多线程同时调用inflate的情况。然后会根据锁对象的状态进行不同的处理:

1.已经是重量级状态,说明膨胀已经完成,返回并继续执行ObjectMonitor::enter方法。
2.如果是轻量级锁则需要进行膨胀操作。
3.如果是膨胀中状态,则进行忙等待。
4.如果是无锁状态则需要进行膨胀操作。

步骤过程

步骤 1、调用omAlloc获取一个可用的ObjectMonitor对象。在omAlloc方法中会先从线程私有monitor集合omFreeList中分配对象,如果omFreeList中已经没有monitor对象,则从JVM全局gFreeList中分配一批monitoromFreeList中;

步骤 2、通过CAS尝试将Mark Word设置为markOopDesc:INFLATING,标识当前锁正在膨胀中。如果CAS失败,说明同一时刻其它线程已经将Mark Word设置为markOopDesc:INFLATING,当前线程进行自旋等待膨胀完成。

步骤 3、如果CAS成功,设置monitor的各个字段:设置monitor的header字段为displaced mark word,owner字段为Lock Record,obj字段为锁对象等;

步骤 4、设置锁对象头的mark word为重量级锁状态,指向第一步分配的monitor对象;

monitor竞争

​ 当锁膨胀inflate执行完并返回对应的ObjectMonitor时,并不表示该线程竞争到了锁,真正的锁竞争发生在ObjectMonitor::enter方法中。

monitor等待

ObjectMonitor竞争失败的线程,通过自旋执行ObjectMonitor::EnterI方法等待锁的释放。

EnterI大致原理

一个ObjectMonitor对象包括两个同步队列(_cxq_EntryList) ,以及一个等待队列_WaitSet。cxq、EntryList 、WaitSet都是由ObjectWaiter构成的链表结构。其中,_cxq为单向链表,_EntryList为双向链表。

​ 当一个线程尝试获得重量级锁且没有竞争到时,该线程会被封装成一个ObjectWaiter对象插入到cxq的队列的队首,然后调用park函数挂起当前线程,进入BLOCKED状态。

​ 当线程释放锁时,会根据唤醒策略,从cxq或EntryList中挑选一个线程unpark唤醒。如果线程获得锁后调用Object#wait方法,则会将线程加入到WaitSet中,进入WAITING或TIMED_WAITING状态。

​ 当被Object#notify唤醒后,会将线程从WaitSet移动到cxq或EntryList中去,进入BLOCKED状态。需要注意的是,当调用一个锁对象的waitnotify方法时,若当前锁的状态是偏向锁或轻量级锁则会先膨胀成重量级锁。

monitor释放

​ 当某个持有锁的线程执行完同步代码块时,会进行锁的释放。在HotSpot中,通过退出monitor的方式实现锁的释放,并通知被阻塞的线程,具体实现位于ObjectMonitor::exit方法中。

Java线程

1、线程的实现

1)内核线程实现

内核线程(Kernel-Level Thread,KLT):由内核来完成线程切换,内核通过调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。 程序一般不会直接去使用内核线程,而是使用内核线程的一种高级接口——轻量级进程(Light Weight Process,LWP),也就是通常意义上的线程。

优点:每个LWP都是独立的调度单元。一个LWP被阻塞,不影响其他LWP。

缺点:基于KLT,耗资源。线程的创建、析构、同步都需要进行系统调用,频繁的用户态、内核态切换。

2)用户线程实现(User Thread,UT)

广义:非内核线程,都可认为是用户线程。(包括LWT,虽然LWT的大多操作都要映射到KLT)

狭义:完全建立在用户空间的线程库上,系统内核不能感知线程存在的实现。UT也只感知到掌管这些UT的进程P。

优点:用户线程的创建、同步、销毁和调度完全在用户态中完成,不需要内核的帮助。

缺点:线程的创建、销毁、切换和调度都是用户必须考虑到问题。“阻塞如何处理”、“多处理器系统中如何将线程映射到其他处理器上”这类问题解决起来将会异常困难。

3)混合模式。即存在用户线程,也存在轻量级进程

​ 用户线程的创建、切换、析构等操作依然廉价,可以支持大规模的用户线程并发,且可以使用内核线程提供的线程调度功能及处理器映射。

​ 线程的实现依赖操作系统支持的线程模型。在主流的操作系统上,hotspot、classic、art等虚拟机默认是 1:1的线程模型。在Solaris平台上,hotspot支持1:1、N:M两种线程模型。

2、线程的转换

​ 线程的状态,指的是Thread 类中threadStatus的值。

​ 创建Thread 类的对象,才能谈其状态。这个时候,线程t就处于新建状态。但他还不是“线程”。**调用start()后,会执行一个native方法创建内核线程。 这时候才有一个真正的线程创建出来,并即刻开始运行。这个内核线程与线程t进行1:1的映射。**这时候t具备运行能力,进入RUNNABLE状态。 RUNNABLE可以细分为READY和RUNNING,两者的区别只是是否等待到了资源并开始运行。

​ 处于RUNNABLE且未运行的线程,会进入一个就绪队列中,等待操作系统的调度。处于就绪队列的线程都在等待资源,这个资源可以是cpu的时间片、也可以是系统的IO。

3、线程相关方法

1)wait

​ 通过object获得objectMonitor,将Thread封装成OjectWaiter对象,然后addWaiter将它插入waitSet中,进入waiting或timed_waiting状态。最后释放锁,并通过底层的park方法挂起线程;

2)notify

​ 通过object获得objectMonitor,调用objectMonitor的notify方法。这个notify最后会走到ObjectMonitor::DequeueWaiter方法,获取waitSet列表中的第一个ObjectWaiter节点。并根据不同的策略,将取出来的ObjectWaiter节点,加入到EntryListcxq中。 notifyAll的实现类似于notify,主要差别在多了个for循环。

notifynotifyAll并不会立即释放所占有的ObjectMonitor对象,其真正释放ObjectMonitor的时间点是在执行monitorexit指令。

一旦释放ObjectMonitor对象了,entryListcxq中的ObjectWaiter节点会依据QMode所配置的策略,通过ExitEpilog方法唤醒取出来的ObjectWaiter节点。被唤醒的线程,继续参与monitor的竞争。若竞争失败,重新进入BLOCKED状态

3)join

join的本质仍然是 wait() 方法。在使用join时,JVM会帮我们隐式调用notify,因此我们不需要主动notify唤醒主线程。 而sleep()方法最终是调用SleepEvent对象的park方法

4)sleep

Thread.sleep在jvm层面上是调用thread中SleepEvent对象的park()方法实现阻塞线程,在此过程中会通过判断时间戳来决定线程的睡眠时间是否达到了指定的毫秒。

5)park

parkunpark方法也与同步语义无关。每个线程都与一个许可(permit)关联。unpark函数为线程提供permit,线程调用park函数则等待并消耗permit。

小结

​ 1、 JVM线程状态不代表内核线程状态。

​ 2、BLOCKED的线程一定处于entryList或cxq中,而处于WAITING和TIMED WAITING的线程,可能是由于执行了sleep或park进入该状态,不一定在waitSet中。也就是说,处于BLOCKED状态的线程一定是与同步相关。由这可延伸出,调用 jdk 的 lock并获取不到锁的线程,进入的是 WAITING 或 TIMED_WAITING 状态,而不是BLOCKED状态。

总结

在这里插入图片描述

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

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

相关文章

CMake教程6:调用lib、dll

之前我们学到了如何编写一个可执行程序和Library&#xff0c;在继续学习之前&#xff0c;需要解释下target&#xff0c;在cmake中我们可以给executable和library设置一个target名字&#xff0c;这样可以方便我们在后续对target进行更加详细的属性设置。 本节我们将学习如何在项…

Hlang--用Python写个解释器

文章目录 前言流程数学解释器结果封装数的操作运行时异常运行解释实现总结前言 没错今天提前来做这个东西,昨天晚上干这个玩意差不多干了两个多小时才搞定,导致凌晨2点才睡觉,最要命的是,写着写着突然想到有一道线代理解错了,一个晚上,做梦全是这两个东西。尤其是晚上效…

Mr. Cappuccino的第61杯咖啡——Spring之BeanPostProcessor

Spring之BeanPostProcessor 概述基本使用项目结构项目代码运行结果源代码 常用处理器项目结构项目代码执行结果 概述 BeanPostProcessor&#xff1a;Bean对象的后置处理器&#xff0c;负责对已创建好的bean对象进行加工处理&#xff1b; BeanPostProcessor中的两个核心方法&am…

【快应用】如何避免通知栏提示快应用一直获取地理位置

【关键词】 地理位置、subscribe、unsubscribe 【问题背景】 快应用中调用geolocation.subscribe接口获取地理位置&#xff0c;即使在定位完成后&#xff0c;会在通知栏一直显示某某快应用在获取地理位置&#xff0c;为了避免用户认为一直在获取他的位置&#xff0c;导致用户…

详解web前端长度单位(px、em、rem、%、vw/vh、vmin/vmax、vm、calc())

1&#xff09;简介 在前端开发中&#xff0c;会遇到各种不同类型的长度单位&#xff0c;比如px,em,rem等。 而整体的长度单位分为两大类&#xff1a; 绝对长度单位包括&#xff1a;in,cm, mm, q, pt, pc, px 相对长度单位包括&#xff1a;em, rem, %, vw, vh, vmax, vmin, vm,…

react 09之状态管理工具1 redux+ react-thunk的使用实现跨组件状态管理与异步操作

目录 react 09之状态管理工具1 redux react-thunk的使用实现跨组件状态管理与异步操作store / index.js store的入口文件index.js 在项目入口文件 引入store / actionType.js 定义action的唯一标识store / reducers / index.jsstore / actions / form.jsstore / reducers / for…

政务、商务数据资源有效共享:让数据上“链”,记录每一个存储过程!

数据上链是目前“区块链”最常见的场景。因为链上所有参与方都分享了统一的事实来源&#xff0c;所有人都可以即时获得最新的信息&#xff0c;数据可用不可见。因此&#xff0c;不同参与方之间的协作效率得以大幅提高。同时&#xff0c;因为区块链上的数据难以篡改&#xff0c;…

【CI/CD】Rancher K8s

Rancher & K8s Rancher 和 K8s 的关系是什么&#xff1f;K8s 全称为 Kubernetes&#xff0c;它是一个开源的&#xff0c;用于管理云平台中多个主机上的容器化的应用。而 Rancher 是一个完全开源的企业级多集群 Kubernetes 管理平台&#xff0c;实现了 Kubernetes 集群在混合…

zabbix自动注册服务器以及部署代理服务器

文章目录 Zabbix自动注册服务器及部署代理服务器一.zabbix自动注册1.什么是自动注册2.环境准备3.zabbix客户端配置4.在 Web 页面配置自动注册5.验证自动注册 二.部署 zabbix 代理服务器1.分布式监控的作用&#xff1a;2.环境部署3.代理服务器配置4.客户端配置5.web页面配置5.1 …

基于OpenCV的人脸识别和模型训练系统(万字详解)

前言 我们身边的人脸识别有车站检票&#xff0c;监控人脸&#xff0c;无人超市&#xff0c;支付宝人脸支付&#xff0c;上班打卡&#xff0c;人脸解锁手机。 人脸检测是人脸识别系统组成的关键部分之一&#xff0c;其目的是检测出任意给定图片中的包含的一个或多个人脸&#…

基于扩频的数字视频水印嵌入和检测算法matlab仿真

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 5.算法完整程序工程 1.算法运行效果图预览 2.算法运行软件版本 matlab2022a 3.部分核心程序 ................................................................. for j 1:length(attens…

生成国密密钥对

在线生成国密密钥对 生成的密钥对要妥善保管&#xff0c;丢失是无法找回的。

JVM——类的生命周期

文章目录 类加载过程加载验证准备解析初始化 卸载 一个类的完整生命周期如下&#xff1a; 类加载过程 Class 文件需要加载到虚拟机中之后才能运行和使用&#xff0c;那么虚拟机是如何加载这些 Class 文件呢&#xff1f; 系统加载 Class 类型的文件主要三步:加载->连接->…

Spring中Bean的生命周期以及Bean的单例与多例模式

一. Bean的生命周期 bean的生命周期可以表达为&#xff1a;bean的定义➡bean的初始化➡bean的使用➡bean的销毁 Bean的初始化过程 1&#xff09;通过XML、Java annotation&#xff08;注解&#xff09;以及Java Configuration&#xff08;配置类&#xff09; 等方式加载Bea…

同步jenkinsfile流水线(sync-job)

环境 变量&#xff1a;env&#xff08;环境变量&#xff1a;sit/dev/simulation/prod/all&#xff09;&#xff0c;job&#xff08;job-name/all&#xff09;目录&#xff1a;/var/lib/jenkins/jenkinsfile environment.json&#xff1a; [roottest-01 jenkinsfile]# cat env…

elementUI时间选择器el-time-picker的坑

//开始时间<el-time-pickerplaceholder"选择时间":format"HH:mm:ss" //显示的时间样式value-format"HH:mm:ss" //绑定值的样式 //不给默认为 Date 对象值&#xff1a;"2023-07-31T16:00:00.000Z"v-model"FormData.startTime&…

JavaWeb-Servlet服务连接器(终)

上一篇文章JavaWeb-Servlet服务连接器&#xff08;三&#xff09;_Alphamilk的博客-CSDN博客 目录 1.ServletContext通信 会话技术Cookie与Session 1.Cookie 2.Session 1.ServletContext通信 概念&#xff1a;代表了整个web应用&#xff0c;用于与服务器实现通信 可以通…

docker-php扩展

生成扩展骨架 环境&#xff1a;docker-compose、php74 1.本地要有一份 php-src git clone https://github.com/php/php-src.git cd php-src git checkout PHP-7.4.52.\www\php-src\ext可以看到有一个 ext_skel.php 文件 3.通过ext_skel.php脚本创建了一个hello扩展&#xf…

同步请求和异步请求

同步请求和异步请求是在网络编程中常用的两种通信模式&#xff0c;它们有以下区别&#xff1a; 同步请求&#xff1a; 在发送一个请求后&#xff0c;程序会一直等待服务器返回响应&#xff0c;期间无法进行其他操作。请求发出后&#xff0c;程序会阻塞在请求处&#xff0c;直…

变压器故障诊断(python代码,逻辑回归/SVM/KNN三种方法同时使用,有详细中文注释)

视频效果&#xff1a;变压器三种方法下故障诊断Python代码_哔哩哔哩_bilibili代码运行要求&#xff1a;tensorflow版本>2.4.0,Python>3.6.0即可&#xff0c;无需修改数据路径。 1.数据集介绍&#xff1a; 采集数据的设备照片 变压器在电力系统中扮演着非常重要的角色。…