当前位置: 首页 > news >正文

共享模型之无锁(三)

1.原子累加器

示例代码:

public class TestAtomicAdder {public static void main(String[] args) {for (int i = 0; i < 5; i++) {demo(() -> new AtomicLong(0),(adder) -> adder.getAndIncrement());}for (int i = 0; i < 5; i++) {demo(() -> new LongAdder(),(adder) -> adder.increment());}}/*** @param adderSupplier 提供者 无中生有 ()->结果* @param action        消费者 一个参数没结果* @param <T>*/private static <T> void demo(Supplier<T> adderSupplier, Consumer<T> action) {T adder = adderSupplier.get();long start = System.nanoTime();List<Thread> ts = new ArrayList<>();//4个线程,每个累加5万次,最终结果是200000for (int i = 0; i < 4; i++) {ts.add(new Thread(() -> {for (int j = 0; j < 50000; j++) {action.accept(adder);}}));}ts.forEach(t -> t.start());ts.forEach(t -> {try {t.join();} catch (InterruptedException e) {e.printStackTrace();}});long end = System.nanoTime();System.out.println(adder + " cost(ns):" + (end - start) / 1000_000);}
}

比较 AtomicLong 与 LongAdder:
在这里插入图片描述

LongAdder累加器性能提升的原因很简单,就是在有竞争时,设置多个累加单元,Therad-0累加Cell[0],而Thread-1累加Cell[1]… 最后将结果汇总.这样它们在累加时操作不同的Cell变量,因此减少了CAS重试失败,从而提高性能;

1.1.LongAdder源码分析

1>.LongAdder是并发大师Doug Lea(大哥李)的作品,设计的非常精巧;

2>.LongAdder类有几个关键域:

// 累加单元数组,懒惰初始化
// transient关键字作用在是序列化的时候保证这些域不会被序列化
transient volatile Cell[] cells;// 基础值,如果没有竞争,则用cas累加这个域
transient volatile long base;// 在cells创建或扩容时,置为1,表示加锁
transient volatile int cellsBusy;

3>.CAS锁

// 不要用于实践!!!
public class LockCas {private AtomicInteger state = new AtomicInteger(0);public void lock() {while (true) {if (state.compareAndSet(0, 1)) {break;}}}public void unlock() {log.debug("unlock...");state.set(0);}
}//测试代码
LockCas lock = new LockCas();
new Thread(() -> {log.debug("begin...");lock.lock();try {log.debug("lock...");sleep(1);} finally {lock.unlock();}
}).start();new Thread(() -> {log.debug("begin...");lock.lock();try {log.debug("lock...");} finally {lock.unlock();}
}).start();

4>.缓存行(hang)伪共享(一个缓存行加入了多个内存cell对象被称为伪共享)
其中Cell即为累加单元

// 防止缓存行伪共享,防止一个缓存行容纳多个内存cell对象
@sun.misc.Contended
static final class Cell {volatile long value;Cell(long x) { value = x; }// 最重要的方法, 用cas方式进行累加, prev表示旧值, next表示新值final boolean cas(long prev, long next) {return UNSAFE.compareAndSwapLong(this, valueOffset, prev, next);}// 省略不重要代码
}

1.1.1.LongAdder#add()源码

public void add(long x) {// as为累加单元数组// b为基础值// x为累加值Cell[] as; long b, v; int m; Cell a;// 进入 if 的两个条件// 1.as有值,表示已经发生过竞争, 进入if// 2.cas给base累加时失败了,表示base发生了竞争,进入ifif ((as = cells) != null || !casBase(b = base, b + x)) {// uncontended表示cell没有竞争boolean uncontended = true;// as还没有创建// 当前线程对应的cell还没有// cas给当前线程的cell累加失败uncontended=false(a为当前线程的cell)if (as == null || (m = as.length - 1) < 0 ||(a = as[getProbe() & m]) == null ||!(uncontended = a.cas(v = a.value, v + x)))// 进入cell数组创建、cell创建的流程longAccumulate(x, null, uncontended);}
}

在这里插入图片描述

1.1.2.Striped64#longAccumulate()源码

final void longAccumulate(long x, LongBinaryOperator fn,boolean wasUncontended) {int h;// 当前线程还没有对应的cell,需要随机生成一个h值用来将当前线程绑定到cellif ((h = getProbe()) == 0) {// 初始化probeThreadLocalRandom.current(); // force initialization// h对应新的probe值,用来对应(/占用)cell
h = getProbe();wasUncontended = true;}// collide为true表示需要扩容boolean collide = false;                // True if last slot nonemptyfor (;;) {Cell[] as; Cell a; int n; long v;// 已经有了cellsif ((as = cells) != null && (n = as.length) > 0) {// 还没有cellif ((a = as[(n - 1) & h]) == null) {// 为cellsBusy加锁,创建cell,cell的初始累加值为x// 成功则 break,否则继续continue循环if (cellsBusy == 0) {       // Try to attach new CellCell r = new Cell(x);   // Optimistically createif (cellsBusy == 0 && casCellsBusy()) {boolean created = false;try {               // Recheck under lockCell[] rs; int m, j;if ((rs = cells) != null &&(m = rs.length) > 0 &&rs[j = (m - 1) & h] == null) {rs[j] = r;created = true;}} finally {cellsBusy = 0;}if (created)break;continue;           // Slot is now non-empty}}collide = false;}// 有竞争,改变线程对应的cell来重试caselse if (!wasUncontended)       // CAS already known to failwasUncontended = true;      // Continue after rehash// cas尝试累加,fn配合LongAccumulator不为null,配合LongAdder为null
else if (a.cas(v = a.value, ((fn == null) ? v + x :fn.applyAsLong(v, x))))break;// 如果cells长度已经超过了最大长度,或者已经扩容,改变线程对应的cell来重试caselse if (n >= NCPU || cells != as)collide = false;            // At max size or stale// 确保collide为false进入此分支,就不会进入下面的else if进行扩容了
else if (!collide)collide = true;// 加锁else if (cellsBusy == 0 && casCellsBusy()) {try {if (cells == as) {      // Expand table unless staleCell[] rs = new Cell[n << 1];for (int i = 0; i < n; ++i)rs[i] = as[i];cells = rs;}} finally {cellsBusy = 0;}collide = false;// 加锁成功,扩容continue;                   // Retry with expanded table}// 改变线程对应的cell对象h = advanceProbe(h);}// 还没有cells,尝试给cellsBusy加锁else if (cellsBusy == 0 && cells == as && casCellsBusy()) {// 加锁成功,初始化cells,最开始长度为2,并填充一个cellboolean init = false;try {                           // Initialize tableif (cells == as) {Cell[] rs = new Cell[2];rs[h & 1] = new Cell(x);cells = rs;init = true;}} finally {cellsBusy = 0;}if (init)// 成功则break;break;}// 上两种情况失败,尝试给base累加else if (casBase(v = base, ((fn == null) ? v + x :fn.applyAsLong(v, x))))break;                          // Fall back on using base}
}

