6.volatile与JMM

文章目录

  • 被 volatile 修饰的变量有两大特点
    • volatile 的内存语义
    • volatile 凭什么可以保证有序性和可见性?
  • 内存屏障(面试重点)
    • 解读 volatile 的两大特性
    • 内存屏障
  • volatile 特性
    • 两大类
      • 读屏障(Load Barrier)
      • 写屏障(Store Barrier)
    • 四小类
      • C++源码分析
      • 四类屏障特点
    • 如何保证有序性?
      • happens-before 之 volatile 变量规则
      • JMM 就内存屏障插入策略分为 4 中规则
    • 如何保证可见性?
      • volatile 变量的读写过程
    • 无法保证原子性
      • 关于上述现象的解释
      • 结论
      • 面试回答
    • 指令禁止重排序
      • 案例说明
      • volatile 底层实现通过内存屏障
      • 代码案例分析
  • volatile 的使用场景
    • 单一赋值可用,存在复合运算赋值不可用(i++等)
    • 状态标志,判断业务是否结束
    • 开销较低的读,写锁策略
    • DCL 双端锁的发布
      • 问题描述
      • 解决方案
  • Summary

被 volatile 修饰的变量有两大特点

  • 可见性,某个线程对该变量的修改对其它线程可见
  • 有序性,允许指令重排序,有时也需要禁止指令重排序

volatile 的内存语义

  • 当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量立即刷新回主内存中
  • 当读一个 volatile 变量时间,JMM 会把本地内存置为无效,重新回到主内存中读取最新共享变量
  • 因此 volatile 的写内存语义是直接刷新到主内存中,写内存语义是直接从主内存中读取

volatile 凭什么可以保证有序性和可见性?

  • 基于内存屏障(Memory Barrier)

内存屏障(面试重点)

解读 volatile 的两大特性

  • 可见性
    • 一个线程完成对变量的修改之后,立即写回主内存,并及时通知其它线程
    • 其他线程读取时,直接丢弃本地副本,读取主内存变量
  • 有序性(支持禁止重排序)
    • 指令重排序是编译器和处理器为了优化程序性能,在不改变程序执行效果的前提在对指令进行重新排序的一种手段
    • 不存在数据依赖时,可以重排序,存在数据依赖时,禁止重排序
    • 重排序指令不看更改原有串行语义

内存屏障

内存屏障(也称为内存栅栏,屏障指令等)

  • 是一类同步屏障指令,
  • 是 CPU 或编译器在对内存随机访问的操作中的一个同步点,
    • 使得此点之前所有读写操作都执行后才可以开始执行此点之后的操作,避免代码重排序
  • 本质是一种 JVM 指令,
    • Java 内存模型的重排规则会要求 Java 编译器在生成 JVM 指令时,插入特定的内存屏障指令,通过这些内存屏障指令,volatile 实现了 Java 内存模型中的可见性和有序性(禁重排),但是 volatile 无法保证原子性
  • 内存屏障之前的所有写操作都要写回主内存
  • 内存屏障之后的所有读操作都能获得内存屏障之前所有写操作的最新结果(实现了可见性)

  • 写屏障(Store Memory Barrier)
  • 告诉处理器在写屏障之前将所有存储在缓存(store bufferes)中的数据同步到主内存。
    • 当看到 store 屏障指令,就必须把该指令之前所有写入指令执行完毕才能继续往下执行
  • 读屏障(Load Memory Barrier)
  • 处理器在读屏障之后的读操作,均在读屏障之后执行
    • 在 Load 屏障指令之后就能保证后面读取数据指令一定能够读取到最新的数据

  • 因此重排序时,不允许把内存屏障之后的指令重排序到内存屏障之前
    • 对一个 volatile 变量的写,先行发生于任何一个后续的对这个 volatile 变量的读,亦称为写后读

volatile 特性

先行发生原则,happens-before,本质是一个理论,一套规范,内存屏障基于该规范进行实现

两大类

读屏障(Load Barrier)

  • 在读指令之前插入读屏障,让工作内存或 CPU 高速缓存当中的缓存数据失效,重新回到主内存中获取最新数据

写屏障(Store Barrier)

  • 在写指令之后插入写屏障,强制把缓冲区的数据刷回主内存中

四小类

C++源码分析

Unsafe.class,JDK 中的屏障接口


//....code segment...
public native void loadFence();public native void storeFence();public native void fullFence();

