CAS详解
文章目录
- CAS
- 使用示例
- Unsafe类
- 实现原理
- CAS问题
CAS
CAS全称为Compare and Swap被译为比较并交换,是一种无锁算法。用于实现并发编程中的原子操作。CAS操作检查某个变量是否与预期的值相同,如果相同则将其更新为新值。CAS操作是原子的,这意味着在多个线程同时执行CAS操作时,不会发生竞争条件。
使用示例
java.util.concurrent.atomic并发包下的所有原子类都是基于CAS来实现的。
public class CASExample {public static void main(String[] args) {AtomicInteger atomicInteger = new AtomicInteger(0);int expectedValue = 0;int newValue = 1;boolean result = atomicInteger.compareAndSet(expectedValue, newValue);if (result) {System.out.println("更新成功,当前值:" + atomicInteger.get());} else {System.out.println("更新失败,当前值:" + atomicInteger.get());}}
}
CAS一些常见使用场景:
- 使用CAS实现线程安全的计数器,避免传统锁的开销。
private AtomicInteger counter = new AtomicInteger(0);public int increment() {int oldValue, newValue;do {oldValue = counter.get();newValue = oldValue + 1;} while (!counter.compareAndSet(oldValue, newValue));return newValue; } - 使用CAS来实现无锁队列、栈等数据结构。
public class CASQueue<E> {private static class Node<E> {final E item;final AtomicReference<Node<E>> next = new AtomicReference<>(null);Node(E item) { this.item = item; }}private final AtomicReference<Node<E>> head = new AtomicReference<>(null);private final AtomicReference<Node<E>> tail = new AtomicReference<>(null);public void enqueue(E item) {Node<E> newNode = new Node<>(item);while (true) {Node<E> currentTail = tail.get();if (currentTail == null) {if (head.compareAndSet(null, newNode)) { tail.set(newNode); return; }} else {if (currentTail.next.compareAndSet(null, newNode)) { tail.compareAndSet(currentTail, newNode); return; }else { tail.compareAndSet(currentTail, currentTail.next.get()); }}}}public E dequeue() {while (true) {Node<E> currentHead = head.get();if (currentHead == null) { return null; }Node<E> nextNode = currentHead.next.get();if (head.compareAndSet(currentHead, nextNode)) { return currentHead.item; }}}} - 在数据库中,CAS可以用于实现乐观锁机制,避免长时间持有锁。
public class OptimisticLocking {private AtomicInteger version = new AtomicInteger(0);public boolean updateWithOptimisticLock(int expectedVersion, Runnable updateTask) {int currentVersion = version.get();if (currentVersion != expectedVersion) { return false; }updateTask.run();return version.compareAndSet(currentVersion, currentVersion + 1);}public int getVersion() { return version.get(); }public static void main(String[] args) {OptimisticLocking lock = new OptimisticLocking();Runnable updateTask = () -> System.out.println("Performing update");int version = lock.getVersion();boolean success = lock.updateWithOptimisticLock(version, updateTask);if (success) { System.out.println("Update successful."); } else { System.out.println("Update failed."); }} } - 在实现线程池时,CAS可以用于安全地管理线程状态和任务队列。
public class CASThreadPool {private static class Node<E> {final E item;final AtomicReference<Node<E>> next = new AtomicReference<>(null);Node(E item) { this.item = item; }}private final AtomicReference<Node<Runnable>> head = new AtomicReference<>(null);private final AtomicReference<Node<Runnable>> tail = new AtomicReference<>(null);public void submitTask(Runnable task) {Node<Runnable> newNode = new Node<>(task);while (true) {Node<Runnable> currentTail = tail.get();if (currentTail == null) {if (head.compareAndSet(null, newNode)) { tail.set(newNode); return; }} else {if (currentTail.next.compareAndSet(null, newNode)) { tail.compareAndSet(currentTail, newNode); return; }else { tail.compareAndSet(currentTail, currentTail.next.get()); }}}}public Runnable getTask() {while (true) {Node<Runnable> currentHead = head.get();if (currentHead == null) { return null; }Node<Runnable> nextNode = currentHead.next.get();if (head.compareAndSet(currentHead, nextNode)) { return currentHead.item; }}} }
Unsafe类
Unsafe是CAS的核心类,Java无法直接访问底层操作系统,而是通过native方法来访问。不过尽管如此,JVM还是开了一个后门,JDK中有一个类Unsafe,它提供了硬件级别的原子操作。
Unsafe类位于sun.misc包中,它提供了访问底层操作系统的特定功能,如直接内存访问、CAS 操作等。由于其提供了直接操作内存的能力,使用不当可能导致内存泄漏、数据损坏等问题,应谨慎使用。Unsafe类包含了许多不安全的操作,所以它并不是Java标准的一部分,而且在Java9开始已经标记为受限制的API。
Java中CAS操作的执行依赖于Unsafe类的方法,Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务。
public class UnsafeExample {private static final Unsafe unsafe;private static final long valueOffset;private volatile int value = 0;static {try {Field field = Unsafe.class.getDeclaredField("theUnsafe");field.setAccessible(true);unsafe = (Unsafe) field.get(null);valueOffset = unsafe.objectFieldOffset(UnsafeExample.class.getDeclaredField("value"));} catch (Exception e) {throw new Error(e);}}public void increment() {int current;do {current = unsafe.getIntVolatile(this, valueOffset);} while (!unsafe.compareAndSwapInt(this, valueOffset, current, current + 1));}}
实现原理
以AtomicInteger原子整型类为例,来看一下CAS实现原理。
public class MainTest {public static void main(String[] args) {new AtomicInteger().compareAndSet(1,2);}
}
调用栈如下:
compareAndSet--> unsafe.compareAndSwapInt---> unsafe.compareAndSwapInt--> (C++) cmpxchg
AtomicInteger内部方法都是基于Unsafe类实现的。
Unsafe是CAS的核心类,Java无法直接访问底层操作系统,而是通过native方法来访问。不过尽管如此,JVM还是开了一个后门,JDK中有一个类Unsafe,它提供了硬件级别的原子操作。
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
private volatile int value;static {try {valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));} catch (Exception ex) { throw new Error(ex); }
}public final boolean compareAndSet(int expect, int update) {return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
compareAndSwapInt方法参数:
this:Unsafe对象本身,需要通过这个类来获取value的内存偏移地址;valueOffset:valueOffset表示的是变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的原值的。expect:当前预期的值;update:要设置的新值;
继续向底层深入,就会看到Unsafe类中的一些其他方法:
public final class Unsafe {// ...public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);// ...
}
对应查看openjdk的hotspot源码,src/share/vm/prims/unsafe.cpp。
#define FN_PTR(f) CAST_FROM_FN_PTR(void*, &f){CC"compareAndSwapObject", CC"("OBJ"J"OBJ""OBJ")Z", FN_PTR(Unsafe_CompareAndSwapObject)},{CC"compareAndSwapInt", CC"("OBJ"J""I""I"")Z", FN_PTR(Unsafe_CompareAndSwapInt)},{CC"compareAndSwapLong", CC"("OBJ"J""J""J"")Z", FN_PTR(Unsafe_CompareAndSwapLong)},
最终在hotspot源码实现/src/share/vm/runtime/Atomic.cpp中都会调用统一的cmpxchg函数。
jbyte Atomic::cmpxchg(jbyte exchange_value, volatile jbyte*dest, jbyte compare_value) {assert (sizeof(jbyte) == 1,"assumption.");uintptr_t dest_addr = (uintptr_t) dest;uintptr_t offset = dest_addr % sizeof(jint);volatile jint*dest_int = ( volatile jint*)(dest_addr - offset);// 对象当前值jint cur = *dest_int;// 当前值cur的地址jbyte * cur_as_bytes = (jbyte *) ( & cur);// new_val地址jint new_val = cur;jbyte * new_val_as_bytes = (jbyte *) ( & new_val);// new_val存exchange_value,后面修改则直接从new_val中取值new_val_as_bytes[offset] = exchange_value;// 比较当前值与期望值,如果相同则更新,不同则直接返回while (cur_as_bytes[offset] == compare_value) {// 调用汇编指令cmpxchg执行CAS操作,期望值为cur,更新值为new_valjint res = cmpxchg(new_val, dest_int, cur);if (res == cur) break;cur = res;new_val = cur;new_val_as_bytes[offset] = exchange_value;}// 返回当前值return cur_as_bytes[offset];
}
从上述源码可以看出CAS操作通过CPU提供的原子指令cmpxchg来实现无锁操作,这个指令会保证在多个处理器同时访问和修改数据时的正确性。
CPU处理器速度远远大于在主内存中的速度,为了加快访问速度,现代CPU引入了多级缓存,如L1、L2、L3 级别的缓存,这些缓存离CPU越近就越快。这些缓存存储了频繁使用的数据,但在多处理器环境中,缓存的一致性成为了下一个问题。当CPU中某个处理器对缓存中的共享变量进行了操作后,其他处理器会有个嗅探机制。即将其他处理器共享变量的缓存失效,当其他线程读取时会重新从主内存中读取最新的数据,这是基于MESI缓存一致性协议来实现的。
在多线程环境中,CAS就是比较当前线程工作内存中的值和主内存中的值,如果相同则执行规定操作,否则继续比较,直到主内存和当前线程工作内存中的值一致为止。每个CPU核心都有自己的缓存,用于存储频繁访问的数据。当一个线程在某个CPU核心上修改了共享变量的值时,其他CPU核心上缓存中的该变量会被标记为无效,这样其他线程再访问该变量时就会重新从主内存中获取最新值,从而保证了数据的一致性。CAS操作通过CPU提供的原子指令cmpxchg来比较和交换变量的值,它的原子性和线程安全性依赖于CPU的硬件支持和缓存一致性协议的保障。

所以当执行CAS方法时,读取变量当前的值,并与预期值进行比较。如果变量的当前值等于预期值,则将其更新为新值。如果变量的当前值不等于预期值,则不执行更新操作。注意CAS操作是原子的,即整个过程不会被其他线程打断。
public final int getAndAddInt(Object var1, long var2, int var4) {int var5;do {var5 = this.getIntVolatile(var1, var2);} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));return var5;
}
CAS问题
- 循环时间长开销:CAS操作在失败时会进行自旋重试,即反复尝试CAS操作直到成功或达到一定的重试次数。自旋次数过多可能会影响性能,因此在使用CAS时需要权衡自旋次数和性能之间的关系。例如
getAndAddInt方法执行,如果CAS失败会一直会进行尝试,如果CAS长时间不成功,可能会给CPU带来很大的开销。public final int getAndAddInt(Object var1, long var2, int var4) {int var5;do {var5 = this.getIntVolatile(var1, var2);} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));return var5; } - 原子性问题:CAS操作本身是原子的,即在执行过程中不会被中断。但需要注意的是,CAS操作是针对单个变量的原子操作,而对于判断某个变量的值并根据结果进行另外的操作,需要额外的控制确保整体的原子性。这个时候就可以用锁来保证原子性,但是Java从1.5开始JDK提供了
AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。public class AtomicReferenceSimpleExample {static class DataObject {private int var1;private String var2;public DataObject(int var1, String var2) {this.var1 = var1;this.var2 = var2;}}public static void main(String[] args) {// 创建一个 AtomicReference 实例,并初始化为一个 DataObject 对象AtomicReference<DataObject> atomicRef = new AtomicReference<>(new DataObject(1, "Initial"));// 执行 CAS 操作,修改 DataObject 对象的属性atomicRef.updateAndGet(data -> {data.setVar1(data.getVar1() + 10);data.setVar2("Updated");return data;});// 获取修改后的值DataObject updatedObject = atomicRef.get();System.out.println("Updated var1: " + updatedObject.getVar1());System.out.println("Updated var2: " + updatedObject.getVar2());} } - ABA问题:ABA问题指的是,在CAS操作过程中,如果一个变量的值从A变成了B,然后再变回A,那么CAS操作会错误地认为变量的值未改变过。比如,线程1从内存位置V取出A,线程2同时也从内存取出A,并且线程2进行一些操作将值改为B,然后线程2又将V位置数据改成A,这时候线程1进行CAS操作发现内存中的值依然时A,然后线程1操作成功。尽管线程1的CAS操作成功,但是不代表这个过程没有问题。简而言之就是只比较结果,不比较过程。解决ABA问题的常见方法是使用版本号或者标记来跟踪变量的变化。
public class ABASolutionWithVersion {public static void main(String[] args) {// 初始值为100,初始版本号为0AtomicStampedReference<Integer> atomicRef = new AtomicStampedReference<>(100, 0);int[] stampHolder = new int[1]; // 用于获取当前版本号int expectedValue = 100; // 期望值int newValue = 200; // 新值// 模拟一个线程进行 ABA 操作new Thread(() -> {int stamp = atomicRef.getStamp(); // 获取当前版本号atomicRef.compareAndSet(expectedValue, newValue, stamp, stamp + 1); // 修改值和版本号atomicRef.compareAndSet(newValue, expectedValue, stamp + 1, stamp + 2); // 再次修改回原值和新版本号}).start();// 其他线程进行 CAS 操作new Thread(() -> {int stamp = atomicRef.getStamp(); // 获取当前版本号boolean result = atomicRef.compareAndSet(expectedValue, newValue, stamp, stamp + 1);System.out.println("CAS Result: " + result); // 输出CAS操作结果}).start();} }
相关文章:
CAS详解
文章目录 CAS使用示例Unsafe类实现原理CAS问题 CAS CAS全称为Compare and Swap被译为比较并交换,是一种无锁算法。用于实现并发编程中的原子操作。CAS操作检查某个变量是否与预期的值相同,如果相同则将其更新为新值。CAS操作是原子的,这意味…...
【笔记】虚拟机中的主从数据库连接实体数据库成功后的从数据库不同步问题解决方法2
错误: Last_Errno: 1008 Last_Error: Coordinator stopped because there were error(s) in the worker(s). The most recent failure being: Worker 1 failed executing transaction ANONYMOUS at source log mysql-bin.000014, end_log_pos 200275. See error lo…...
【每日一练】python类和对象现实举例详细讲解
""" 本节课程目的: 1.掌握类描述现实世界实物思想 2.掌握类和对象的关系 3.理解什么事面向对象 """ #比如设计一个闹钟,在这里就新建一个类 class Clock:idNone #闹钟的序列号,也就是类的属性priceNone #闹…...
【学习css1】flex布局-页面footer部分保持在网页底部
中间内容高度不够屏幕高度撑不开的页面时候,页面footer部分都能保持在网页页脚(最底部)的方法 1、首先上图看显示效果 2、奉上源码 2.1、html部分 <body><header>头部</header><main>主区域</main><foot…...
Java中创建线程的几种方式
底层都是基于实现Runnable接口 1.继承thread类,new一个thread对象,实现run方法,无返回值 public class MyThread extends Thread {Overridepublic void run() {System.out.println("Thread created by extending Thread class is runn…...
[A-04] ARMv8/ARMv9-Cache的相关策略
ver0.2 前言 前面我们已经通过三篇文章反反复复的讲Cache的概念、结构、架构,相信大家对Cache已经大概有了初步的了解。这里简单归纳一下: (1) Cache从硬件视角看,是连接PE-Core和主存的一种存储介质,存储的数据是主存中数据的副本…...
【笔试常见编程题06】最近公共祖先、求最大连续bit数、二进制插入、查找组成一个偶数最接近的两个素数
1. 最近公共祖先 将一棵无穷大满二叉树的结点按根结点一层一层地从左往右编号,根结点编号为1。现给定a,b为两个结点。设计一个算法,返回a、b最近的公共祖先的编号。注意其祖先也可能是结点本身。 测试样例: 2,3 返回&a…...
【工具分享】Gophish——网络钓鱼框架
文章目录 Gophish安装方式功能简介 Gophish Gophish 是一个开源的网络钓鱼框架,它被设计用于模拟真实世界的钓鱼攻击,以帮助企业和渗透测试人员测试和评估他们的网络钓鱼风险。Gophish 旨在使行业级的网络钓鱼培训对每个人都是可获取的,它易…...
“职业三大底层逻辑“是啥呢?
大家好,我是有用就扩散。 掌握职业发展的三大底层逻辑以宏观视角看待自己的职业发展道路具备长远规划自己职业路劲的能力通过成就事件呈现自己的工作成绩 一、痛点陈述 不喜欢眼前的工作?眼前的工作琐碎没前途?找不到能力提升的方向时候会…...
飞睿智能无线高速uwb安全数据传输模块,低功耗、抗干扰超宽带uwb芯片传输速度技术新突破
在信息化的时代,数据传输的速度和安全性无疑是每个企业和个人都极为关注的话题。随着科技的飞速发展,超宽带(Ultra-Wideband,简称UWB)技术凭借其性能和广泛的应用前景,逐渐成为了数据传输领域的新星。今天&…...
手把手教你从微信中取出聊天表情图片,以动态表情保存为gif为例
以下方法静态图片同样适用 收到动画表情像保存为gif 这时候我们就要借助微信官方的文件小助手网页版。 登录之后把要保存的表情转发给微信传输助手 这个时候就会出现将图像另存为 如果需要保存动图就修改后缀为.gif...
【深度学习】图形模型基础(5):线性回归模型第三部分:线性回归模型拟合
1.引言 本博文专辑的焦点主要集中在回归模型的实用案例和工具上,从简单的单变量线性回归入手,逐步过渡到包含多个预测变量、非线性模型,以及在预测和因果推断中的应用。本文我们将介绍回归模型推断的一些数学结构,并提供一些代数…...
【Git 入门】初始化配置与新建仓库
文章目录 前言配置git新建仓库仓库的概念创建仓库命令总结前言 在现代软件开发中,版本控制系统已经成为了不可或缺的工具。其中,Git 是最为广泛使用的版本控制系统之一。Git 不仅可以帮助我们管理和跟踪代码的变化,还可以方便地与他人协作。本文将介绍 Git 的基础知识,包括…...
C语言 求两个整数的最大公约数和最小公倍数
写两个函数,分别求两个整数的最大公约数和最小公倍数,用主函数调用这两个函数,并输出结果。两个整数由键盘输入。 #include <stdio.h>// 求最大公约数 int gcd(int a, int b) {while (b ! 0) {int temp b;b a % b;a temp;}return a; }// 求最小公倍数 int lcm(int a,…...
Linux arm64平台指令替换函数 aarch64_insn_patch_text_nosync
文章目录 前言一、简介1.1 aarch64_insn_patch_text_nosync1.2 aarch64_insn_write1.3 patch_map1.4 set_fixmap_offset1.5 __set_fixmap 二、用途2.1 jump lable2.2 ftrace 参考资料 前言 这篇文章介绍了 Linux x86_64平台指令替换函数 text_poke_smp/bp 接下来介绍arm64平台…...
谷歌浏览器插件开发笔记0.1.033
谷歌浏览器插件开发笔记0.1.000 示例文件manifest.jsonpopup.htmloptions.jsoptions.htmlcontent.jsbackground.js 网页按钮快捷键插件api使用基础参考链接 示例文件 共计有6个常用的文件 manifest.json background字段:随着浏览器的打开而打开,随着浏…...
ETag:Springboot接口如何添加Tag
ETag简介 在Web开发中,ETag(Entity Tag)是一种HTTP头字段,用于标识特定版本的资源。ETag的主要用途是缓存控制和优化,通过比较客户端和服务器资源的ETag值,可以判断资源是否发生变化,从而避免不…...
JavaSe系列二十七: Java正则表达式
正则表达式 为什么要学习正则表达式再提几个问题解决之道-正则表达式正则表达式基本介绍介绍 正则表达式底层实现实例分析 正则表达式语法基本介绍元字符-转义号 \\\\元字符-字符匹配符元字符-选择匹配符元字符-限定符元字符-定位符分组非贪婪匹配 应用实例对字符串进行如下验证…...
(深度估计学习)Depth Anything V2 复现
Depth Anything V2 复现 一、配置环境二、准备数据1. 权重文件2. 训练数据 三、Test四、Train 代码:https://github.com/DepthAnything/Depth-Anything-V2 一、配置环境 在本机电脑win跑之后依旧爆显存,放到服务器跑:Ubuntu22.04,…...
C语言——printf、scanf、其他输入输出函数
printf函数 1.printf 函数的一般格式: printf 函数的一般格式为printf(格式控制,输出表列) 例如: printf("%d,%c\n",i,c); (1)“格式控制" 是用双撇号括起来的一个字符串,称“转换控制字符串”,简称“格式字符串”。它包括…...
基于距离变化能量开销动态调整的WSN低功耗拓扑控制开销算法matlab仿真
目录 1.程序功能描述 2.测试软件版本以及运行结果展示 3.核心程序 4.算法仿真参数 5.算法理论概述 6.参考文献 7.完整程序 1.程序功能描述 通过动态调整节点通信的能量开销,平衡网络负载,延长WSN生命周期。具体通过建立基于距离的能量消耗模型&am…...
java 实现excel文件转pdf | 无水印 | 无限制
文章目录 目录 文章目录 前言 1.项目远程仓库配置 2.pom文件引入相关依赖 3.代码破解 二、Excel转PDF 1.代码实现 2.Aspose.License.xml 授权文件 总结 前言 java处理excel转pdf一直没找到什么好用的免费jar包工具,自己手写的难度,恐怕高级程序员花费一年的事件,也…...
【算法训练营Day07】字符串part1
文章目录 反转字符串反转字符串II替换数字 反转字符串 题目链接:344. 反转字符串 双指针法,两个指针的元素直接调转即可 class Solution {public void reverseString(char[] s) {int head 0;int end s.length - 1;while(head < end) {char temp …...
Nginx server_name 配置说明
Nginx 是一个高性能的反向代理和负载均衡服务器,其核心配置之一是 server 块中的 server_name 指令。server_name 决定了 Nginx 如何根据客户端请求的 Host 头匹配对应的虚拟主机(Virtual Host)。 1. 简介 Nginx 使用 server_name 指令来确定…...
python爬虫:Newspaper3k 的详细使用(好用的新闻网站文章抓取和解析的Python库)
更多内容请见: 爬虫和逆向教程-专栏介绍和目录 文章目录 一、Newspaper3k 概述1.1 Newspaper3k 介绍1.2 主要功能1.3 典型应用场景1.4 安装二、基本用法2.2 提取单篇文章的内容2.2 处理多篇文档三、高级选项3.1 自定义配置3.2 分析文章情感四、实战案例4.1 构建新闻摘要聚合器…...
Linux-07 ubuntu 的 chrome 启动不了
文章目录 问题原因解决步骤一、卸载旧版chrome二、重新安装chorme三、启动不了,报错如下四、启动不了,解决如下 总结 问题原因 在应用中可以看到chrome,但是打不开(说明:原来的ubuntu系统出问题了,这个是备用的硬盘&a…...
【数据分析】R版IntelliGenes用于生物标志物发现的可解释机器学习
禁止商业或二改转载,仅供自学使用,侵权必究,如需截取部分内容请后台联系作者! 文章目录 介绍流程步骤1. 输入数据2. 特征选择3. 模型训练4. I-Genes 评分计算5. 输出结果 IntelliGenesR 安装包1. 特征选择2. 模型训练和评估3. I-Genes 评分计…...
【Java学习笔记】BigInteger 和 BigDecimal 类
BigInteger 和 BigDecimal 类 二者共有的常见方法 方法功能add加subtract减multiply乘divide除 注意点:传参类型必须是类对象 一、BigInteger 1. 作用:适合保存比较大的整型数 2. 使用说明 创建BigInteger对象 传入字符串 3. 代码示例 import j…...
2025季度云服务器排行榜
在全球云服务器市场,各厂商的排名和地位并非一成不变,而是由其独特的优势、战略布局和市场适应性共同决定的。以下是根据2025年市场趋势,对主要云服务器厂商在排行榜中占据重要位置的原因和优势进行深度分析: 一、全球“三巨头”…...
安宝特案例丨Vuzix AR智能眼镜集成专业软件,助力卢森堡医院药房转型,赢得辉瑞创新奖
在Vuzix M400 AR智能眼镜的助力下,卢森堡罗伯特舒曼医院(the Robert Schuman Hospitals, HRS)凭借在无菌制剂生产流程中引入增强现实技术(AR)创新项目,荣获了2024年6月7日由卢森堡医院药剂师协会࿰…...
