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

volatile 详解

目录

一. 前言

二. 可见性

2.1. 可见性概述

2.2. 内存屏障

2.3. 代码实例

三. 不保证原子性

3.1. 原子性概述

3.2. 如何解决 volatile 的原子性问题呢?

四. 禁止指令重排

4.1. volatile 的 happens-before 关系

4.2. 代码实例

五. volatile 应用场景

5.1. 状态标志

5.2. 一次性安全发布(one-time safe publication)

5.3. 独立观察(independent observation)

5.4. volatile bean 模式

5.5. 开销较低的读-写锁策略

5.6. 双重检查(double-checked)


一. 前言

    volatile 可以看做是轻量级的 synchronized,它只保证了共享变量的可见性,是Java虚拟机提供的轻量级的同步机制。在线程 A 修改被 volatile 修饰的共享变量之后,线程 B 能够读取到正确的值。Java 在多线程中操作共享变量的过程中,会存在指令重排序与共享变量工作内存缓存的问题。volatile 一共有三大特性,保证可见性不保证原子性禁止指令重排

二. 可见性

2.1. 可见性概述

    首先提一个JMM的概念,JMM是Java内存模型,它描述的是一组规范或规则,通过这组规范定义了程序中各个变量(实例字段,静态字段和构成数组对象的元素)的访问方式。JMM规定所有的变量都在主内存,主内存是公共的,所有线程都可以访问,线程对变量的操作必须是在自己的工作内存中。在这个过程中可能出现一个问题。

现在假设主物理内存中存在一个变量,他的值为7,现在线程A和线程B要操作这个变量,所以他们首先要将这个变量的值拷贝一份放到自己的工作内存中,如果A将这个值改为1,这时候线程B要使用这个变量但是B线程工作内存中的变量副本是7 不是新修改的1 这就会出现问题。

所以JMM规定线程解锁前一定要将自己工作内存的变量写回主物理内存中,线程加锁前一定要读取主物理内存的值。也就是说一旦线程A修改了变量值,线程B马上能知道并且能更新自己工作内存的值。这就是可见性。 

2.2. 内存屏障

volatile 变量的内存可见性是基于内存屏障(Memory Barrier)实现。

内存屏障又称内存栅栏,是一个 CPU 指令。在程序运行时,为了提高执行性能,编译器和处理器会对指令进行重排序,JMM 为了保证在不同的编译器和 CPU 上有相同的结果,通过插入特定类型的内存屏障来禁止 + 特定类型的编译器重排序和处理器重排序,插入一条内存屏障会告诉编译器和 CPU:不管什么指令都不能和这条 Memory Barrier 指令重排序。

2.3. 代码实例

设计思路:首先,我们新建一个类,里面有一个 number,然后写一个方法可以让他的值变成60,这时候在主线程中开启一个新的线程让他 3s 后将number值改为60,然后主线程写一个循环如果主线程能立刻监听到number值的改变则主线程输出改变后的值,此时说明有可见性。如果一直死循环,说明主线程没有监听到number值的更改,说明不具有可见性。

class MyData {public int number = 0;public void change() {number = 60;}
}public class VolatileTest {public static void main(String[] args) {MyData myData = new MyData();new Thread(()->{System.out.println(Thread.currentThread().getName() + "number is :"+ myData.number);try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}myData.change();System.out.println(Thread.currentThread().getName() + "has changed :" + myData.number);}, "A").start();while(myData.number == 0){}System.out.println(Thread.currentThread().getName() + "number is:" + myData.number);}
}

看一下结果:

结果是进入了死循环一直空转,说明不具有可见性。下面我们在number前面加上关键字volatile。 public volatile int number = 0;

证明能监控到number值已经修改,说明加上volatile具有可见性。

三. 不保证原子性

3.1. 原子性概述

    原子性指的是不可分割,完整性,也即某个线程正在做某个业务时不能被分割,要么同时成功,要么同时失败。

    为了证明 volatile 能不能保证原子性,我们可以通过一个案例来证明一下。首先我们在之前的MyData 类中加入一个方法 addplus() 能让number加1,然后我们创建20个线程,每个线程调用1000次 addplus()。看看结果,如果number是20000,那么他就能保证原子性,如果不是20000,那么就不能保证原子性。

