
本文旨在探讨Java中如何在不同运行类之间安全有效地共享和更新变量值,特别是在需要实时监控操作进度的场景。我们将通过三种核心策略——观察者模式(推模型)、轮询模式(拉模型)以及基于多线程的共享状态管理——来详细阐述如何实现类间的通信与数据同步,并提供相应的代码示例和最佳实践建议。
在Java应用程序开发中,不同类之间的数据交互是常见的需求。尤其是在执行耗时操作(如文件拷贝、网络下载)时,我们通常需要在一个类中执行任务,而在另一个类中实时显示或监控任务的进度。直接通过静态变量进行访问虽然可行,但在多线程或复杂场景下可能导致难以维护和调试的问题。本教程将深入探讨几种更健壮、更专业的解决方案。
1. 任务进度监控的挑战
设想一个场景:CopyFile 类负责大文件的分块拷贝,它会不断更新已拷贝的数据量。而 ProgressMonitor 类则需要在用户界面或控制台实时显示这个进度。核心挑战在于:
- CopyFile 如何将更新后的进度值通知给 ProgressMonitor?
- ProgressMonitor 如何获取到 CopyFile 的最新进度?
- 如何在不紧密耦合两个类的情况下实现这种通信?
- 如果 CopyFile 和 ProgressMonitor 运行在不同的线程中,如何确保数据同步和线程安全?
下面我们将介绍三种主流的实现策略。
立即学习“Java免费学习笔记(深入)”;
2. 策略一:观察者模式(推模型)
观察者模式是一种行为设计模式,它定义了对象之间的一对多依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都会得到通知并自动更新。在这种模式下,执行任务的类(Copy)是“主题”(Subject),负责监控进度的类(Observer)是“观察者”(Observer)。
核心思想: Copy 类持有 Observer 类的实例,并在每次进度更新时主动调用 Observer 的方法来推送最新进度。
示例代码:
// Test.java - 主程序入口
public class Test {
public static void main(String[] args) {
// 创建观察者实例
Observer observer = new Observer();
// 创建拷贝任务实例,并将观察者注入
Copy copy = new Copy(1000, observer);
// 启动拷贝任务
copy.start();
}
}
// Observer.java - 进度观察者类
public class Observer {
/**
* 接收并显示进度更新
* @param current 当前已完成的块数
* @param total 总块数
*/
public void updateProgress(int current, int total) {
System.out.println("当前进度: " + current + "/" + total);
}
}
// Copy.java - 文件拷贝任务类
public class Copy {
public final int totalBlocks; // 总块数
private Observer observer; // 观察者实例
/**
* 构造函数,注入观察者
* @param totalBlocks 文件总块数
* @param observer 进度观察者
*/
public Copy(int totalBlocks, Observer observer) {
this.totalBlocks = totalBlocks;
this.observer = observer;
}
/**
* 启动文件拷贝模拟过程
*/
public void start() {
for (int current = 1; current <= totalBlocks; current++) {
// 模拟耗时操作
try {
Thread.sleep(10); // 每次拷贝一小块数据
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println("拷贝过程被中断。");
return;
}
// 每次完成一个块,就通知观察者更新进度
observer.updateProgress(current, totalBlocks);
}
System.out.println("文件拷贝完成!");
}
}优点:
- 实时性强: 进度更新后立即通知观察者。
- 职责分离: Copy 专注于拷贝逻辑,Observer 专注于显示逻辑。
- 低耦合: Copy 只需知道 Observer 有一个 updateProgress 方法,不需要了解其内部实现。
3. 策略二:轮询模式(拉模型)
轮询模式与观察者模式相反,它不依赖于主动通知。在这种模式下,负责监控进度的类(Observer)会周期性地主动向执行任务的类(Copy)查询当前进度。
核心思想: Observer 类持有 Copy 类的实例,并在一个循环中不断调用 Copy 的方法来获取最新进度。
示例代码:
// Test.java - 主程序入口
public class Test {
public static void main(String[] args) {
// 创建拷贝任务实例
Copy copy = new Copy(1000);
// 创建观察者实例,并将拷贝任务注入
Observer observer = new Observer(copy);
// 启动观察者,它将开始轮询进度
observer.start();
}
}
// Observer.java - 进度观察者类
public class Observer {
private Copy copy; // 拷贝任务实例
/**
* 构造函数,注入拷贝任务
* @param copy 拷贝任务实例
*/
public Observer(Copy copy) {
this.copy = copy;
}
/**
* 启动进度轮询
*/
public void start() {
System.out.println("开始监控文件拷贝进度...");
while (copy.hasNextBlock()) { // 只要还有未完成的块
// 模拟轮询间隔
try {
Thread.sleep(100); // 每100毫秒查询一次进度
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println("进度监控被中断。");
return;
}
// 获取并显示当前进度
System.out.println("当前进度: " + copy.getCurrentBlock() + "/" + copy.totalBlocks);
}
System.out.println("文件拷贝完成!(通过轮询确认)");
}
}
// Copy.java - 文件拷贝任务类
public class Copy {
public final int totalBlocks; // 总块数
private int currentBlock = 0; // 当前已完成的块数
/**
* 构造函数
* @param totalBlocks 文件总块数
*/
public Copy(int totalBlocks) {
this.totalBlocks = totalBlocks;
// 在后台启动一个线程来模拟拷贝过程
new Thread(() -> {
for (int i = 1; i <= totalBlocks; i++) {
try {
Thread.sleep(20); // 模拟每次拷贝一小块数据的时间
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println("拷贝线程被中断。");
return;
}
currentBlock = i; // 更新进度
}
}).start();
}
/**
* 判断是否还有未完成的块
* @return 如果还有未完成的块,返回 true
*/
public boolean hasNextBlock() {
return currentBlock < totalBlocks;
}
/**
* 获取当前已完成的块数
* @return 当前已完成的块数
*/
public int getCurrentBlock() {
return currentBlock;
}
}优点:
- 控制权在观察者: 观察者可以控制查询的频率。
- 实现相对简单: 对于简单的状态共享,不需要复杂的通知机制。
缺点:
- 实时性稍差: 进度更新可能存在延迟,取决于轮询间隔。
- 资源消耗: 频繁轮询可能造成不必要的CPU开销,尤其是在进度更新不频繁时。
4. 策略三:多线程共享状态与同步
当 Copy 和 Observer 运行在不同的线程中时,直接访问共享变量需要考虑线程安全问题。此策略结合了轮询的思想,但更强调了多线程环境下的数据同步。
核心思想: Copy 类在一个单独的线程中执行任务并更新一个共享变量,Observer 类在另一个线程中周期性地读取这个共享变量。为了确保数据可见性和一致性,需要使用 volatile 关键字或更高级的同步机制。
示例代码:
import java.util.concurrent.atomic.AtomicInteger;
// Test.java - 主程序入口
public class Test {
public static void main(String[] args) {
// 创建共享进度对象
SharedProgress progress = new SharedProgress(1000);
// 创建并启动拷贝线程
Thread copyThread = new Thread(new CopyTask(progress));
copyThread.setName("CopyThread");
copyThread.start();
// 创建并启动观察者线程
Thread observerThread = new Thread(new ProgressMonitor(progress));
observerThread.setName("ObserverThread");
observerThread.start();
// 等待拷贝线程完成
try {
copyThread.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("主线程:拷贝任务已完成。");
}
}
// SharedProgress.java - 共享进度数据类
class SharedProgress {
private final int totalBlocks;
// 使用 AtomicInteger 保证原子性操作,或使用 volatile int currentBlock;
// volatile 保证可见性,但不能保证复合操作的原子性
private volatile int currentBlock = 0;
public SharedProgress(int totalBlocks) {
this.totalBlocks = totalBlocks;
}
public int getTotalBlocks() {
return totalBlocks;
}
public int getCurrentBlock() {
return currentBlock;
}
public void incrementProgress() {
// 对于简单的自增操作,volatile 配合适当的逻辑可以工作
// 但如果需要更复杂的原子操作,AtomicInteger 更安全
currentBlock++;
}
public boolean isCompleted() {
return currentBlock >= totalBlocks;
}
}
// CopyTask.java - 拷贝任务,运行在单独线程中
class CopyTask implements Runnable {
private SharedProgress progress;
public CopyTask(SharedProgress progress) {
this.progress = progress;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ": 拷贝任务开始。");
for (int i = 0; i < progress.getTotalBlocks(); i++) {
try {
Thread.sleep(15); // 模拟拷贝每一块数据的时间
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println(Thread.currentThread().getName() + ": 拷贝任务被中断。");
return;
}
progress.incrementProgress(); // 更新共享进度
}
System.out.println(Thread.currentThread().getName() + ": 拷贝任务完成。");
}
}
// ProgressMonitor.java - 进度监控器,运行在单独线程中
class ProgressMonitor implements Runnable {
private SharedProgress progress;
public ProgressMonitor(SharedProgress progress) {
this.progress = progress;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ": 进度监控开始。");
while (!progress.isCompleted()) {
try {
Thread.sleep(80); // 每隔一段时间轮询进度
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println(Thread.currentThread().getName() + ": 进度监控被中断。");
return;
}
System.out.println(Thread.currentThread().getName() + ": 进度 " + progress.getCurrentBlock() + "/" + progress.getTotalBlocks());
}
System.out.println(Thread.currentThread().getName() + ": 进度监控结束,任务已完成。");
}
}注意事项:
- volatile 关键字: 确保 currentBlock 变量的可见性。当一个线程修改了 currentBlock 的值时,其他线程能立即看到最新的值,而不是其本地缓存的旧值。
- 原子操作: 如果 incrementProgress() 方法不仅仅是简单的 currentBlock++,而是包含多个操作(例如 currentBlock = currentBlock + 1),那么 volatile 无法保证其原子性。在这种情况下,应使用 java.util.concurrent.atomic 包中的类(如 AtomicInteger)或 synchronized 关键字来保护共享变量的访问。在上述示例中,currentBlock++ 在JVM层面通常是原子操作,但为了严谨和更复杂的场景,AtomicInteger 是更安全的做法。
- 线程管理: 使用 Thread 类或 ExecutorService 来管理线程生命周期。
- 优雅退出: 线程中断机制 (Thread.interrupt()) 是停止线程的推荐方式,而不是 Thread.stop()。
5. 总结与最佳实践
选择哪种策略取决于具体的应用场景和需求:
- 观察者模式(推模型): 适用于需要实时、即时通知的场景,且生产者(任务执行者)希望主动通知消费者(进度显示者)的情况。它提供了良好的解耦。
- 轮询模式(拉模型): 适用于对实时性要求不高,或者消费者希望控制获取频率的场景。实现相对简单,但可能存在延迟和资源浪费。
- 多线程共享状态: 当任务和监控器必须运行在不同线程时,这是必然的选择。核心是正确处理线程安全和数据可见性,通常需要 volatile、synchronized 或 java.util.concurrent.atomic 包中的工具。
通用建议:
- 解耦: 尽量保持类之间的低耦合,一个类不应过度依赖另一个类的内部实现细节。接口(interface)是实现解耦的有力工具。
- 明确职责: 每个类应有清晰单一的职责。
- 错误处理: 考虑任务中断、异常等情况,并进行适当处理。
- 并发考量: 在多线程环境中,始终优先考虑线程安全,使用Java提供的并发工具和关键字。
通过以上三种策略,开发者可以根据项目的具体需求,灵活选择最适合的方式来实现在Java中不同运行类之间安全、高效地共享变量和更新进度。