Unsafe.java

     /*** Ensures lack of reordering of loads before the fence* with loads or stores after the fence.* @since 1.8*/public native void loadFence();/*** Ensures lack of reordering of stores before the fence* with loads or stores after the fence.* @since 1.8*/public native void storeFence();/*** Ensures lack of reordering of loads or stores before the fence* with loads or stores after the fence.* @since 1.8*/public native void fullFence();

Unsafe.cpp

UNSAFE_ENTRY(void, Unsafe_LoadFence(JNIEnv *env, jobject unsafe))UnsafeWrapper("Unsafe_LoadFence");OrderAccess::acquire();
UNSAFE_ENDUNSAFE_ENTRY(void, Unsafe_StoreFence(JNIEnv *env, jobject unsafe))UnsafeWrapper("Unsafe_StoreFence");OrderAccess::release();
UNSAFE_ENDUNSAFE_ENTRY(void, Unsafe_FullFence(JNIEnv *env, jobject unsafe))UnsafeWrapper("Unsafe_FullFence");OrderAccess::fence();
UNSAFE_END

OrderAccess.hpp

//声明四类屏障类型,读读,写写,读写,写读
class OrderAccess : AllStatic {public:static void     loadload();static void     storestore();static void     loadstore();static void     storeload();//....more code.....
}

orderAccess_linux_ x86.inline.hpp