class MyData{public static volatile int number = 0;public void change(){number = 60;}public void incre(){number++;}
}public class VolatileDemo {public static void main(String[] args) {MyData data = new MyData();for (int i = 0; i < 20; i++) {new Thread(()-> {for (int j = 0; j < 1000; j++) {data.incre();}}, String.valueOf(i)).start();}while(Thread.activeCount() > 2) {Thread.yield();}System.out.println("number is: " + MyData.number);}
}

结果不是20000,说明不能保证原子性。 

不保证原子性的原因:number++ 这个操作一共有3步,第一步从主物理内存中拿到number的值,第二步 number + 1,第三步写回主物理内存。

假设一开始主物理内存的值为0,线程A、线程B分别读取主物理内存的值到自己的工作内存,然后执行加1操作。这时候按理说线程A 将1写回主物理内存,然后线程B 读取主物理内存的值然后加1变成2,但是在线程A写回的过程中突然被打断线程A挂起,线程B 将1写回主物理内存这时候线程A重新将1写回主物理内存最终主物理内存的值为1,两个线程加了两次最后值居然是1,出错了。 

3.2. 如何解决 volatile 的原子性问题呢?

    我们需要使用原子类,原子类是保证原子性的。加入一个 AtomicInteger 类的成员,然后调用他的getAndIncrement() 方法(就是把这个数加1,底层用CAS保证原子性)。原子类的具体讲解请参见《JUC之Atomic原子类》。

运行结果:

这就解决了不保证原子性的问题。 

四. 禁止指令重排

    禁止指令重排又叫保证有序性。计算机编译器在执行代码的时候不一定非得按照你写代码的顺序执行。他会经历编译器优化的重排,指令并行的重排,内存系统的重排,最终才会执行指令,多线程环境更是如此,可能每个线程代码的执行顺序都不一样,这就是指令重排。

4.1. volatile 的 happens-before 关系

happens-before 规则中有一条是 volatile 变量规则:对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。

// 假设线程A执行writer方法,线程B执行reader方法
class VolatileExample {int a = 0;volatile boolean flag = false;public void writer() {a = 1;              // 1 线程A修改共享变量flag = true;        // 2 线程A写volatile变量} public void reader() {if (flag) {         // 3 线程B读同一个volatile变量int i = a;          // 4 线程B读共享变量……}}
}

根据 happens-before 规则,上面过程会建立 3 类 happens-before 关系。
1. 根据程序次序规则:1 happens-before 2 且 3 happens-before 4。
2. 根据 volatile 规则:2 happens-before 3。
3. 根据 happens-before 的传递性规则:1 happens-before 4。

因为以上规则,当线程 A 将 volatile 变量 flag 更改为 true 后,线程 B 能够迅速感知。 

4.2. 代码实例

public class VolatileReSort {int a = 0;boolean flag = false;public void methodA() { // 线程Aa = 1;flag = true;}public void methodB() { // 线程Bif(flag) {a = a + 5;System.out.println("**********retValue:" + a);}}
}

假设现在线程A、线程B 分别执行上面两个方法,由于指令的重排序,可能线程A中的两条语句发生了指令重排,flag先变为true,这时候线程B突然进来判断flag为true,然后执行下面的最后输出结果为a = 5,但是也有可能先执行a = 1,那这样结果就是a = 6,所以,由于指令重排可能导致结果出现多种情况。现在加上volatile关键字,他会在指令间插入一条Memory Barrier,来保证指令按照顺序执行不被重排。

五. volatile 应用场景

5.1. 状态标志