在这里插入图片描述在这里插入图片描述
每个线程刚进入longAccumulate()时,会尝试对应(/占用)一个cell对象(找到一个位置)
在这里插入图片描述

1.1.3.LongAdder#sum()源码

public long sum() {Cell[] as = cells; Cell a;long sum = base;if (as != null) {for (int i = 0; i < as.length; ++i) {if ((a = as[i]) != null)//将Cells数组中的每个元素进行累加sum += a.value;}}return sum;
}

扩展: 缓存与内存的速度比较

1>.CPU内存结构:
在这里插入图片描述
2>.CPU访问不同的缓存和内存耗费的时钟周期
在这里插入图片描述

①.CPU与内存的速度差异很大,需要靠预读数据至缓存来提升效率;
②.缓存以缓存行为单位,每个缓存行对应着一块内存,一般是64byte(8个long);
③.缓存的加入会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中;
④.CPU要保证数据的一致性,如果某个CPU核心更改了数据,其它CPU核心对应的整个缓存行(64byte)必须全部失效;
在这里插入图片描述
⑤.由于内存Cell是数组形式,在内存中是连续存储的,一个内存Cell为24字节(16字节的对象头和8字节的value),因此缓存行可以存下2个内存Cell对象.这样问题来了:

  • Core-0要修改Cell[0]
  • Core-1要修改Cell[1]

无论谁修改成功,都会导致对方Core的缓存行失效,比如Core-0中Cell[0]=6000,Cell[1]=8000要累加Cell[0]=6001,Cell[1]=8000,这时会让Core-1的缓存行失效;
⑥."@sun.misc.Contended"就是用来解决这个问题的,它的原理是在使用此注解的对象或字段的前后各增加128字节大小的padding(填充内存Cell),从而让CPU将(不同的)对象预读至缓存时占用不同的缓存行,这样不会造成对方缓存行的失效;
在这里插入图片描述

2.Unsafe对象

2.1.概述

1> Unsafe对象提供了非常底层的,操作内存、线程的方法,Unsafe对象不能直接调用,只能通过反射获得;

2>.示例代码:

public class TestUnsafeDemo1 {public static void main(String[] args) throws Exception {//通过反射获取到unsafe对象,这里的参数"theUnsafe"是固定的写法,对应着unsafe对象中的(类类型)成员变量Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");//由于unsafe对象中"theUnsafe"成员变量是"private"私有的,因此要设置访问权限theUnsafe.setAccessible(true);//通过成员变量获取到所属的unsafe对象//由于unsafe对象中"theUnsafe"成员变量是静态的,因此这里原本的对象参数就是nullUnsafe unsafe = (Unsafe) theUnsafe.get(null);System.out.println(unsafe);  //sun.misc.Unsafe@7ea987ac}
}

3>.封装成工具类:

public class UnsafeAccessor {static Unsafe unsafe;static {try { Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");theUnsafe.setAccessible(true);unsafe = (Unsafe) theUnsafe.get(null);} catch (NoSuchFieldException | IllegalAccessException e) {throw new Error(e);}}static Unsafe getUnsafe() {return unsafe;}
}

2.2.Unsafe CAS操作

1>.使用Unsafe对象通过CAS机制线程安全的修改对象的成员变量:

@Slf4j
public class TestUnsafeCasDemo1 {public static void main(String[] args) throws Exception {//获取unsafe对象Unsafe unsafe = UnsafeAccessor.getUnsafe();//获取对象中某个成员变量的偏移地址long idOffset = unsafe.objectFieldOffset(Teacher.class.getDeclaredField("id"));long nameOffset = unsafe.objectFieldOffset(Teacher.class.getDeclaredField("name"));//执行CAS操作Teacher teacher = new Teacher();boolean b = unsafe.compareAndSwapInt(teacher, idOffset, 0, 1);boolean b1 = unsafe.compareAndSwapObject(teacher, nameOffset, null, "张三");//打印结果log.info(teacher.toString());}
}@Data
@NoArgsConstructor
class Teacher {volatile int id;volatile String name;
}//unsafe对象封装
class UnsafeAccessor {static Unsafe unsafe;static {try {Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");theUnsafe.setAccessible(true);unsafe = (Unsafe) theUnsafe.get(null);} catch (NoSuchFieldException | IllegalAccessException e) {throw new Error(e);}}static Unsafe getUnsafe() {return unsafe;}
}

在这里插入图片描述

2.3.模拟实现原子整数

1>.以转账为例:

public class TestCustomAtomicIntegerDemo1 {public static void main(String[] args) {Account1.demo(new MyAtomicInteger(200));}
}//自定义的原子整数类
class MyAtomicInteger implements Account1 {private volatile int value;private static final long valueOffset;private static final Unsafe UNSAFE;static {UNSAFE = UnsafeAccessorUtil.getUnsafe();try {valueOffset = UNSAFE.objectFieldOffset(MyAtomicInteger.class.getDeclaredField("value"));} catch (NoSuchFieldException e) {e.printStackTrace();throw new RuntimeException();}}public MyAtomicInteger(int value) {this.value = value;}public int getValue() {return this.value;}public void decrement(int amount) {while (true) {int prev = this.value;int next = prev - amount;if (UNSAFE.compareAndSwapInt(this, valueOffset, prev, next)) {break;}}}@Overridepublic Integer getBalance() {return this.getValue();}@Overridepublic void withDraw(int amount) {this.decrement(amount);}
}//unsafe工具类
class UnsafeAccessorUtil {static Unsafe unsafe;static {try {Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");theUnsafe.setAccessible(true);unsafe = (Unsafe) theUnsafe.get(null);} catch (NoSuchFieldException | IllegalAccessException e) {throw new Error(e);}}static Unsafe getUnsafe() {return unsafe;}
}//封装转账相关的方法
interface Account1 {//获取余额Integer getBalance();//取款void withDraw(int amount);/*** 方法内会启动20个线程,每个线程做(-10元)的操作* 如果初始余额为200那么正确的结果应当是 0*/static void demo(Account1 account) {List<Thread> ts = new ArrayList<>();long start = System.nanoTime();for (int i = 0; i < 20; i++) {ts.add(new Thread(() -> {account.withDraw(10);}));}ts.forEach(Thread::start);ts.forEach(t -> {try {t.join();} catch (InterruptedException e) {e.printStackTrace();}});long end = System.nanoTime();System.out.println(account.getBalance()+ " cost: " + (end - start) / 1000_000 + " ms");}
}

在这里插入图片描述

相关文章:

共享模型之无锁(三)

1.原子累加器 示例代码: public class TestAtomicAdder {public static void main(String[] args) {for (int i 0; i < 5; i) {demo(() -> new AtomicLong(0),(adder) -> adder.getAndIncrement());}for (int i 0; i < 5; i) {demo(() -> new LongAdder(),(…...

微信小程序 Springboot校运会高校运动会管理系统

3.1小程序端 小程序登录页面&#xff0c;用户也可以在此页面进行注册并且登录等。 登录成功后可以在我的个人中心查看自己的个人信息或者修改信息等 在广播信息中我们可以查看校运会发布的一些信息情况。 在首页我们可以看到校运会具体有什么项目运动。 在查看具体有什么活动我…...

走进独自开,带你轻松干副业

今天给大家分享一个开发者的福利平台——独自开&#xff08;点击直接注册&#xff09;&#xff0c;让你在家就能解决收入问题。 文章目录一、平台介绍二、系统案例三、获取收益四、使用平台1、用户注册2、用户认证3、任务报价五、文末总结一、平台介绍 简单说明 独自开信息科技…...

SpringBoot+Vue实现师生健康信息管理系统

文末获取源码 开发语言&#xff1a;Java 框架&#xff1a;springboot JDK版本&#xff1a;JDK1.8 服务器&#xff1a;tomcat7 数据库&#xff1a;mysql 5.7/8.0 数据库工具&#xff1a;Navicat11 开发软件&#xff1a;eclipse/myeclipse/idea Maven包&#xff1a;Maven3.3.9 浏…...

数据库第四章节第三次作业内容

1、显示所有职工的基本信息。 2、查询所有职工所属部门的部门号&#xff0c;不显示重复的部门号。 3、求出所有职工的人数。 4、列出最高工和最低工资。 5、列出职工的平均工资和总工资。 6、创建一个只有职工号、姓名和参加工作的新表&#xff0c;名为工作日期表…...

一篇五分生信临床模型预测文章代码复现——FIgure 9.列线图构建,ROC分析,DCA分析 (四)

之前讲过临床模型预测的专栏,但那只是基础版本,下面我们以自噬相关基因为例子,模仿一篇五分文章,将图和代码复现出来,学会本专栏课程,可以具备发一篇五分左右文章的水平: 本专栏目录如下: Figure 1:差异表达基因及预后基因筛选(图片仅供参考) Figure 2. 生存分析,…...

神经网络实战--使用迁移学习完成猫狗分类

前言&#xff1a; Hello大家好&#xff0c;我是Dream。 今天来学习一下如何使用基于tensorflow和keras的迁移学习完成猫狗分类&#xff0c;欢迎大家一起前来探讨学习~ 本文目录&#xff1a;一、加载数据集1.调用库函数2.加载数据集3.数据集管理二、猫狗数据集介绍1.猫狗数据集介…...

Attention机制 学习笔记

学习自https://easyai.tech/ai-definition/attention/ Attention本质 Attention&#xff08;注意力&#xff09;机制如果浅层的理解&#xff0c;跟他的名字非常匹配。他的核心逻辑就是“从关注全部到关注重点”。 比如我们人在看图片时&#xff0c;对图片的不同地方的注意力…...

数据类型与运算符

1.字符型作用: 字符型变量用于显示单个字符语法: char cc a ;注意1: 在显示字符型变量时&#xff0c;用单引号将字符括起来,不要用双引号注意2: 单引号内只能有一个字符&#xff0c;不可以是字符串C和C中字符型变量只占用1个字节。字符型变是并不是把字符本身放到内存中存储&am…...

算法刷题-二叉树的锯齿形层序遍历、用栈实现队列 栈设计、买卖股票的最佳时机 IV

文章目录二叉树的锯齿形层序遍历&#xff08;树、广度优先搜索&#xff09;用栈实现队列&#xff08;栈、设计&#xff09;买卖股票的最佳时机 IV&#xff08;数组、动态规划&#xff09;二叉树的锯齿形层序遍历&#xff08;树、广度优先搜索&#xff09; 给定一个二叉树&…...

华为OD机试 - 最小传递延迟(Python)| 代码编写思路+核心知识点

最小传递延迟 题目 通讯网络中有 N 个网络节点 用 1 ~ N 进行标识 网络通过一个有向无环图进行表示 其中图的边的值,表示节点之间的消息传递延迟 现给定相连节点之间的延时列表 times[i]={u,v,w} 其中 u 表示源节点,v 表示目的节点,w 表示 u 和 v 之间的消息传递延时 请计…...

集中供热调度系统天然气仪表内网仪表图像识别案例

一、项目需求 出于能耗采集与冬季集中供暖工作的节能和能耗分析需要&#xff0c;要采集现场的6块天然气表计&#xff0c;并存储进入客户的mySQL数据库中&#xff0c;现场采集的表计不允许接线&#xff0c;且网络环境为内网环境&#xff0c;需要采集表计数据并存入数据库&#…...

笔试题-2023-复旦微-数字IC设计【纯净题目版】

回到首页:2023 数字IC设计秋招复盘——数十家公司笔试题、面试实录 推荐内容:数字IC设计学习比较实用的资料推荐 题目背景 笔试时间:2022.07.26应聘岗位:数字前端工程师笔试时长:120min笔试平台:赛码题目类型:基础题(10道)、选做题(10道)、验证题(5道)主观评价 难…...

【Linux】冯诺依曼体系结构和操作系统概念

文章目录&#x1f3aa; 冯诺依曼体系结构&#x1f680;1.体系概述&#x1f680;2.CPU和内存的数据交换&#x1f680;3.体系结构中数据的流动&#x1f3aa; 操作系统概念理解&#x1f680;1.简述&#x1f680;2.设计目的&#x1f680;3.定位&#x1f680;4.理解&#x1f680;5.管…...

HTML5之HTML基础学习笔记

列表标签 列表的应用场景 场景&#xff1a;在网页中按照行展示关联性的内容&#xff0c;如&#xff1a;新闻列表、排行榜、账单等特点&#xff1a;按照行的方式&#xff0c;整齐显示内容种类&#xff1a;无序列表、有序列表、自定义列表 这是老师PPT上的内容&#xff0c; 列表…...

FreeRTOS信号量 | FreeRTOS十

目录 说明&#xff1a; 一、信号量 1.1、信号量简介 1.2、信号量特点 二、二值信号量 2.1、二值信号量简介 2.2、获取与释放二值信号量函数 2.3、二值信号量使用过程与相关API函数 2.4、创建二值信号量函数了解 2.5、释放二值信号量了解 2.6、获取二值信号量了解 三…...

【SpringBoot】SpringBoot常用注解

一、前言首先这里说的SpringBoot常用注解是指在我们开发项目过程中&#xff0c;我们经常使用的注解&#xff0c;包含Spring、SpringBoot、SpringCloud、SpringMVC等这些框架中的注解&#xff0c;而不仅仅是SpringBoot中的注解。这里只是作一个注解列举&#xff0c;每个注解具体…...

数据一致性

目录一、AOP 动态代理切入方法(1) Aspect Oriented Programming(2) 切入点表达式二、SpringBoot 项目扫描类(1) ResourceLoader 扫描类(2) Map 的 computeIfAbsent 方法(3) 反射几个常用 api① 创建一个测试注解② 创建测试 PO 类③ 反射 api 获取指定类的指定注解信息(4) 返回…...

Docker不做虚拟化内核,对.NET有什么影响?

引子前两天刷抖音&#xff0c;看见了这样一个问题。问题&#xff1a;容器化不做虚拟内核&#xff0c;会有什么弊端&#xff1f;Java很多方法会跟CPU的核数有关&#xff0c;这个时候调用系统函数&#xff0c;读到的是宿主机信息&#xff0c;而不是我们限制资源的大小。思考&…...

HTML总结

CSS代码风格 空格规范&#xff1a; 1. 属性值前面&#xff0c;冒号后面&#xff0c;保留一个空格&#xff1b; 2. 选择器&#xff08;标签&#xff09;和大括号中间保留空格。 基本语法概述&#xff1a; 1.HTML标签是由尖括号包围的关键词&#xff0c;如<html> 2.HTM…...

Linux 文件类型,目录与路径,文件与目录管理

文件类型 后面的字符表示文件类型标志 普通文件&#xff1a;-&#xff08;纯文本文件&#xff0c;二进制文件&#xff0c;数据格式文件&#xff09; 如文本文件、图片、程序文件等。 目录文件&#xff1a;d&#xff08;directory&#xff09; 用来存放其他文件或子目录。 设备…...

CTF show Web 红包题第六弹

提示 1.不是SQL注入 2.需要找关键源码 思路 进入页面发现是一个登录框&#xff0c;很难让人不联想到SQL注入&#xff0c;但提示都说了不是SQL注入&#xff0c;所以就不往这方面想了 ​ 先查看一下网页源码&#xff0c;发现一段JavaScript代码&#xff0c;有一个关键类ctfs…...

DockerHub与私有镜像仓库在容器化中的应用与管理

哈喽&#xff0c;大家好&#xff0c;我是左手python&#xff01; Docker Hub的应用与管理 Docker Hub的基本概念与使用方法 Docker Hub是Docker官方提供的一个公共镜像仓库&#xff0c;用户可以在其中找到各种操作系统、软件和应用的镜像。开发者可以通过Docker Hub轻松获取所…...

Python:操作 Excel 折叠

💖亲爱的技术爱好者们,热烈欢迎来到 Kant2048 的博客!我是 Thomas Kant,很开心能在CSDN上与你们相遇~💖 本博客的精华专栏: 【自动化测试】 【测试经验】 【人工智能】 【Python】 Python 操作 Excel 系列 读取单元格数据按行写入设置行高和列宽自动调整行高和列宽水平…...

遍历 Map 类型集合的方法汇总

1 方法一 先用方法 keySet() 获取集合中的所有键。再通过 gey(key) 方法用对应键获取值 import java.util.HashMap; import java.util.Set;public class Test {public static void main(String[] args) {HashMap hashMap new HashMap();hashMap.put("语文",99);has…...

基于Uniapp开发HarmonyOS 5.0旅游应用技术实践

一、技术选型背景 1.跨平台优势 Uniapp采用Vue.js框架&#xff0c;支持"一次开发&#xff0c;多端部署"&#xff0c;可同步生成HarmonyOS、iOS、Android等多平台应用。 2.鸿蒙特性融合 HarmonyOS 5.0的分布式能力与原子化服务&#xff0c;为旅游应用带来&#xf…...

【Java_EE】Spring MVC

目录 Spring Web MVC ​编辑注解 RestController RequestMapping RequestParam RequestParam RequestBody PathVariable RequestPart 参数传递 注意事项 ​编辑参数重命名 RequestParam ​编辑​编辑传递集合 RequestParam 传递JSON数据 ​编辑RequestBody ​…...

【JVM面试篇】高频八股汇总——类加载和类加载器

目录 1. 讲一下类加载过程&#xff1f; 2. Java创建对象的过程&#xff1f; 3. 对象的生命周期&#xff1f; 4. 类加载器有哪些&#xff1f; 5. 双亲委派模型的作用&#xff08;好处&#xff09;&#xff1f; 6. 讲一下类的加载和双亲委派原则&#xff1f; 7. 双亲委派模…...

Ubuntu Cursor升级成v1.0

0. 当前版本低 使用当前 Cursor v0.50时 GitHub Copilot Chat 打不开&#xff0c;快捷键也不好用&#xff0c;当看到 Cursor 升级后&#xff0c;还是蛮高兴的 1. 下载 Cursor 下载地址&#xff1a;https://www.cursor.com/cn/downloads 点击下载 Linux (x64) &#xff0c;…...

「全栈技术解析」推客小程序系统开发:从架构设计到裂变增长的完整解决方案

在移动互联网营销竞争白热化的当下&#xff0c;推客小程序系统凭借其裂变传播、精准营销等特性&#xff0c;成为企业抢占市场的利器。本文将深度解析推客小程序系统开发的核心技术与实现路径&#xff0c;助力开发者打造具有市场竞争力的营销工具。​ 一、系统核心功能架构&…...