#include "runtime/atomic.inline.hpp"
#include "runtime/orderAccess.hpp"
#include "runtime/os.hpp"
#include "vm_version_x86.hpp"// Implementation of class OrderAccess.inline void OrderAccess::loadload()   { acquire(); }
inline void OrderAccess::storestore() { release(); }
inline void OrderAccess::loadstore()  { acquire(); }
inline void OrderAccess::storeload()  { fence(); }inline void OrderAccess::acquire() {volatile intptr_t local_dummy;
#ifdef AMD64__asm__ volatile ("movq 0(%%rsp), %0" : "=r" (local_dummy) : : "memory");
#else__asm__ volatile ("movl 0(%%esp),%0" : "=r" (local_dummy) : : "memory");
#endif // AMD64
}inline void OrderAccess::release() {// Avoid hitting the same cache-line from// different threads.volatile jint local_dummy = 0;
}inline void OrderAccess::fence() {if (os::is_MP()) {// always use locked addl since mfence is sometimes expensive
#ifdef AMD64__asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
#else__asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif}
}

四类屏障特点

屏障类型指令示例说明
LoadLoadLoad1;LoadLoad;Load2;保证 load1 的读操作在 load2 及后续读操作之前执行
StoreStoreStore1;StoreStore;Store2;在 store2 及其后的写操作执行前,保证 store1 的写操作已经刷新到主内存中
LoadStoreLoad1;LoadStore;Store2;在 store2 及其后的写操作执行前,保证 load1 的读操作已读取结束
StoreLoadStore1;StoreLoad;Load2保证 store1 的写操作已刷新到主内存之后,load2 以及其后的读操作才能执行

如何保证有序性?

核心思想==>禁止指令重排序
实现方式==>内存屏障

  • 重排序有可能影响程序的执行和实现
    • 对编译器的重排序,JMM 会根据重排序规则,禁止特定类型的编译器重排序
    • 对处理器的重排序,Java 编译器在生成指令序列的适当位置,插入内存品质指令,用于禁止特定类型的处理器重排序

happens-before 之 volatile 变量规则

  • 当第一个操作为 volatile 读时
    • 无论第二个操作是什么,都禁止重排序

保证了 volatile 读之后的操作不会被重排到 volatile 读之前

  • 当第一个操作为 volatile 写时,第二个操作为 volatile 读时,不能重排(写后读)
  • 当第二个操作为 volatile 写时,
    • 无论第一个操作是什么,都禁止重排序

保证了 volatile 写之前的操作不会被重排序到 volatile 写之后

操作 1操作 2-普通读写操作 2-volatile 读操作 2-volatile 写
普通读写可以重排可以重排不可重排
volatile 读不可重排不可重排不可重排
volatile 写可以重排不可重排不可重排

JMM 就内存屏障插入策略分为 4 中规则

  • 在每个 volatile 读操作之后插入一个 LoadLoad/LoadStore 屏障
    • 禁止处理器把上面的 volatile 读与下面的普通读/普通写重排序

  • 在每个 volatile 写操作的前面插入一个 StoreStore/StoreLoad 屏障
    • StoreStore可以保证在 volatile 写之前,前面的所有普通写操作都刷新至主内存中
    • StoreLoad避免 volatile 写与其后的 volatile 读/写重排序


如何保证可见性?

什么是 volatile 的可见性?

  • 保证不同线程对某个共享变量完成操作后,结果及时对其它线程可见
  • caseDemo
    static boolean flag = true;public static void main(String[] args) {new Thread(() -> {System.out.println(Thread.currentThread().getName() + "\t -----come in");while (flag) {}System.out.println(Thread.currentThread().getName() + "\t -----flag被设置为false,程序停止");}, "t1").start();//暂停几秒钟线程try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}flag = false;System.out.println(Thread.currentThread().getName() + "\t 修改完成flag: " + flag);}
  • flag 为普通变量时,程序没有停止,flag 已经修改为 false,但是对其他线程(t1)不可见

image.png

  • flag 为 volatile 变量时,static volatile boolean flag = true;

image.png

  • 小结
  • 不加 volatile,flag 的修改对其他线程不可见,t1 持续死循环
  • 加了 volatile 对其他线程可见,t1 结束死循环
  • 案例解释
  • 线程 t1 中为何看不到被主线程 main 修改为 false 的 flag?
    • 主线程 main 修改了 flag 之后没有刷新到主内存中
    • 主线程将最新值刷新到主内存之后,t1 线程一直读取本地工作内存中的 flag,没有将其置为无效再去访问主内存中的最新值
  • 场景诉求
  • 写操作,对变量进行操作后立即刷新回主内存
  • 读操作,舍弃本地变量,从主内存中进行拷贝至工作内存,再读取
  • volatile 读写特性与诉求一致

volatile 变量的读写过程

  • JMM 中定义了 8 种每个线程自己的工作内存与主物理内存之间的原子操作

  • read,作用于主内存,将变量值从主内存中读取到工作内存
  • load,作用于工作内存,将 read 从主内存读取的变量值存入工作内存中
  • use,作用于工作内存,将变量值交有 CPU 中的 JVM 进行执行
  • assgin,作用于工作内存,JVM 执行赋值操作
  • store,作用于工作内存,CPU 在 JVM 执行完赋值操作后将更新值写回主内存
  • write,作用于主内存,更新主内存中由工作内存 store 过来的变量

上述 6 条只能保证单条指令的原子性,针对多条指令的组合性原子保证,没有大量加锁,因此,JVM 提供了另外两条原子指令


  • lock,作用于主内存,将一个变量标记为线程独占状态,只在写的时候加锁,锁住写变量的过程
  • unlock,作用于主内存,将一个处于锁定状态的变量释放

无法保证原子性

  • 场景描述

资源类中,一个普通的 int 变量,synchronized 修饰的自增方法
10 个线程并发修改

  • 资源类
class MyNumber {int number;public synchronized void addPlusPlus() {number++;}
}
  • 测试代码
    public static void main(String[] args) {MyNumber myNumber = new MyNumber();for (int i = 1; i <= 10; i++) {new Thread(() -> {for (int j = 1; j <= 1000; j++) {myNumber.addPlusPlus();}}, String.valueOf(i)).start();}//暂停几秒钟线程try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(myNumber.number);}
  • 测试结果,正常得到期望值 1000

image.png

  • 修改资源类
    //volatile变量的复合操作不具备原子性volatile int number; public void addPlusPlus() {number++;}

image.png

关于上述现象的解释

读取赋值一个普通变量的情况

  • 当线程 1 对主内存对象发起 read 操作到 write 操作的过程中,线程 2 随时可能对主内存中的对象发起一轮新的操作

不保证原子性

从 i++的字节码角度说明

image.png

  • 原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其它线程所影响
  • 上述代码中 n++;不具备原子性,若第二个线程在第一个线程读取旧值和写回新值期间读取 n 的域值,则两个线程均会对相同的 n 执行+1 操作,其结果就是+2 的操作
  • 对于 add 方法必须使用悲观锁保证线程安全

结论

volatile 变量不适合参与到依赖当前值的运算(i=i+1;/i++;等)

  • 通常使用 volatile 保存某个状态的 boolean 值或者 int 值
  • 《深入理解 Java 虚拟机》中提到

由于 volatile 变量只能保证可见性,在不符合以下两条规定的运算场景中,仍需要使用 synchronized 或是 java.util.concurrent 中的锁或者原子类,来保证原子性

  • 运算结果不依赖变量的当前值,或者能够确保只有单一线程修改变量的值
  • 变量不需要与其他状态变量共同参与不变约束

面试回答

JVM 字节码角度,n++底层指令角度,间隙期不同步非原子操作(n++)

  • 对于 volatile 变量具备可见性,JVM 只是保证从主内存加载到线程工作空间的值是最新的(数据加载是最新的)
  • 若第二个线程在第一个线程读取旧值和写回新值期间读取 i 的域值,则会有线程安全问题


指令禁止重排序

案例说明

详见上文《Java 内存模型 JMM》JMM 有序性

volatile 底层实现通过内存屏障

  • volatile 禁止指令重排序的情况
    • 详见上文,happens-before 之 volatile 变量规则
  • 四大屏障的插入情况
    • 详见上文四类屏障特点

代码案例分析


volatile 的使用场景

单一赋值可用,存在复合运算赋值不可用(i++等)

  • volatile int a = 0;
  • volatile boolean flag = false;

状态标志,判断业务是否结束

  • 详见上述,3.4 如何保证可见性?案例

开销较低的读,写锁策略

    /*** 使用 : 当读多于写,结合使用内部锁和volatile变量来减少同步的开销* 理由 : 利用volatile保证读取操作的可见性,利用synchronized保证复合操作的原子性*/private volatile int value;public int getValue() {return value; //利用volatile保证读取操作的可见性}public synchronized int increment() {return this.value++; //利用synchronized保证复合操作的原子性}

DCL 双端锁的发布

问题描述

public class SafeDoubleCheckSingleton {private static SafeDoubleCheckSingleton safeDoubleCheckSingleton;/*** 私有构造方法*/private SafeDoubleCheckSingleton() {}/*** 双重锁设计*/public static SafeDoubleCheckSingleton getInstance(){if (safeDoubleCheckSingleton==null) {synchronized (SafeDoubleCheckSingleton.class){if (safeDoubleCheckSingleton==null){//存在隐患==>多线程环境下,由于重排序,该对象可能未完成初始化,就被其它线程所读取safeDoubleCheckSingleton = new SafeDoubleCheckSingleton();}}}//对象不为空,已存在或是创建完毕,执行getInstance不需要获取锁,直接返回对象return safeDoubleCheckSingleton;}
}
  • 单线程情况下,隐患处的代码细分为三条指令
memory = allcate();		//1.分配对象内存空间
ctorInstance(memory);	//2.初始化对象
instance = memory;		//3.设置instance指向所分配的内存地址
  • 单线程情况上述代码可以指向;由于存在指令重排序,多线程情况下存在问题
    • 指令重排序将 2,3 倒序,造成的后果就是其他线程得到的是 null 并非完成初始化的对象
  • 重排序后的指令顺序
memory = allcate();		//1.分配对象内存空间
instance = memory;		//3.设置instance指向所分配的内存地址//此时对象还未进行初始化
ctorInstance(memory);	//2.初始化对象

解决方案

添加 volatile 修饰

 //通过volatile 声明实现线程安全的延迟初始化private volatile static SafeDoubleCheckSingleton safeDoubleCheckSingleton;/*** 私有构造方法*/private SafeDoubleCheckSingleton() {}/*** 双重锁设计*/public static SafeDoubleCheckSingleton getInstance(){//首次检查if (safeDoubleCheckSingleton==null) {synchronized (SafeDoubleCheckSingleton.class){//第二次检查if (safeDoubleCheckSingleton==null){//存在隐患==>多线程环境下,由于重排序,该对象可能未完成初始化,就被其它线程所读取safeDoubleCheckSingleton = new SafeDoubleCheckSingleton();//解决隐患的原理: 利用volatile,禁止"初始化对象"(2)和"设置singleton指向内存空间"(3)的重排序}}}//对象不为空,已存在或是创建完毕,执行getInstance不需要获取锁,直接返回对象return safeDoubleCheckSingleton;}

Summary

  • volatile 可见性,基于读写特性
    • 写操作,变量值会立即刷新回主内存(不保证原子性,存在写丢失)
    • 读操作,总是能够读取到这个变量的最新值
    • 当某个线程收到通知,去读取 volatile 修饰的变量时,线程工作内存中的数据就会失效,重新回主内存中读取最新的数据
  • volatile 没有原子性,在多线程进行写操作必须加锁
  • volatile 禁止指令重排序,基于读操作与写操作的内存屏障
    • volatile 写操作,前置 StoreStore 屏障,后置 StoreLoad

  • volatile 读操作,后置 LoadLoad,LoadStore

  • volatile 与内存屏障的关联,基于 JVM 字节码字段特性

image.png

  • 内存屏障是什么

内存屏障 : 是一种屏障指令,使得 CPU 或编译器对屏障指令的前和后所发出的内存操作执行一个排序的约束,也称为内存栅栏或栅栏指令

  • 内存屏障能干吗
    • 阻止屏障两边的指令重排序
    • 写数据时加入屏障,强制将线程私有工作内存的数据刷回主物理内存
    • 读数据时加入屏障,线程私有工作内存的数据失败,重新到主物理内存中获取最新的数据
  • 内存屏障四大指令
    • 写屏障的 StoreStore/StoreLoad
    • 读屏障的 LoadLoad/LoadStore
  • 小总结
    • volatile 写之前的操作禁止重排序到 volatile 之后
    • volatile 读之后的操作禁止重排序到 volatile 之前
    • volatile 写之后 volatile 读禁止重排序

    在这里插入图片描述

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

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

相关文章

huggingface的transformers训练gpt

目录 1.原理 2.安装 3.运行 ​编辑 4.数据集 ​编辑 4.代码 4.1 model init​编辑 forward&#xff1a; 总结&#xff1a; 关于loss和因果语言模型&#xff1a; ​编辑 交叉熵&#xff1a;​编辑 记录一下transformers库训练gpt的过程。 transformers/examples/…

klipper源码分析之simulavr测试

分析Klipper源码&#xff0c;有时需要结合下位机一起分析&#xff0c;这样才能更加全面的了解Klipper的工作原理。如果手头上有打印机主板&#xff0c;电脑当做上位机运行Klipper&#xff0c;这样是比较方便。如果手头上没有打印机主板&#xff0c;可以用simulavr模拟AVR下位机…

Linux常见指令解析一

Linux常见指令解析一 常见指令1. ls 指令2.pwd 命令3.cd 命令4.touch 命令5.mkdir 命令6.rmdir指令 && rm 指令7.man 指令8.cp 指令9.cat 命令 && tac 命令10.mv 指令11.more 指令12.less 指令13.head 指令14.tail 指令15.cal 指令 常见指令 1. ls 指令 语法…

Vscode创建php项目

1.安装中文插件&#xff08;可安装可不安装&#xff09; 2.安装主题&#xff08;可安装可不安装&#xff09; 3.安装和php相关的插件 4.打开文件夹 5.路由操作 查看项目中的route路由 浏览器中访问think 隐藏index.php入口文件 访问ThinkPHP5.1开发手册&#xff0c;复制apa…

【Python实战】——神经网络识别手写数字

&#x1f349;CSDN小墨&晓末:https://blog.csdn.net/jd1813346972 个人介绍: 研一&#xff5c;统计学&#xff5c;干货分享          擅长Python、Matlab、R等主流编程软件          累计十余项国家级比赛奖项&#xff0c;参与研究经费10w、40w级横向 文…

鸿蒙APP应用开发教程—超详细的项目结构说明

1. 新建项目 打开DevEco Studio, 选择 Create Project: 1.1 选择模版 Create Project - Choose Template 1.2 配置项目 Create Project - Configure Project 如果使用的是 DevEco 3.X 版本, 可以根据 Compile SDK版本选择不同的模式, 比如: 3.0.0(API 8)及更早 - 仅支持 …

数字图像处理学习笔记(五)

数字图像处理学习笔记&#xff08;五&#xff09; 表示与描述表示链码最小周长多边形的多边形近似&#xff08;MPP&#xff09;标记边界片段骨骼&#xff08;表示平面区域结构形状&#xff09; SIFT原理(尺度不变特征变换匹配算法:Scale-invariant feature transform)SIFT算法分…

有名的爬虫框架 colly 的特性及2个详细采集案例

一. Colly概述 前言&#xff1a;colly 是 Go 实现的比较有名的一款爬虫框架&#xff0c;而且 Go 在高并发和分布式场景的优势也正是爬虫技术所需要的。它的主要特点是轻量、快速&#xff0c;设计非常优雅&#xff0c;并且分布式的支持也非常简单&#xff0c;易于扩展。 框架简…

Linux之时间子系统(四): tick 层模块(periodic 和dynamic )

一、时间子系统的软件架构 二、tick 层模块的文件 tick-common.c tick-oneshot.c tick-sched.c tick-broadcast.c tick-broadcast-hrtimer.c 这三个文件属于tick device layer。 tick-common.c文件是periodic tick模块&#xff0c;用于管理周期性tick事件。 tick-oneshot.c文…

ubuntu22.04物理机双系统手动分区

ubuntu22.04物理机双系统手动分区 文章目录 ubuntu22.04物理机双系统手动分区1. EFI系统分区2. 交换分区3. /根分区4. /home分区分区后的信息 手动分区顺序&#xff1a;EFI系统分区(/boot/efi)、交换分区(/swap)、/根分区、/home分区。 具体参数设置&#xff1a; 1. EFI系统分…

OpenHarmony使用智能指针管理动态分配内存对象

概述 智能指针是行为类似指针的类&#xff0c;在模拟指针功能的同时提供增强特性&#xff0c;如针对具有动态分配内存对象的自动内存管理等。 自动内存管理主要是指对超出生命周期的对象正确并自动地释放其内存空间&#xff0c;以避免出现内存泄漏等相关内存问题。智能指针对…

Vue复习

1. MVVM 模型 ● Model&#xff08;模型&#xff09;&#xff1a;表示应用程序中的数据模型。它代表着应用程序中的业务逻辑和状态。 ● View&#xff08;视图&#xff09;&#xff1a;表示应用程序的用户界面。它是用户与应用程序交互的方式。 ● ViewModel&#xff08;视图模…

Docker 安装 Nginx 容器,反向代理

Docker官方镜像https://hub.docker.com/ 寻找Nginx镜像 下载Nginx镜像 docker pull nginx #下载最新版Nginx镜像 (其实此命令就等同于 : docker pull nginx:latest ) docker pull nginx:xxx #下载指定版本的Nginx镜像 (xxx指具体版本号)检查当前所有Docker下载的镜像 docker…

关于使用TCP-S7协议读写西门子PLC字符串的问题

我们可以使用TCP-S7协议读写西门子PLC&#xff0c; 比如PLC中定义一个String[50] 的地址DB300.20 地址DB300.20 DB块编号为300&#xff0c;偏移量【地址】是30 S7协议是西门子PLC自定义的协议&#xff0c;默认端口102&#xff0c;本质仍然是TCP协议的一种具体实现&#xff…

HMI界面之:医疗设备界面

一、什么是医疗HMI界面 医疗HMI界面是指医疗设备或系统中的人机界面&#xff08;Human-Machine Interface&#xff09;&#xff0c;用于与医疗设备进行交互和操作的界面。它是医疗设备中的重要组成部分&#xff0c;通过图形化、直观化的界面&#xff0c;使医护人员能够方便地控…

短剧APP系统开发:探索短剧的发展机遇,提高收益

近年来&#xff0c;短剧在各大社交平台上快速发展&#xff0c;市场规模大幅度上升&#xff0c;成为了大众闲暇时光的娱乐的首选方式之一&#xff0c;深受大众的喜爱。 与传统的影视相比&#xff0c;短剧时间短、节奏快、剧情爽&#xff0c;让给观众更加容易“上头”。对于创业…

举4例说明Python如何使用正则表达式分割字符串

在Python中&#xff0c;你可以使用re模块的split()函数来根据正则表达式分割字符串。这个函数的工作原理类似于Python内置的str.split()方法&#xff0c;但它允许你使用正则表达式作为分隔符。 示例 1: 使用单个字符作为分隔符 假设你有一个由逗号分隔的字符串&#xff0c;你可…

力扣:205. 同构字符串

前言&#xff1a;剑指offer刷题系列 问题&#xff1a; 给定两个字符串 s 和 t &#xff0c;判断它们是否是同构的。 如果 s 中的字符可以按某种映射关系替换得到 t &#xff0c;那么这两个字符串是同构的。 每个出现的字符都应当映射到另一个字符&#xff0c;同时不改变字符…

【Lazy ORM 框架学习】

Gitee 点赞关注不迷路 项目地址 快速入门 模块所属层级描述快照版本正式版本wu-database-lazy-lambdalambda针对不同数据源wu-database-lazy-orm-coreorm 核心orm核心处理wu-database-lazy-sqlsql核心处理成处理sql解析、sql执行、sql映射wu-elasticsearch-starterESESwu-hb…

时间的守护者:无硫手指套的神奇传说

在钟表制造的世界里&#xff0c;有一个神奇的工具被誉为“精工制表良器”——那就是无硫手指套。这并不是一个普通的故事&#xff0c;而是一段讲述质量、技术和关怀的传奇。 很久以前&#xff0c;在一个钟表制造工坊里&#xff0c;技师们为了追求完美&#xff0c;不断地探索着提…