    也许实现 volatile 变量的规范使用仅仅是使用一个布尔状态标志,用于指示发生了一个重要的一次性事件,例如完成初始化或请求停机。

volatile boolean shutdownRequested;
......
public void shutdown() { shutdownRequested = true; }public void doWork() { while (!shutdownRequested) { // do stuff}
}

5.2. 一次性安全发布(one-time safe publication)

    缺乏同步会导致无法实现可见性,这使得确定何时写入对象引用而不是原始值变得更加困难。在缺乏同步的情况下,可能会遇到某个对象引用的更新值(由另一个线程写入)和该对象状态的旧值同时存在。这就是造成著名的双重检查锁定【double-checked-locking】问题的根源,其中对象引用在没有同步的情况下进行读操作,产生的问题是您可能会看到一个更新的引用,但是仍然会通过该引用看到不完全构造的对象。

public class BackgroundFloobleLoader {public volatile Flooble theFlooble;public void initInBackground() {// do lots of stufftheFlooble = new Flooble();  // this is the only write to theFlooble}
}public class SomeOtherClass {public void doWork() {while (true) { // do some stuff...// use the Flooble, but only if it is readyif (floobleLoader.theFlooble != null) doSomething(floobleLoader.theFlooble);}}
}

5.3. 独立观察(independent observation)

    安全使用 volatile 的另一种简单模式是定期发布观察结果供程序内部使用。例如,假设有一种环境传感器能够感觉环境温度。一个后台线程可能会每隔几秒读取一次该传感器,并更新包含当前文档的 volatile 变量。然后,其他线程可以读取这个变量,从而随时能够看到最新的温度值。

public class UserManager {public volatile String lastUser;public boolean authenticate(String user, String password) {boolean valid = passwordIsValid(user, password);if (valid) {User u = new User();activeUsers.add(u);lastUser = user;}return valid;}
}

5.4. volatile bean 模式

在 volatile bean 模式中,JavaBean 的所有数据成员都是 volatile 类型的,并且 getter 和 setter 方法必须非常普通 —— 除了获取或设置相应的属性外,不能包含任何逻辑。此外,对于对象引用的数据成员,引用的对象必须是有效不可变的。(这将禁止具有数组值的属性,因为当数组引用被声明为 volatile 时,只有引用而不是数组本身具有 volatile 语义)。对于任何 volatile 变量,不变式或约束都不能包含 JavaBean 属性。

@ThreadSafe
public class Person {private volatile String firstName;private volatile String lastName;private volatile int age;public String getFirstName() { return firstName; }public String getLastName() { return lastName; }public int getAge() { return age; }public void setFirstName(String firstName) { this.firstName = firstName;}public void setLastName(String lastName) { this.lastName = lastName;}public void setAge(int age) { this.age = age;}
}

5.5. 开销较低的读-写锁策略

    volatile 的功能还不足以实现计数器。因为 ++x 实际上是三种操作(读、添加、存储)的简单组合,如果多个线程凑巧试图同时对 volatile 计数器执行增量操作,那么它的更新值有可能会丢失。 如果读操作远远超过写操作,可以结合使用内部锁和 volatile 变量来减少公共代码路径的开销。 安全的计数器使用 synchronized 确保增量操作是原子的,并使用 volatile 保证当前结果的可见性。如果更新不频繁的话,该方法可实现更好的性能,因为读路径的开销仅仅涉及 volatile 读操作,这通常要优于一个无竞争的锁获取的开销。

@ThreadSafe
public class CheesyCounter {// Employs the cheap read-write lock trick// All mutative operations MUST be done with the 'this' lock held@GuardedBy("this") private volatile int value;public int getValue() { return value; }public synchronized int increment() {return value++;}
}

5.6. 双重检查(double-checked)

传统的单例模式,在单线程下其实是没有什么问题的,多线程条件下就不行了。

public class SingletonDemo {private static SingletonDemo instance = null;private SingletonDemo() {System.out.println(Thread.currentThread().getName() + " :构造方法被执行");}public static SingletonDemo getInstance() {if(instance == null) {instance = new SingletonDemo();}return instance;}public static void main(String[] args) {for(int i = 0; i < 10; i++) {new Thread(() -> {SingletonDemo.getInstance();}, String.valueOf(i)).start();}}
}

可以看出多线程下单例模式将会失效。我们通过DCL双重检查锁可以解决上述问题。 

public static SingletonDemo getInstance() {if(instance == null) {synchronized (SingletonDemo.class) {if(instance == null) {instance = new SingletonDemo();}}}return instance;
}

但是这种方式也有一定的风险。原因在于某一个线程执行到第一次检测,读取到的 instance 不为null 时,instance的引用对象可能没有完成初始化。instance = new SingletonDemo(); 可以分为以下3步完成:1. 分配对象内存空间;2. 初始化对象;3. 设置instance指向刚分配的内存地址,此时instance!=null。但是由于编译器优化可能会对2、3两步进行指令重排也就是先设置instance指向刚分配的内存地址,但是这时候对象还没有初始化,如果这时候新来的线程调用了这个方法就会发现instance != null 然后就返回 instance,实际上 instance 没被初始化,也就造成了线程安全的问题。为了避免这个问题我们可以使用 volatile 对其进行优化,禁止他的指令重排就不会发生上述问题了,给变量加上 volatile,如 private static volatile SingletonDemo instance = null;。 

相关文章:

volatile 详解

目录 一. 前言 二. 可见性 2.1. 可见性概述 2.2. 内存屏障 2.3. 代码实例 三. 不保证原子性 3.1. 原子性概述 3.2. 如何解决 volatile 的原子性问题呢&#xff1f; 四. 禁止指令重排 4.1. volatile 的 happens-before 关系 4.2. 代码实例 五. volatile 应用场景 5…...

Flink Operator 使用指南 之 Flink Operator安装

介绍 Flink Kubernetes Operator 充当控制平面来管理 Apache Flink 应用程序的完整部署生命周期。尽管 Flink 的Native Kubernetes 集成已经允许用户在运行的 Kubernetes(k8s) 集群上直接部署 Flink 应用程序,但自定义资源和Operator Pattern 也已成为 Kubernetes 原生部署体…...

类与对象(上篇)

前言 在之前我们学的C入门主要是为现在学习类与对象打基础&#xff0c;今天我们才算真正开始学习C了。因为类与对象的知识点比较多&#xff0c;所以我们将它分为三部分讲解&#xff0c;今天我们学习类与对象的上篇。 一、面向过程和面向对象的初步认识 1、面向过程 面向过程顾…...

使用SpringBoot集成MyBatis对管理员的查询操作

增删改查中的查询操作&#xff0c;对所有的普通管理员进行查询操作。 效果展示&#xff1a; 不仅可以在打开页面时进行对管理员的自动查询操作&#xff0c;还可以在输入框进行查询。 首先是前端向后端发送POST请求&#xff0c;后端接收到请求&#xff0c;如果是有参数传到后端…...

数据报文去哪儿了

背景 今天遇到一个诡异的现象&#xff0c;当接口附加一个IP时&#xff0c;主IP业务正常&#xff0c;附加IP死活不行&#xff0c;tcpdump抓包确可以正常抓到到业务的报文&#xff0c;但是在PREROUTING raw添加规则确没有命中&#xff0c;说明报文没有到netfilter框架内&#xff…...

Mysql中join on中的like使用

1、使用mysql中的函数CONCAT(str1,str2,…) 返回结果为连接参数产生的字符串。如有任何一个参数为NULL &#xff0c;则返回值为 NULL。 SELECT * FROM Table1 INNER JOIN Table2 ON Table1.col LIKE CONCAT(%, Table2.col, %) 2、放弃使用join语句 SELECT * FROM Table1, T…...

微信运营神器:从群发到批量添加,让你的微信营销更轻松

在这个数字化时代&#xff0c;微信已经成为了我们生活中不可或缺的一部分。对于许多企业和个人来说&#xff0c;微信营销也是非常重要的一部分。但是&#xff0c;微信营销并不是一件容易的事情&#xff0c;需要花费大量的时间和精力。为了解决这个问题&#xff0c;今天我们将向…...

白杨SEO:2B企业营销是什么?当下主流的短视频直播平台有哪些?企业营销要做短视频直播选哪个平台更好?

今天白杨SEO就正式来讲讲2B企业营销选择哪个短视频直播平台更好&#xff1f; 图片在公众号&#xff1a;白杨SEO上看。 文章大纲提前看&#xff1a; 1、先说说2B企业营销是什么&#xff1f; 2、当下主流的短视频直播平台有哪些&#xff1f; 3、2B企业营销要做短视频直播选哪…...

将word中的表格无变形的弄进excel中

在上篇文章中记录了将excel表拷贝到word中来&#xff1a; 记录将excel表无变形的弄进word里面来-CSDN博客 本篇记录&#xff1a;将word中的表格无变形的弄进excel中。 1.按F12&#xff0c;“另存为...”&#xff0c;保存类型&#xff1a;“单个文件页面”&#xff0c;保存。…...

美国服务器在大陆连不上怎么回事?

​  在租用任何美国服务器之前&#xff0c;都需要先搞清楚一些使用问题&#xff0c;毕竟服务器能够不间断地访问也是站在们所期望的。但有时&#xff0c;美国服务器网站或许也会突然出现在大陆打不开的情况&#xff0c;在面临这种情况时&#xff0c;我们应该怎么做? 查看连不…...

postgresql数据库中update使用的坑

简介 在数据库中进行增删改查比较常见&#xff0c;经常会用到update的使用。但是在近期发现update在oracle和postgresql使用却有一些隐形区别&#xff0c;oracle 在执行update语句的时候set 后面必须跟着1对1的数据关联而postgresql数据库却可以一对多&#xff0c;这就导致数据…...

高可用elasticsearch集群搭建

目录 一、环境准备 二、机器配置 2.1 创建用户 2.2 修改用户权限 2.3 解析主机名 2.4 优化最大文件数 2.5 优化最大进程数 2.6 优化虚拟内存 2.7 重载配置 三、部署 3.1 创建文件夹并赋予权限 3.2 解压安装包并赋予权限 3.3 配置环境变量 3.4 创建数据、证书存放目录并赋…...

Linux本地MinIO存储服务远程调用上传文件

&#x1f525;博客主页&#xff1a; 小羊失眠啦. &#x1f3a5;系列专栏&#xff1a;《C语言》 《数据结构》 《Linux》《Cpolar》 ❤️感谢大家点赞&#x1f44d;收藏⭐评论✍️ 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;…...

C语言 子函数调malloc申请内存返回给主函数使用——可行,但要注意

一般情况&#xff0c;子函数中动态申请内存&#xff0c;将地址返回给主函数&#xff0c;理论上应该也是可以的&#xff0c;需要子函数返回动态内存地址&#xff0c;主函数实参是相应的地址变量即可。只不过&#xff0c;主函数实参传入子函数之前&#xff0c;可能会将指针置空&a…...

Python入门教程之条件语句与运算符优先级详解

文章目录 Python 条件语句Python运算符优先级关于Python技术储备一、Python所有方向的学习路线二、Python基础学习视频三、精品Python学习书籍四、Python工具包项目源码合集①Python工具包②Python实战案例③Python小游戏源码五、面试资料六、Python兼职渠道 Python 条件语句 …...

高通Camera HAL3: CamX、Chi-CDK要点

目录 一、概述 二、目录 三、CamX组件之前的关系 一、概述 高通CamX架构是高通实现的相机HAL3架构&#xff0c;被各OEM厂商广泛采用。 二、目录 代码位于vendor/qcom/proprietary下&#xff1a; camx&#xff1a;通用功能性接口的代码实现集合chi-cdk&#xff1a;可定制化…...

springboot+vue热带野生动物园景点预约门票订票系统

热带野生动物园景点预约订票系统为野生动物园提供景点管理服务的系统&#xff0c;通过登录系统&#xff0c;管理该野生动物园所有的景点信息、景点分类信息、野生动物园新闻、通知公告、回复会员留言等&#xff0c;并可以通过订单管理查看会员预定的订单信息&#xff0c;对订单…...

Flutter和Android的混合跳转

1、项目特点 项目是Flutter作为主工程&#xff0c;将Android module或SDK作为模块嵌入到flutter中&#xff0c;与通常所熟悉的Android&#xff08;或iOS&#xff09;工程将flutter 为module嵌入到工程中有所不同。 2、业务需求 任意界面间的跳转&#xff0c;不管是flutter页…...

CyberRT-共享内存实现

CyberRT共享内存类图 共享内存消息发布 数据用共享内存发布时&#xff0c;首先会创建ShmTransmitter对象&#xff0c;包含两个主要成员segment和notifier&#xff0c;Segment用于创建共享内存&#xff08;上面绿色部分&#xff09;&#xff0c;Notifer 最终构建ReadableInfo通…...

linux通过串口传输文件

简介 在嵌入式调试过程中&#xff0c;我们经常会使用调试串口来查看Log或者执行指令&#xff0c;其实&#xff0c;调试串口还有另一种功能&#xff0c;就是传输文件&#xff0c;本文说明使用MobaXterm串口工具来传输文件。 环境要求 嵌入式系统需要安装lsz和lrz&#xff0c;…...

PHP 8.9大文件处理性能跃迁实录(87%内存降低+4.2倍吞吐提升):Fiber协程+Chunked Transfer全链路解析

更多请点击&#xff1a; https://intelliparadigm.com 第一章&#xff1a;PHP 8.9大文件分块处理性能跃迁全景概览 PHP 8.9 并非官方发布版本&#xff08;截至 2024 年&#xff0c;PHP 最新稳定版为 8.3&#xff09;&#xff0c;但作为技术前瞻推演场景&#xff0c;本章基于 P…...

Python 为什么这么慢?真凶不只是 GIL

Python 为什么这么慢&#xff1f;真凶不只是 GIL 你在网上可能听过无数遍&#xff1a;“Python 慢是因为有 GIL&#xff08;全局解释器锁&#xff09;。” 于是你心安理得地想&#xff1a;哦&#xff0c;那等到多解释器出来&#xff0c;或者我用多进程&#xff0c;它就该快起来…...

猫抓Cat-Catch:重新定义浏览器资源获取的智能捕手

猫抓Cat-Catch&#xff1a;重新定义浏览器资源获取的智能捕手 【免费下载链接】cat-catch 猫抓 浏览器资源嗅探扩展 / cat-catch Browser Resource Sniffing Extension 项目地址: https://gitcode.com/GitHub_Trending/ca/cat-catch 你是否曾遇到过这样的场景&#xff1…...

CompactGUI:Windows透明压缩工具的开源贡献完全指南

CompactGUI&#xff1a;Windows透明压缩工具的开源贡献完全指南 【免费下载链接】CompactGUI Reduce the space taken up by games and programs on disk by using native Windows APIs 项目地址: https://gitcode.com/gh_mirrors/co/CompactGUI 在数字内容日益膨胀的今…...

JDK7之前hashmap链表采用头插法为什么会导致循环链表?

hashmap发生哈希碰撞之后形成的链表&#xff0c;在早的jdk版本会采用头插法的方法&#xff0c;也就是新插入的值&#xff0c;作为链表的头部。这种方法在单线程的情况下没有什么问题&#xff0c;这里扩容的时候要说一点&#xff0c;当扩容的时候会创建一个新哈希表&#xff0c;…...

告别死记硬背:用一张图+三个实战案例搞定RocketMQ核心机制

图解RocketMQ&#xff1a;三场景实战拆解消息队列核心机制 消息队列技术早已成为分布式系统的标配基础设施&#xff0c;但真正掌握其精髓的开发者却不多。很多人在学习RocketMQ时陷入概念迷宫&#xff1a;Producer、Broker、Consumer、NameServer之间的关系像一团乱麻&#xff…...

如何用RS ASIO技术彻底解决《摇滚史密斯2014》的音频延迟问题:完整低延迟配置终极指南

如何用RS ASIO技术彻底解决《摇滚史密斯2014》的音频延迟问题&#xff1a;完整低延迟配置终极指南 【免费下载链接】rs_asio ASIO for Rocksmith 2014 项目地址: https://gitcode.com/gh_mirrors/rs/rs_asio 音频延迟是《摇滚史密斯2014》玩家面临的核心技术瓶颈&#x…...

如何用Python工具完整备份QQ空间青春记忆:GetQzonehistory终极指南

如何用Python工具完整备份QQ空间青春记忆&#xff1a;GetQzonehistory终极指南 【免费下载链接】GetQzonehistory 获取QQ空间发布的历史说说 项目地址: https://gitcode.com/GitHub_Trending/ge/GetQzonehistory 你是否担心QQ空间里那些珍贵的青春记忆会随着时间流逝而消…...

Windows热键侦探:精准定位快捷键冲突的终极方案

Windows热键侦探&#xff1a;精准定位快捷键冲突的终极方案 【免费下载链接】hotkey-detective A small program for investigating stolen key combinations under Windows 7 and later. 项目地址: https://gitcode.com/gh_mirrors/ho/hotkey-detective 你是否曾经遇到…...

3步释放C盘空间:FreeMove让Windows目录迁移变得安全又简单

3步释放C盘空间&#xff1a;FreeMove让Windows目录迁移变得安全又简单 【免费下载链接】FreeMove Move directories without breaking shortcuts or installations 项目地址: https://gitcode.com/gh_mirrors/fr/FreeMove 你是否曾经因为C盘空间不足而苦恼&#xff1f;那…...