共享模型之无锁
一、问题提出
1.1 需求描述
有如下的需求,需要保证 account.withdraw() 取款方法的线程安全,代码如下:
interface Account {// 获取余额Integer getBalance();// 取款void withdraw(Integer amount);/*** 方法内会启动 1000 个线程,每个线程做 -10 元 的操作* 如果初始余额为 10000 那么正确的结果应当是 0*/static void demo(Account account) {List<Thread> ts = new ArrayList<>();long start = System.nanoTime();for (int i = 0; i < 1000; 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");}
}
class AccountUnsafe implements Account {private Integer balance;public AccountUnsafe(Integer balance) {this.balance = balance;}@Overridepublic Integer getBalance() {return balance;}@Overridepublic void withdraw(Integer amount) {balance -= amount;}public static void main(String[] args) {Account.demo(new AccountUnsafe(10000));}
}
原有的实现并不是线程安全的,执行结果如下所示:
1.2 问题分析
为什么会出现线程安全问题?是因为在多线程的环境下取款的 withdraw() 方法里面是临界区,存在指令交错的行为。
1.3 加锁解决
首先想到的解决方式就是给 Account 对象加锁,如下代码:
class AccountUnsafe implements Account {private Integer balance;public AccountUnsafe(Integer balance) {this.balance = balance;}@Overridepublic Integer getBalance() {synchronized (this){return balance;}}@Overridepublic void withdraw(Integer amount) {synchronized (this){balance -= amount;}}public static void main(String[] args) {Account.demo(new AccountUnsafe(10000));}
}
运行结果如下,没有任何问题。
1.4 无锁解决
也可以通过无锁的方式解决上述的问题,如下代码:
public class AccountSafe implements Account{private AtomicInteger balance;public AccountSafe(Integer balance) {this.balance = new AtomicInteger(balance);}@Overridepublic Integer getBalance() {return balance.get();}@Overridepublic void withdraw(Integer amount) {while(true){int prev = balance.get();int next = prev - amount;if(balance.compareAndSet(prev,next)){break;}}}public static void main(String[] args) {Account.demo(new AccountSafe(10000));}
}
运行结果如下,没有任何问题。
二、CAS 与 volatile
2.1 CAS
在上一小结看到的使用 AtomicInteger 的解决方法,内部并没有用锁来保护共享变量的线程安全。那么它是如何实现的呢?
@Overridepublic void withdraw(Integer amount) {// 需要不断尝试,直到成功为止while(true){// 比如拿到了旧值 1000int prev = balance.get();// 在这个基础上 1000-10 = 990int next = prev - amount;/** compareAndSet 会做一个检查,在 set 值之前先比较 prev 和当前值* 若 prev 值和当前值一致,则用 next 设置为新值,并返回 true 表示成功。* 若 prev 值和当前值不一致,则 next 作废,返回 false 表示失败,进入 while 下次循环重试* */if(balance.compareAndSet(prev,next)){break;}}}
这里面最关键的就是 compareAndSet() 方法,它的简称就是 CAS(也有 compare and swap 的说法),此方法是一个原子操作。
其实 CAS 的底层是 lock cmpxchg 指令,在单核 CPU 和多核 CPU 下都能够保证原子性。
2.2 volatile
获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰。
它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。即一个线程对 volatile 变量的修改,对另一个线程可见。
volatile 仅仅保证了共享变量的可见性,让其它线程能够看到最新值,但不能解决指令交错问题(不能保证原子性)
CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果。
2.3 为什么无锁效率高
无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。
但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换。
2.4 CAS 的特点
将 CAS 和 volatile 结合使用可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。
CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试。
synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会
CAS 体现的是无锁并发、无阻塞并发。因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一;但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响 。
三、原子整数
在 JUC 并发包下提供了一些原子整数的工具类,如:AtomicBoolean、AtomicInteger 和 AtomicLong,这几个类的用法类似,下面就以 AtomicInteger 为例,介绍下常用的方法。
public class AtomicTest {public static void main(String[] args) {// 无参构造的初始值为 0,有参构造的初始值需要自己指定AtomicInteger i = new AtomicInteger(0);// 获取并自增,返回 0,类似于 i++System.out.println(i.getAndIncrement());// 自增并获取(i = 1, 结果 i = 2, 返回 2),类似于 ++iSystem.out.println(i.incrementAndGet());// 自减并获取(i = 2, 结果 i = 1, 返回 1),类似于 --iSystem.out.println(i.decrementAndGet());// 获取并自减(i = 1, 结果 i = 0, 返回 1),类似于 i--System.out.println(i.getAndDecrement());// 获取并加值(i = 0, 结果 i = 5, 返回 0)System.out.println(i.getAndAdd(5));// 加值并获取(i = 5, 结果 i = 0, 返回 0)System.out.println(i.addAndGet(-5));// 获取并更新(i = 0, p 为 i 的当前值, 结果 i = -2, 返回 0)// 其中函数中的操作能保证原子,但函数需要无副作用System.out.println(i.getAndUpdate(p -> p - 2));// 更新并获取(i = -2, p 为 i 的当前值, 结果 i = 0, 返回 0)// 其中函数中的操作能保证原子,但函数需要无副作用System.out.println(i.updateAndGet(p -> p + 2));// 获取并计算(i = 0, p 为 i 的当前值, x 为参数1, 结果 i = 10, 返回 0)// 其中函数中的操作能保证原子,但函数需要无副作用// getAndUpdate 如果在 lambda 中引用了外部的局部变量,要保证该局部变量是 final 的// getAndAccumulate 可以通过 参数1 来引用外部的局部变量,但因为其不在 lambda 中因此不必是 finalSystem.out.println(i.getAndAccumulate(10, (p, x) -> p + x));// 计算并获取(i = 10, p 为 i 的当前值, x 为参数1, 结果 i = 0, 返回 0)// 其中函数中的操作能保证原子,但函数需要无副作用System.out.println(i.accumulateAndGet(-10, (p, x) -> p + x));}
}
四、原子引用
为什么需要原子引用,因为共享的数据并不一定都是基本数据类型的,还有可能是小数类型,那么我们就可以使用原子引用来保证其中的共享变量操作时的线程安全。
原子引用分为如下几种:AtomicReference、AtomicMarkableReference、AtomicStampedReference
4.1 AtomicReference
以最开始的例子为例,假设此时的账户余额是 BigDecimal 类型,我们就需要使用 AtomicReferenceAtomicReference,如下代码:
interface Account {// 获取余额BigDecimal getBalance();// 取款void withdraw(BigDecimal amount);/*** 方法内会启动 1000 个线程,每个线程做 -10 元 的操作* 如果初始余额为 10000 那么正确的结果应当是 0*/static void demo(Account account) {List<Thread> ts = new ArrayList<>();long start = System.nanoTime();for (int i = 0; i < 1000; i++) {ts.add(new Thread(() -> {account.withdraw(BigDecimal.TEN);}));}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");}
}
public class AccountSafe implements Account{private AtomicReference<BigDecimal> balance;public AccountSafe(BigDecimal balance) {this.balance = new AtomicReference(balance);}@Overridepublic BigDecimal getBalance() {return balance.get();}@Overridepublic void withdraw(BigDecimal amount) {// 需要不断尝试,直到成功为止while(true){BigDecimal prev = balance.get();// 调用 subtract 相当于减的操作BigDecimal next = prev.subtract(amount);if(balance.compareAndSet(prev,next)){break;}}}public static void main(String[] args) {Account.demo(new AccountSafe(new BigDecimal("10000")));}
}
测试结果如下,没有任何问题。
4.2 ABA 问题
主线程仅能判断出共享变量的值与最初值 A 是否相同,不能感知到这种从 A 改为 B 又 改回 A 的情况,这就是我们所说的 ABA 问题,如下代码:
@Slf4j(topic = "c.test")
public class Main8 {static AtomicReference<String> ref = new AtomicReference<>("A");public static void main(String[] args) throws InterruptedException {log.debug("main start...");// 获取值 A// 这个共享变量被它线程修改过?String prev = ref.get();other();Thread.sleep(1000);// 尝试改为 Clog.debug("change A->C {}", ref.compareAndSet(prev, "C"));}private static void other() throws InterruptedException {new Thread(() -> {log.debug("change A->B {}", ref.compareAndSet(ref.get(), "B"));}, "t1").start();Thread.sleep(500);new Thread(() -> {log.debug("change B->A {}", ref.compareAndSet(ref.get(), "A"));}, "t2").start();}
}
输出结果如下:
4.3 AtomicStampedReference
如果主线程希望只要有其它线程动过了共享变量,那么自己的 cas 就算失败,这时,仅比较值是不够的,需要再加一个版本号 AtomicStampedReference。
AtomicStampedReference 可以给原子引用加上版本号,追踪原子引用整个的变化过程,如: A -> B -> A ->C ,通过 AtomicStampedReference,我们可以知道,引用变量中途被更改了几次。如下代码:
@Slf4j(topic = "c.test")
public class Main9 {// 不光给变量一个初始值,还给一个初始的版本号static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);public static void main(String[] args) throws InterruptedException {log.debug("main start...");// 获取值 AString prev = ref.getReference();// 获取版本号int stamp = ref.getStamp();log.debug("版本 {}", stamp);// 如果中间有其它线程干扰,发生了 ABA 现象other();Thread.sleep(1000);// 尝试改为 C,此时 compareAndSet 方法需要四个参数,当前值,期望值,当前版本号,期望版本号log.debug("change A->C {}", ref.compareAndSet(prev, "C", stamp, stamp + 1));}private static void other() throws InterruptedException {new Thread(() -> {log.debug("change A->B {}", ref.compareAndSet(ref.getReference(), "B",ref.getStamp(), ref.getStamp() + 1));log.debug("更新版本为 {}", ref.getStamp());}, "t1").start();Thread.sleep(500);new Thread(() -> {log.debug("change B->A {}", ref.compareAndSet(ref.getReference(), "A",ref.getStamp(), ref.getStamp() + 1));log.debug("更新版本为 {}", ref.getStamp());}, "t2").start();}
}
可以看到,解决了 ABA 问题,更新并没有成功。
4.4 AtomicMarkableReference
但是有时候,并不关心引用变量更改了几次,只是单纯的关心是否更改过,用一个布尔值就可以搞定,所以就有了 AtomicMarkableReference。
如下案例,主人要检查垃圾袋满没满,是否需要倒垃圾,如果满了则更换新的垃圾袋;如果还空着呢,就用原有的垃圾袋。此时还有另外一个线程保洁阿姨,她负责倒空垃圾袋里面的垃圾,但是她还是用原来的垃圾袋,如果此时主人检查垃圾袋是空的就不用再去更换垃圾袋了。
代码如下所示:
@Slf4j(topic = "c.test")
public class Main9 {public static void main(String[] args) throws InterruptedException {GarbageBag bag = new GarbageBag("装满了垃圾");// 参数2 mark 可以看作一个标记,表示垃圾袋满了AtomicMarkableReference<GarbageBag> ref = new AtomicMarkableReference<>(bag, true);log.debug("主线程 start...");GarbageBag prev = ref.getReference();log.debug(prev.toString());new Thread(() -> {log.debug("打扫卫生的线程 start...");bag.setDesc("空垃圾袋");while (!ref.compareAndSet(bag, bag, true, false)) {}log.debug(bag.toString());}).start();Thread.sleep(1000);log.debug("主线程想换一只新垃圾袋?");boolean success = ref.compareAndSet(prev, new GarbageBag("空垃圾袋"), true, false);log.debug("换了么?" + success);log.debug(ref.getReference().toString());}
}class GarbageBag {String desc;public GarbageBag(String desc) {this.desc = desc;}public void setDesc(String desc) {this.desc = desc;}@Overridepublic String toString() {return super.toString() + " " + desc;}
}
输出结果如下所示:
五、原子数组
原子数据保护的是数组里面的元素,常用的原子数组类是 AtomicIntegerArray、AtomicLongArray 和 AtomicReferenceArray,测试类的代码如下所示:
@Slf4j(topic = "c.test")
public class Main9 {public static void main(String[] args) throws InterruptedException {// 不安全的数组demo(() -> new int[10],(array) -> array.length,(array, index) -> array[index]++,array -> System.out.println(Arrays.toString(array)));// 安全的数组demo(() -> new AtomicIntegerArray(10),(array) -> array.length(),(array, index) -> array.getAndIncrement(index),array -> System.out.println(array));}/*** 参数1,提供数组、可以是线程不安全数组或线程安全数组* 参数2,获取数组长度的方法* 参数3,自增方法,回传 array, index* 参数4,打印数组的方法*/// supplier 提供者 无中生有 ()->结果// function 函数 一个参数一个结果 (参数)->结果 , BiFunction (参数1,参数2)->结果// consumer 消费者 一个参数没结果 (参数)->void, BiConsumer (参数1,参数2)->private static <T> void demo(Supplier<T> arraySupplier,Function<T, Integer> lengthFun,BiConsumer<T, Integer> putConsumer,Consumer<T> printConsumer) {List<Thread> ts = new ArrayList<>();T array = arraySupplier.get();int length = lengthFun.apply(array);for (int i = 0; i < length; i++) {// 每个线程对数组作 10000 次操作ts.add(new Thread(() -> {for (int j = 0; j < 10000; j++) {putConsumer.accept(array, j % length);}}));}ts.forEach(t -> t.start()); // 启动所有线程ts.forEach(t -> {try {t.join();} catch (InterruptedException e) {e.printStackTrace();}}); // 等所有线程结束printConsumer.accept(array);}
}
输出结果如下所示:
六、字段更新器
字段更新器保护的是对象里面的某个属性,即对这个属性进行原子操作,但是需要配合 volatile 修饰的字段使用,否则会出现异常,如下代码:
public class Test5 {private volatile int field;public static void main(String[] args) {AtomicIntegerFieldUpdater fieldUpdater =AtomicIntegerFieldUpdater.newUpdater(Test5.class, "field");Test5 test5 = new Test5();fieldUpdater.compareAndSet(test5, 0, 10);// 修改成功 field = 10System.out.println(test5.field);// 修改成功 field = 20fieldUpdater.compareAndSet(test5, 10, 20);System.out.println(test5.field);// 修改失败 field = 20fieldUpdater.compareAndSet(test5, 10, 30);System.out.println(test5.field);}
}
输出结果如下:
七、原子累加器
累加器顾名思义,就是对一个整数进行累加的操作,在 jdk8 以后,新增了几个专门用于累加操作的工具类,比如:LongAdder、LongAccumulator 等,他们的性能要比我们的 AtomicLong 性能要高很多。
性能提升的原因很简单,就是在有竞争时,设置多个累加单元,Therad-0 累加 Cell[0],而 Thread-1 累加 Cell[1]... 最后将结果汇总。这样它们在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高性能。如下测试代码:
public class Test5 {public static void main(String[] args) {for (int i = 0; i < 5; i++) {demo(() -> new LongAdder(), adder -> adder.increment());}for (int i = 0; i < 5; i++) {demo(() -> new AtomicLong(), adder -> adder.getAndIncrement());}}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 个线程,每人累加 50 万for (int i = 0; i < 40; i++) {ts.add(new Thread(() -> {for (int j = 0; j < 500000; 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:" + (end - start) / 1000_000);}
}
输出结果如下所示:可以看到,相差的时间还是蛮大的:
相关文章:
共享模型之无锁
一、问题提出 1.1 需求描述 有如下的需求,需要保证 account.withdraw() 取款方法的线程安全,代码如下: interface Account {// 获取余额Integer getBalance();// 取款void withdraw(Integer amount);/*** 方法内会启动 1000 个线程…...
下载安装VSCode并添加插件作为仓颉编程入门编辑器
VSCode下载地址:下载 Visual Studio Code - Mac、Linux、Windows 插件下载:GitCode - 全球开发者的开源社区,开源代码托管平台 仓颉社区中下载解压 cangjie.vsix 插件 打开VSCode 按 Ctrl Shift X 弹出下图 按照上图步骤依次点击选中我们下…...
解决:Linux上SVN 1.12版本以上无法直接存储明文密码
问题:今天在Linux机器上安装了SVN,作为客户端使用,首次执行SVN相关操作,输入账号密码信息后,后面再执行SVN相关操作(比如"svn update")还是每次都需要输入密码。 回想以前在首次输入…...
Mongodb多键索引中索引边界的混合
学习mongodb,体会mongodb的每一个使用细节,欢迎阅读威赞的文章。这是威赞发布的第93篇mongodb技术文章,欢迎浏览本专栏威赞发布的其他文章。如果您认为我的文章对您有帮助或者解决您的问题,欢迎在文章下面点个赞,或者关…...
如何利用windows本机调用Linux服务器,以及如何调用jupyter界面远程操控
其实这篇文章没必要存在,教程太多了 参考博客(1 2 3),如侵删 奈何网上的大神总是会漏掉一些凡人遇到的小问题 (1) 建议下载PuTTy for windows,从而建立与远程服务器的SSH连接 需要确认目标服…...
如何定位Milvus性能瓶颈并优化
假设您拥有一台强大的计算机系统或一个应用,用于快速执行各种任务。但是,系统中有一个组件的速度跟不上其他部分,这个性能不佳的组件拉低了系统的整体性能,成为了整个系统的瓶颈。在软件领域中,瓶颈是指整个路径中吞吐…...
阿里云服务器 篇三:提交搜索引擎收录
文章目录 系列文章推荐:为网站注册域名判断网站是否已被搜索引擎收录主动提交搜索引擎收录未查询到收录结果时,根据提示进行提交网站提交网站时一般需要登录账号主动提交网站可缩短爬虫发现网站链接时间,但不保证一定能够收录所提交的网站百度提交地址360搜索提交地址搜狗提…...
powe bi界面认识及矩阵表基本操作 - 1
powe bi界面认识及矩阵表操作 1. 界面认识1.1 选择数据源1.2 选择相关表及点击加载1.3 表字段显示位置1.4 表属性按钮位置1.5 界面布局按钮认识 2. 矩阵表基本操作2.1 选择矩阵表2.2 创建矩阵表2.3 设置字体大小2.4 行填充:修改高度2.5 列宽:设置列的宽度…...
SpringBoot 项目 pom.xml 中 设置 Docker Maven 插件
在Spring Boot项目中,使用Docker Maven插件(通常是docker-maven-plugin或者fabric8io/docker-maven-plugin)来自动化构建Docker镜像并将其推送到远程仓库。 这里分别介绍这两种插件的基本配置,并说明如何设置远程仓库推送。 1、…...
k8s二次开发-kubebuiler一键式生成deployment,svc,ingress
一 Kubebuilder环境搭建 注:必须在当前的K8S集群有 nginx这个ingressclass rootk8s:~# kubectl get ingressclass NAME CONTROLLER PARAMETERS AGE nginx k8s.io/ingress-nginx <none> 19h1.1 下载kubebuilder wget https://gi…...
Flutter 状态管理新境界:多Provider并行驱动UI
前言 在上一篇文章中,我们讨论了如何使用 Provider 在 Flutter 中进行状态管理。 本篇文章我们来讨论如何使用多个 Provider。 在 Flutter 中,使用 Provider 管理多个不同的状态时,你可以为每个状态创建一个单独的 ChangeNotifierProvider…...
标识符和关键字的区别是什么,常用的关键字有哪些?自增自减运算符,移位运算符continue、break、return的区别是什么?
标识符和关键字的区别是什么,常用的关键字有哪些? 标识符标识符就是当我们给变量,方法,类命名时候的名字,而被赋予特殊含义的标识符就是关键字。例如生活中,当我们需要开一家店时候,我们不能将…...
在VS Code上搭建Vue项目教程(Vue-cli 脚手架)
1.前期环境准备 搭建Vue项目使用的是Vue-cli 脚手架。前期环境需要准备Node.js环境,就像Java开发要依赖JDK环境一样。 1.1 Node.js环境配置 1)具体安装步骤操作即可: npm 安装教程_如何安装npm-CSDN博客文章浏览阅读836次。本文主要在Win…...
AGI 之 【Hugging Face】 的【零样本和少样本学习】之三 [无标注数据] 的简单整理
AGI 之 【Hugging Face】 的【零样本和少样本学习】之三 [无标注数据] 的简单整理 目录 AGI 之 【Hugging Face】 的【零样本和少样本学习】之三 [无标注数据] 的简单整理 一、简单介绍 二、零样本学习 (Zero-shot Learning) 和少样本学习 (Few-shot Learning) 1、零样本学…...
Docker 和 k8s 之间是什么关系?
Docker 简介 Docker 功能: Docker 是一款可以将程序和环境打包并运行的工具软件。通过 Docker,可以将程序及其依赖环境打包,确保在不同操作系统上一致的运行效果。 环境一致性问题: 程序依赖于特定的环境,不同操作系统…...
敲详细的springframework-amqp-rabbit源码解析
看源码时将RabbitMQ的springframework-amqp-rabbit和spring-rabbit的一套区分开,springboot是基于RabbitMQ的Java客户端建立了简便易用的框架。 springboot的框架下相对更多地使用消费者Consumer和监听器Listener的概念,这两个概念不注意区分容易混淆。…...
Telegram Bot、小程序开发(三)Mini Apps小程序
文章目录 一、Telegram Mini Apps小程序二、小程序启动方式三、小程序开发小程序调试模式初始化小程序Keyboard Button Mini Apps 键盘按钮小程序【依赖具体用户信息场景,推荐】**Inline Button Mini Apps内联按钮小程序**initData 的自动传递使用内联菜单时候哪些参数会默认传…...
Django F()函数
F()函数的作用 F()函数在Django中是一个非常强大的工具,主要用于在查询表达式中引用模型的字段。它允许你在数据库层面执行各种操作,而无需将数据加载到Python内存中。这不仅提高了性能,还允许你利用数据库的优化功能。 字段引用 在查询表达…...
GraphRAG的实践
好久没有体验新技术了,今天来玩一下GraphRAG 顾名思义,一种检索增强的方法,利用图谱来实现RAG 1.配置环境 conda create -n GraphRAG python3.11 conda activate GraphRAG pip install graphrag 2.构建GraphRAG mkdir -p ./ragtest/i…...
自动驾驶三维车道线检测系列—LATR: 3D Lane Detection from Monocular Images with Transformer
文章目录 1. 概述2. 背景介绍3. 方法3.1 整体结构3.2 车道感知查询生成器3.3 动态3D地面位置嵌入3.4 预测头和损失 4. 实验评测4.1 数据集和评估指标4.2 实验设置4.3 主要结果 5. 讨论和总结 1. 概述 3D 车道线检测是自动驾驶中的一个基础但具有挑战性的任务。最近的进展主要依…...
守护动物乐园:视频AI智能监管方案助力动物园安全与秩序管理
一、背景分析 近日,某大熊猫参观基地通报了4位游客在参观时,向大熊猫室外活动场内吐口水的不文明行为。这几位游客的行为违反了入园参观规定并可能对大熊猫造成严重危害,已经被该熊猫基地终身禁止再次进入参观。而在此前,另一熊猫…...
FairGuard游戏加固入选《嘶吼2024网络安全产业图谱》
2024年7月16日,国内网络安全专业媒体——嘶吼安全产业研究院正式发布《嘶吼2024网络安全产业图谱》(以下简称“产业图谱”)。 本次发布的产业图谱,共涉及七大类别,127个细分领域。全面展现了网络安全产业的构成和重要组成部分,探…...
数据仓库事实表
数据仓库中的三种常见事实表类型:事务事实表、周期快照事实表和累积快照事实表 事务事实表: 事务事实表是记录事务级别数据的事实表。它记录了每个事务发生的具体度量指标,如销售金额、数量等。事务事实表的优势在于能够提供详细的事务级别…...
LeetCode题练习与总结:两数之和Ⅱ-输入有序数组--167
一、题目描述 给你一个下标从 1 开始的整数数组 numbers ,该数组已按 非递减顺序排列 ,请你从数组中找出满足相加之和等于目标数 target 的两个数。如果设这两个数分别是 numbers[index1] 和 numbers[index2] ,则 1 < index1 < index…...
在 Java 中,怎样设计一个可扩展且易于维护的微服务架构?
在Java中设计一个可扩展且易于维护的微服务架构,可以考虑以下几个方面: 模块化设计:将应用拆分为多个小的、独立的模块,每个模块负责处理特定的业务逻辑。每个模块可以独立开发、测试和部署,增加或替换模块时不会影响其…...
零基础入门鸿蒙开发 HarmonyOS NEXT星河版开发学习
今天开始带大家零基础入门鸿蒙开发,也就是你没有任何编程基础的情况下就可以跟着石头哥零基础学习鸿蒙开发。 目录 一,为什么要学习鸿蒙 1-1,鸿蒙介绍 1-2,为什么要学习鸿蒙 1-3,鸿蒙各个版本介绍 1-4࿰…...
Chromium CI/CD 之Jenkins实用指南2024-在Windows节点上创建任务(九)
1. 引言 在现代软件开发流程中,持续集成(CI)和持续交付(CD)已成为确保代码质量和加速发布周期的关键实践。Jenkins作为一款广泛应用的开源自动化服务器,通过其强大的插件生态系统和灵活的配置选项…...
ceph进程网卡绑定逻辑
main() //如osd进程,是ceph_osd.cc文件的main函数;mon进程,是ceph_mon.cc文件的main函数 -->pick_addresses() // 会读取"cluster_network_interface"和"public_network_interface"这两个配置项来过滤ip ---->fill…...
学习opencv
初步学习可以参考: OpenCV学习之路(附加资料分享)_opencv资料-CSDN博客 【OpenCV】OpenCV常用函数合集【持续更新】_opencv函数手册-CSDN博客 整体框架可以参考: OpenCV学习指南:从零基础到全面掌握(零…...
利用双端队列 实现二叉树的非递归的中序遍历
双端队列:双向队列:支持插入删除元素的线性集合。 java官方文档推荐用deque实现栈(stack)。 pop(): 弹出栈中元素,也就是返回并移除队头元素,等价于removeFirst(),如果队列无元素,则…...
哪些网站可以做设计软件/济宁百度推广公司
【黑果小兵】【微信首发】macOS Big Sur Beta 3 20A5323l Installer for OpenCore 0.6.0 and PE双EFI分区原版镜像今天,苹果发布了Big Sur 11.0 Beta3(20A5323l),这是苹果重新修正了大版本为11.0的首个版本。同时这也正式宣告了更新了十多年的10.xx版本…...
互联网推广品牌/东莞优化排名推广
springMVC下jsp引用外部js,css等静态资源的解决方法参考文章: (1)springMVC下jsp引用外部js,css等静态资源的解决方法 (2)https://www.cnblogs.com/selfchange/p/6255240.html 备忘一下。...
wordpress可视编辑器/网页制作教程书籍
今天学校放假了,去广州市区玩,见识见识大城市的魅力。 今天就不发代码了,给大家看一下广州市区的样子,好美啊!...
做h5最好的网站/免费收录链接网
总结一下auniapp开发安卓app的调试方式及打包; 代码编程查看文档即可.; 1通过vue-cli方式初始化项目 参考地址: https://uniapp.dcloud.io/quickstart-cli 2 将app实时安装到手机上 使用vscode编辑代码, 使用hubuilder去调试运行及打包 手机打开开发者模式 连接电脑 选中运…...
凡科网做网站花多少钱/互动营销平台
Arch一直在我笔记本里边,只是玩gentoo时我不进Arch了,现在回归Arch,升级到了最新,用上了gentoo的最新的2.6.31内核(自己配置,无initrd),引导程序用的grub4dos:Arch的启动脚本非常优雅,这也是我一眼就喜欢上Archlinux的原因之一:这是我的LXDE桌面:关机时的界面...
有关网站建设的公众号/软文营销经典案例200字
A. timestampdiff() 传三个参数,第一个时间类型如年,月,日,第二个开始时间,第三个结束时间select test_name, timestampdiff(YEAR,create_time,end_time) y_date from test_table; --计算时间-------------------| tes…...