【Java并发编程】信号量Semaphore详解
一、简介
Semaphore(信号量):是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。
Semaphore 一般用于流量的控制,特别是公共资源有限的应用场景。例如数据库的连接,假设数据库的连接数上线为10个,多个线程并发操作数据库可以使用Semaphore来控制并发操作数据库的线程个数最多为10个。
Semaphore 是一个有效的流量控制工具,它基于 AQS 共享锁实现。我们常常用它来控制对有限资源的访问。
- 每次使用资源前,先申请一个信号量,如果资源数不够,就会阻塞等待;
- 每次释放资源后,就释放一个信号量。
二、源码
2.1 类总览
通过上面的类图可以看到,Semaphore 与 ReentrantLock 的内部类的结构相同,类内部总共存在 Sync、NonfairSync、FairSync 三个类, NonfairSync 与 FairSync 类继承自 Sync 类,其只有一个 tryAcquireShared() 方法,重写了AQS的该方法。Sync 类继承自 AbstractQueuedSynchronizer 抽象类。
与 CountDownLatch 类似,Semaphore 主要是通过 AQS 的共享锁机制实现的,因此它的核心属性只有一个 Sync。总体源码如下:
public class Semaphore implements java.io.Serializable {//序列化版本号private static final long serialVersionUID = -3222578661600680210L;//同步队列private final Sync sync;//构造方法//指定许可数,默认为非公平策略public Semaphore(int permits) {sync = new NonfairSync(permits);}//指定许可数和是否公平策略public Semaphore(int permits, boolean fair) {sync = fair ? new FairSync(permits) : new NonfairSync(permits);}//Semaphore提供了acquire方法来获取一个许可,会阻塞线程(有重载方法,可以指定获取许可的个数)public void acquire() throws InterruptedException {sync.acquireSharedInterruptibly(1); //调用AQS的acquireSharedInterruptibly方法, 即共享式获取响应中断}//tryAcquire的意思是尝试获取许可,如果获取成功返回true,否则返回false,不会阻塞线程,而且不响应中断public boolean tryAcquire() {return sync.nonfairTryAcquireShared(1) >= 0;}//Semaphore提供release来释放许可public void release() {sync.releaseShared(1); //调用AQS的releaseShared方法,即释放共享式同步状态}abstract static class Sync extends AbstractQueuedSynchronizer {private static final long serialVersionUID = 1192457210091910933L;Sync(int permits) {setState(permits);}//获取许可数目 final int getPermits() {return getState();}//共享模式下非公平策略获取//本质就是一个自旋方法,通过自旋+CAS来保证修改许可值的线程安全性,该方法返回的情况有如下两种情况:// 信号量不够,直接返回,返回值为负数,表示获取失败;// 信号量足够,且CAS操作成功,返回值为剩余许可值,获取成功。final int nonfairTryAcquireShared(int acquires) {for (;;) { //自旋int available = getState(); //获取可用许可值int remaining = available - acquires; //计算剩余的许可值//如果剩余许可值小于0,说明许可不够用了,直接返回,否则CAS更新许可值,更新成功返回,否则继续自旋if (remaining < 0 ||compareAndSetState(available, remaining))return remaining;}}//共享模式下进行释放//该方法也是一个自旋方法,通过自旋+CAS原子性地修改许可值protected final boolean tryReleaseShared(int releases) {for (;;) { //自旋int current = getState(); //获取许可值int next = current + releases; //计算释放后的许可值if (next < current) // overflowthrow new Error("Maximum permit count exceeded");if (compareAndSetState(current, next)) //CAS修改许可值,成功则返回,失败则继续自旋return true;}}//根据指定的缩减量减小可用许可的数目final void reducePermits(int reductions) {for (;;) {int current = getState();int next = current - reductions;if (next > current) // underflowthrow new Error("Permit count underflow");if (compareAndSetState(current, next))return;}}//获取并返回立即可用的所有许可数目final int drainPermits() {for (;;) {int current = getState();if (current == 0 || compareAndSetState(current, 0))return current;}}}//采用非公平策略获取资源static final class NonfairSync extends Sync {private static final long serialVersionUID = -2694183684443567898L;NonfairSync(int permits) {super(permits);}//获取许可protected int tryAcquireShared(int acquires) {return nonfairTryAcquireShared(acquires); //共享模式下非公平策略获取}}//采用公平策略获取资源static final class FairSync extends Sync {private static final long serialVersionUID = 2014338818796000944L;FairSync(int permits) {super(permits);}//获取许可protected int tryAcquireShared(int acquires) {for (;;) {//获取共享锁之前,先调用hasQueuedPredecessors方法来判断队列中是否存在其他正在排队的节点,// 如果是返回true,否则为false。因此当存在其他正在排队的节点,当前节点就无法获取许可,只能排队等待,这也是公平策略的体现。if (hasQueuedPredecessors())return -1;int available = getState();int remaining = available - acquires;if (remaining < 0 ||compareAndSetState(available, remaining))return remaining;}}}
}
2.2 核心方法
获取信号量的方法总共有四个:
释放信号量的方法有两个:
获取信号量四个方法中后面三个方法原理同 acquire() ,我们这里来分析一下 acquire() 和 release() 方法。
2.2.1 acquire() 方法
获取许可,会阻塞线程,响应中断。
// Semaphore
public void acquire() throws InterruptedException {sync.acquireSharedInterruptibly(1);
}
内部调用的是 AQS 的 acquireSharedInterruptibly() 方法, 即共享式获取响应中断,代码如下:
// AbstractQueuedSynchronizer
public final void acquireSharedInterruptibly(int arg)throws InterruptedException {if (Thread.interrupted())throw new InterruptedException();if (tryAcquireShared(arg) < 0)doAcquireSharedInterruptibly(arg);
}
除了 tryAcquireShared() 方法由 AQS 子类实现,其他方法在 《AQS实现原理》中有讲解过,这里不再赘述。我们来分析一下子类实现的 tryAcquireShared() 方法,这里就要分公平和非公平策略两种情况了。
2.2.1.1 非公平策略下
非公平策略下的 tryAcquireShared() 方法:
// Semaphore#NonfairSync
protected int tryAcquireShared(int acquires) {return nonfairTryAcquireShared(acquires);
}
内部调用 Sync#nonfairTryAcquireShared() 方法:
// Sync
final int nonfairTryAcquireShared(int acquires) {//自旋for (;;) {//获取可用许可值int available = getState();//计算剩余的许可值int remaining = available - acquires;//如果剩余许可值小于0,说明许可不够用了,直接返回,否则CAS更新同步状态,更新成功返回,否则继续自旋if (remaining < 0 ||compareAndSetState(available, remaining))return remaining;}
}
该方法本质就是一个自旋方法,通过自旋+CAS来保证修改许可值的线程安全性。方法返回的情况有如下两种情况
- 信号量不够,直接返回,返回值为负数,表示获取失败;
- 信号量足够,且CAS操作成功,返回值为剩余许可值,获取成功。
2.2.1.2 公平策略下
公平策略下的 tryAcquireShared() 方法如下:
// Semaphore#FairSync
protected int tryAcquireShared(int acquires) {//自旋for (;;) {if (hasQueuedPredecessors())return -1;int available = getState();int remaining = available - acquires;if (remaining < 0 ||compareAndSetState(available, remaining))return remaining;}
}
我们看到它与非公平策略的唯一区别就是多了下面这个 if 代码:
protected int tryAcquireShared(int acquires) {for (;;) {if (hasQueuedPredecessors())return -1;......}
}// AbstractQueuedSynchronizer
public final boolean hasQueuedPredecessors() {// The correctness of this depends on head being initialized// before tail and on head.next being accurate if the current// thread is first in queue.Node t = tail; // Read fields in reverse initialization orderNode h = head;Node s;return h != t &&((s = h.next) == null || s.thread != Thread.currentThread());
}
即在获取共享锁之前,先调用 hasQueuedPredecessors() 方法来判断队列中是否存在其他正在排队的节点,如果是返回true,否则为false。因此当存在其他正在排队的节点,当前节点就无法获取许可,只能排队等待,这也是公平策略的体现。
2.2.2 release() 方法
Semaphore 提供 release() 方法来释放许可。我们继续分析 release() 方法,源码如下:
// Semaphore
public void release() {sync.releaseShared(1);
}//AbstractQueuedSynchronizer
public final boolean releaseShared(int arg) {if (tryReleaseShared(arg)) {//如果释放锁成功,唤醒正在排队的节点doReleaseShared();return true;}return false;
}//Semaphore#Sync
protected final boolean tryReleaseShared(int releases) {//自旋for (;;) {//获取许可值int current = getState();//计算释放后的许可值int next = current + releases;//如果释放后比释放前的许可值还小,直接报Errorif (next < current) // overflowthrow new Error("Maximum permit count exceeded");//CAS修改许可值,成功则返回,失败则继续自旋if (compareAndSetState(current, next))return true;}
}
tryReleaseShared() 方法是一个自旋方法,通过自旋+CAS原子性地修改同步状态,逻辑很简单。
2.2.3 其余方法
获取信号量的方法有四个:
释放信号量的方法有两个:
其余获取和释放信号量的方法原理同上问,不再赘述。接下来看看其余的工具方法。
2.2.3.1 tryAcquire() 尝试获取许可
该方法一共有四种重载形式:
- tryAcquire() :尝试获取许可,如果获取成功返回true,否则返回false,不会阻塞线程,而且不响应中断。
- tryAcquire(int permits) :同上的基础上,可以指定获取许可的个数。
- tryAcquire(long timeout, TimeUnit unit) :指定超时时间,它调用AQS的tryAcquireSharedNanos() 方法,即共享式超时获取。
- tryAcquire(int permits, long timeout, TimeUnit unit) :可以指定获取许可的个数和超时时间。
//Semaphore
public boolean tryAcquire() {return sync.nonfairTryAcquireShared(1) >= 0;
}public boolean tryAcquire(int permits) {if (permits < 0) throw new IllegalArgumentException();return sync.nonfairTryAcquireShared(permits) >= 0;
}public boolean tryAcquire(long timeout, TimeUnit unit)throws InterruptedException {return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}public boolean tryAcquire(int permits, long timeout, TimeUnit unit)throws InterruptedException {if (permits < 0) throw new IllegalArgumentException();return sync.tryAcquireSharedNanos(permits, unit.toNanos(timeout));
}
2.2.3.2 availablePermits() 获取可用许可数
源码如下:
//Semaphore
public int availablePermits() {//获取可用许可数return sync.getPermits();
}//Sync
//获取可用许可数
final int getPermits() {return getState();
}
2.2.3.3 drainPermits() 耗光信号量
将剩下的信号量一次性消耗光,并且返回所消耗的信号量。
//Semaphore
public int drainPermits() {return sync.drainPermits();
}//Sync
final int drainPermits() {//自旋操作for (;;) {//获取信号量值int current = getState();//如果信号量为0,直接返回//否则CAS修改为0,成功则返回,否则继续自旋if (current == 0 || compareAndSetState(current, 0))return current;}
}
2.2.3.4 reducePermits() 减少信号量
reducePermits() 和 acquire() 方法相比都是减少信号量的值,但是 reducePermits() 不会导致任何线程阻塞,即只要传递的参数 reductions(减少的信号量的数量)大于0,操作就会成功。所以调用该方法可能会导致信号量最终为负数。
//Semaphore
protected void reducePermits(int reduction) {if (reduction < 0) throw new IllegalArgumentException();sync.reducePermits(reduction);
}//Sync
final void reducePermits(int reductions) {//自旋for (;;) {//获取当前信号量值int current = getState();//计算剩余许可值int next = current - reductions;if (next > current) // underflowthrow new Error("Permit count underflow");//CAS修改同步状态,成功则返回,失败则继续自旋if (compareAndSetState(current, next))return;}
}
三、使用案例
这里以经典的停车作为案例。假设停车场有3个停车位,此时有5辆汽车需要进入停车场停车。
public static void main(String[] args) {//定义semaphore实例,设置许可数为3,即停车位为3个Semaphore semaphore = new Semaphore(3);//创建五个线程,即有5辆汽车准备进入停车场停车for (int i = 1; i <= 5; i++) {new Thread(() -> {try {System.out.println(Thread.currentThread().getName() + "尝试进入停车场...");//尝试获取许可semaphore.acquire();//模拟停车long time = (long) (Math.random() * 10 + 1);System.out.println(Thread.currentThread().getName() + "进入了停车场,停车" + time +"秒...");Thread.sleep(time);} catch (InterruptedException e) {e.printStackTrace();} finally {System.out.println(Thread.currentThread().getName() + "开始驶离停车场...");//释放许可semaphore.release();System.out.println(Thread.currentThread().getName() + "离开了停车场!");}}, i + "号汽车").start();}
}
//执行结果
1号汽车尝试进入停车场...
5号汽车尝试进入停车场...
4号汽车尝试进入停车场...
3号汽车尝试进入停车场...
2号汽车尝试进入停车场...
5号汽车进入了停车场,停车5秒...
1号汽车进入了停车场,停车8秒...
4号汽车进入了停车场,停车9秒...
5号汽车开始驶离停车场...
5号汽车离开了停车场!
3号汽车进入了停车场,停车10秒...
1号汽车开始驶离停车场...
1号汽车离开了停车场!
2号汽车进入了停车场,停车2秒...
4号汽车开始驶离停车场...
4号汽车离开了停车场!
2号汽车开始驶离停车场...
2号汽车离开了停车场!
3号汽车开始驶离停车场...
3号汽车离开了停车场!
相关文章:
【Java并发编程】信号量Semaphore详解
一、简介 Semaphore(信号量):是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。 Semaphore 一般用于流量的控制,特别是公共资源有限的应用场景。例如数据库的连接&am…...
window11使用wsl2安装Ubuntu22.04
目录 1、快速了解wsl2 安装子系统linux流程(B站视频) 2、wsl2常用命令 3、windows与子系统Linux文件访问方法 4、子系统linux使用windows网络代理、网络配置(镜像网络,非NAT) 5、wsl2 Ubuntu miniconda 安装 6、…...
虚拟滚动 - 从基本实现到 Angular CDK
简介 在大数据列表的处理上,虚拟滚动是一种优化性能的有效方式。本篇文章将详细介绍两种常见的虚拟滚动实现方式:使用 transform 属性和 Intersection Observer。重点讲解如何通过 transform 属性实现高效的虚拟滚动,并对比Angular CDK中的实…...
Spring WebFlux学习笔记(一)
核心思想 WebFlux主要是异步 例子 参考一个源码: https://blog.csdn.net/qq_43923045/article/details/106309432?spm1001.2014.3001.5506 GetMapping("/delay1")public Mono<RestResult> delayResult() {long l System.currentTimeMillis();…...
富格林:正确追损思维安全交易
富格林指出,对于如何正确追损的这个问题是需要持续付出时间和精力的,发现具备耐心的投资者往往在正确追损的路上更加游刃有余。他们总是可以保持较为平和的心态,不急不躁地分析原因并通过自身掌握的安全应对措施来进行交易。富格林在以下分享…...
前端vue2迁移至uni-app
1.确定文件存放位置 components: 继续沿用 pages: views内容移动到pages static: assets内容移动到static uni_modules: uni-app的插件存放位置 迁移前 src├─assets│ └─less├─components│ ├─common│ │ ├─CommentPart│ │ └─MessDetail│ ├─home│…...
恋爱脑学Rust之闭包三Traits:Fn,FnOnce,FnMut
在Rust中,FnOnce、FnMut和Fn是三个用于表示闭包(closure)类型的trait。闭包是一种特殊的函数,它可以捕获其环境变量,即在其定义时所处的作用域中的变量。以下是关于这三个trait的详细介绍: 1. FnOnce&#…...
区块链介绍
区块链(英文名:blockchain或block chain)是一种块链式存储、不可篡改、安全可信的去中心化分布式账本,它结合了分布式存储、点对点传输、共识机制、密码学等技术,通过不断增长的数据块链(Blocks)…...
git回滚间隔的提交
如果你需要回滚几个非连续的提交,可以使用 git revert 来选择性地撤销这些提交。这样做不会改变提交历史,只是会在当前分支上创建新的提交来反转指定的更改。 ### 使用 git revert 回滚间隔的提交 1. **查看提交历史**: 首先,…...
Map和Set(数据结构)
一、概念 Map 和 set 是一种专门用来进行搜索的容器或者数据结构,其搜索的效率与其具体的实例化子类有关。 Map 和 Set 是一种适合动态查找的集合容器。 模型 一般把搜索的数据称为关键字( Key ),和关键字对应的称为值࿰…...
vue3uniapp实现自定义拱形底部导航栏,解决首次闪烁问题
前言: 我最初在网上翻阅查找了很多方法,发现大家都是说在page.json中tabbar中添加:"custom": true,即可解决首次闪烁的问题,可是添加了我这边还是会闪烁,因此我这边改变了思路,使用了虚拟页面来解…...
新需求编码如何注意低级错误代码
1. 日常开发常见错误问题 变量拷贝未修改变量定义的值刚开始是随意写的一个值,想等到上线的时候再改成正确的,但是上线的时候忘记改了程序常量配置的错误逻辑关系判断错误 常见的如都不为null、都不为空集合判断不为空逻辑取反了多个关系的 && …...
系统架构图设计(行业领域架构)
物联网 感知层:主要功能是感知和收集信息。感知层通过各种传感器、RFID标签等设备来识别物体、采集信息,并对这些信息进行初步处理。这一层的作用是实现对物理世界的感知和初步处理,为上层提供数据基础网络层:网络层负责处理和传输…...
windows 文件监控 c++ 11及以上版本可用
在该版本上稍微改了一下https://blog.csdn.net/weixin_50964512/article/details/125002563 #include<iostream> #include<string> #include<Windows.h> #include<list> #include<locale> using namespace std;class WatchFolder {HANDLE m_hFi…...
jsMind:炸裂项目,用JavaScript构建的思维导图库,GitHub上的热门开源项目
嗨,大家好,我是小华同学,关注我们获得“最新、最全、最优质”开源项目和工作学习方法 jsMind 是一个基于 JavaScript 的思维导图库,它利用 HTML5 Canvas 和 SVG 技术构建,可以轻松地在网页中嵌入和编辑思维导图。它以 …...
postman的脚本设置接口关联
pm常用的对象 变量基础知识 postman获取响应结果的脚本的编写 下面是购物场景存在接口信息的关联 登录进入---搜索商品---进入商品详情---加入购物车 资源在附件中,可以私聊单独发送 postman的SHA256加密 var CryptoJS require(crypto-js);// 需要加密的字符串 …...
【python】OpenCV—Tracking(10.3)—GOTURN
文章目录 1、功能描述2、模型介绍3、代码实现4、完整代码5、结果展示6、优缺点分析7、参考 1、功能描述 基于 Generic Object Tracking using Regression Networks 方法,实现单目标跟踪 2、模型介绍 (1)发表来自 Held D, Thrun S, Savarese…...
git pull遇到一个问题
shell request failed on channel 0 需要修改服务器配置[rootadmin ~]# cat /etc/security/limits.d/20-nproc.conf # Default limit for number of users processes to prevent # accidental fork bombs. # See rhbz #432903 for reasoning.* soft nproc 409…...
书生-第四期闯关:完成SSH连接与端口映射并运行hello_world.py
端口映射完成后,访问127.0.0.1:7860成功展示如下界面: 书生浦语大模型实战营 项目地址:https://github.com/InternLM/Tutorial/...
【CSS3】css开篇基础(5)
1.❤️❤️前言~🥳🎉🎉🎉 Hello, Hello~ 亲爱的朋友们👋👋,这里是E绵绵呀✍️✍️。 如果你喜欢这篇文章,请别吝啬你的点赞❤️❤️和收藏📖📖。如果你对我的…...
AI产品独立开发变现实战营,炒掉老板做自由职业赚大钱
课程背景 在经济下行和外部就业压力增大的背景下,为解决程序员的焦虑、失业和被裁员,我们开始了这门课程,课程基于3个真实已经盈利的商业项目,从0到1带你实践AI产品的设计、开发、运营和盈利模式的全流程开发。 课程特色 增加‘…...
【UE5.3 Cesium for Unreal】编译GlobePawn
目录 前言 效果 步骤 一、下载所需文件 二、下载CesiumForUnreal插件 三、处理下载的文件 四、修改代码 “CesiumForUnreal.uplugin”部分 “CesiumEditor.cpp”部分 “CesiumEditor.h”部分 “CesiumPanel.cpp”部分 “IonQuickAddPanel.cpp”部分 “IonQuickAd…...
idea连接数据库出现错误的解决方式
在使用idea连接数据库时,出现错误: The server has terminated the handshake. The protocol list option (enabledTLSProtocols) is set, this option might cause connection issues with some versions of MySQL. Consider removing the protocol li…...
数据分级分类工具:敏感数据识别中的AI智能化转型之路
背景 在现代数字化和信息化飞速发展的背景下,数据安全愈发成为企业与组织的重要课题,尤其是敏感数据的保护更是重中之重。敏感数据的泄露不仅会导致商业损失和法律责任,还会直接影响客户信任和企业声誉。为此,数据分级分类工具逐…...
乘云而上,OceanBase再越山峰
一座山峰都是一个挑战,每一次攀登都是一次超越。 商业数据库时代,面对国外数据库巨头这座大山,实现市场突破一直都是中国数据库产业多年夙愿,而OceanBase在金融核心系统等领域的攻坚克难,为产业突破交出一副令人信服的…...
设计模式4-工厂模式策略模式
目录 一 工厂模式 1.1 思想 1.2 案例 1.2.1 接口 1.2.2 实现类 1.2.3 工厂类 1.2.4 调用 二 策略模式 2.1 思想 2.2 案例 2.2.1 接口 2.2.2 实现类 2.2.3 策略类 2.2.4 调用 三 工厂模式策略模式 3.1 思想 3.2 案例 3.2.1 接口 3.2.2 实现类 3.2.3 定义F…...
使用Html5基本标签实现“时空电影网”案例步骤及详细代码
根据您的需求,我为您实现了对“时空电影网”电影节页面的美化。以下是详细的步骤: 设置一级标题“电影节”文字的颜色:将一级标题的颜色设置为深蓝色(#0000FF)。 <h1><font color"darkblue">电…...
Servlet 3.0 新特性全解
文章目录 Servlet3.0新特性全解Servlet 3.0 新增特性Servlet3.0的注解Servlet3.0的Web模块支持servlet3.0提供的异步处理提供异步原因实现异步原理配置servlet类成为异步的servlet类具体实现异步监听器改进的ServletAPI(上传文件) Servlet3.0新特性全解 tomcat 7以上的版本都支…...
VUE组件学习 | 五、v-for组件
v-for 指令基础知识 v-for 是 Vue.js 中的一个指令,用于基于源数据多次渲染元素或模板块。它类似于 JavaScript 中的 for 循环。 基本语法 <template><div><!-- 基本列表渲染 --><ul><li v-for"item in items" :key"i…...
uniapp写移动端,适配苹果手机底部导航栏,ios安全区问题,苹果手机遮挡底部信息,uview的u-action-sheet组件
手机上有很多组件,需要手机底部弹窗来做选择,picker选择器,select列选择器呀这些,在苹果手机上会被底部nav遮住 采用了好几种配置的方式,多多少少都不太行,还是采用css来做吧,但是css来写想让它生效&#x…...
大学网络推广培训/成都百度seo公司
2019独角兽企业重金招聘Python工程师标准>>> 问题描述:在Linux RedHat5.5中安装的JDK1.7,并且通过在profile文件中修改JAVA_HOME,发现java -version命令依旧显示1.4版本,分析原因发现系统不按照profile的设定来执行jav…...
做网站哪里需要用钱/成人厨师短期培训班
load方法中能实现什么? 我们经常会有一些需求:让某方法的执行先于main方法。比如要hook某个系统类的方法(hook UIViewController的viewWillAppear和viewDidDisappear通过AOP的方式实现埋点功能),或者做一些初始化操作。…...
公司做竞拍网站的收入怎么报税/现在什么app引流效果好
2019独角兽企业重金招聘Python工程师标准>>> 学习阶段可以安装一个windows版本的mongodb,尽快把mongodb服务跑起来,让程序连上。mongodb的安装、运行、基础命令的知识,可以参考 菜鸟教程mongodb spring mongodb 依赖版本ÿ…...
wordpress视频插件w/南宁百度seo排名价格
Ceph 基础组件 Monitors(监视器,ceph-mon):Ceph Monitor 其维护集群状态映射,包括监视器映射、OSD映射、MDS映射、CRUSH映射;通过保存集群状态的映射来跟踪整个集群的健康状况。除此之外 Monitor 还负载管理守护进程和客户端之间的…...
wordpress主题 标签/广告主资源哪里找
多列-column-rule-style列之间规则的样式 微信小程序交流群:111733917 | 微信小程序从0基础到就业的课程:https://edu.csdn.net/topic/huangjuhua 通用语法 定义和用法 column-rule-style 属性规定列之间的样式规则。 默认值: none 继承性&a…...
如何只做网站/微信客户管理
【概述】几乎每个企业都有自己的核心东西或说是框架性的东西,框架的好处是将我们经常要使用的功能,控件等包装一个个易于使用的单元,就算是初学者也极其容易上手,减少项目的开发成本。因此框架的重要性和好处是不言而喻的。在我的…...