# scau-os-design-Lumxi **Repository Path**: lumxi/scau-os-design-lumxi ## Basic Information - **Project Name**: scau-os-design-Lumxi - **Description**: 华南农业大学操作系统小课设 - **Primary Language**: Java - **License**: MulanPSL-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2022-05-23 - **Last Updated**: 2025-06-24 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 基于JAVA-FX的进程的同步与互斥模拟程序 ## 1. 题目需求 ### 1.1 哲学家就餐问题简述 一张圆桌上有一碗面和`5个盘子`,每位哲学家一个盘子,还有`5把叉子`。哲学家使用盘子`两侧的叉子`就餐。 请你设计一套算法,算法必须保证`互斥(两位哲学家不能同时使用同一把叉子)`,同时还要避免死锁和饥饿。 ### 1.2 哲学家就餐问题实现提示 首先设置一个“PCB”数组或队列,其中一个字段表示“阻塞原因兼阻塞标志”, 本实验中,该数组有 5 个元素表示 5 个哲学家即可。它们随机提出申请以及进行 “思考"、“吃”的行为。再设一个“筷子”数组。还需要设置哪些数据结构以及需要哪些字段自己考虑。 示,例图如下,仅供参考。 ![图片](.\doc\程序示意图.png) ### 1.3 程序实现要求 - 要求后端与前端尽量分离,也即后端模拟哲学家就餐过程的随机结果,将结果发送到前端,前端将结果显示给用户 - 要求实现的前端交互性强,能够将结果以可视化的结果呈现给用户 ## 2. 需求解决原理 ### 2.1 哲学家就餐-将发生死锁情形 ```C++ void phi(int i){//i代表的是当前是第几位哲学家线程在运行,i从0开始 think(); wait(fork[i]); wait(fork[(i+1)%5]); //等资源获得完毕后开始就餐 eat(); //释放资源的顺序可以任意 singnal(fork[(i+1)%5]); singnal(fork[i]); } /*当两个哲学家都拿起两把左叉,发生死锁*/ ``` ### 2.2 哲学家就餐-使用信号量 ```C++ /*最多允许四位哲学家拿起左叉*/ semphore room = 4; semphore fork[5] ={1};//一开始所有资源都是正常的 void phi(int i){ think(); wait(room);//是否得到允许拿左叉,不允许则阻塞 wait(fork[i]); wait(fork[(i+1)%5]); eat(); singnal(fork[(i+1)%5]); singnal(fork[i]); signal(room); } ``` ### 2.3 哲学家就餐-对应号数取餐叉 ```c++ /*奇数号的人先拿左叉,偶数号的人先拿右叉*/ void phi(int i){ think(); if(i%2==1){//奇数号人 wait(fork[i]); wait(fork[(i+1)%5]); }else{ wait(fork[(i+1)%5]); wait(fork[i]); } eat(); singal(fork[i]); singal(fork[(i+1)%5]); } ``` ### 2.4 JAVA多线程开发基础 #### 2.4.1 Semaphore JAVA提供了Semaphore信号量数据结构供开发者进行使用 - `Semaphore`(信号量)是用来控制同时访问特定资源的线程数量,通过协调各个线程以保证合理地使用公共资源。 Semaphore通过使用计数器来控制对共享资源的访问。 `如果计数器大于0,则允许访问`。 `如果为0,则拒绝访问`。 计数器所计数的是允许访问共享资源的许可。 因此,要访问资源,必须从信号量中授予线程许可。 - 提供的API如下: - `void acquire()` :从信号量获取一个许可,如果无可用许可前将一直阻塞等待。也就相当于wait。 - `void acquire(int permits)` :获取指定数目的许可,如果无可用许可前也将会一直阻塞等待。相当于要获取一定量的资源才不会阻塞 - `boolean tryAcquire()`:从信号量尝试获取一个许可,如果无可用许可,直接返回false,不会阻塞。 - `boolean tryAcquire(int permits)`: 尝试获取指定数目的许可,如果无可用许可直接返回false - `boolean tryAcquire(int permits, long timeout, TimeUnit unit)`: 在指定的时间内尝试从信号量中获取许可,如果在指定的时间内获取成功,返回true,否则返回false - `void release()`:释放一个许可,别忘了在finally中使用,注意:多次调用该方法,会使信号量的许可数增加,达到动态扩展的效果,如:初始permits为1,调用了两次release,最大许可会改变为2 - `int availablePermits()`: 获取当前信号量可用的许可 #### 2.4.2 synchronized `synchronized`是java中用于`同步与互斥`的关键字,在处理并发问题的时候,该关键字可以原子化地修饰某些成分: - 代码块 - 方法 - 静态方法 - 类 在并发编程中,对于临界资源如果使用了synchronized进行修饰,那么当一个进程占用该资源时,其他进程申请时将被阻塞。 ## 3. 环境搭建 ### 3.1 项目环境 `jdk`:open_jdk16 `FX版本`:11 `IDEA版本`:2021.3.2 ### 3.1 构建项目 ![图片](.\doc\构建.png) 这里我们选择一个普通的JAVAFX项目构建即可,构建方法的还是Maven ### 3.2 配置lib ![图片](./doc/配置lib.png) 将下载好的JAVAFX加到库环境中即可 ### 3.3 配置虚拟机启动变量 ![图片](./doc/配置虚拟机环境.png) 修改这一行为--module-path "H:\JAVA\FX\javafx-sdk-11.0.2\lib" -add-modules javafx.controls,javafx.fxml 注:"H:\JAVA\FX\javafx-sdk-11.0.2\lib"为你本地的JAVAFX所在lib ### 3.4 测试运行 ![图片](./doc/运行测试.png) 这样就配置成功了,就可以开始写代码了 ## 4. 难点讲解 ### 4.1 前端编写 对于算法,是比较容易实现的,但是如何来编写前端是一个比较难想的模块,我们从手头上有的东西出发,首先我们有一个叉子数组,叉子的Sem变量能够帮助我们得知该叉子是否被占用,有一个哲学家数组,该数组能够帮助我们得知每一个哲学家的状态。 于是我们编写前端的思路就是:`定期检查运行模型的状态,获取运行模型的状态,将运行模型的状态以图片的形式表现出来即可` - 定期检查的时间:这个时间考虑到我们执行一次thinking,eating等的时间都是3000ms,因此我们每3000ms采样一次会得到比较好的动画效果 - 动画显示的思路:首先`viewer`对象拿到phis数组和forks数组,检查里面的情况,然后对imageView的visable属性进行修改,对于不同的哲学家状态,可以通过重新载入图片的方式来进行。 ### 4.2 JAVAFX中使用多线程 在JAVAFX中,存在一个UI线程用来直接控制当前显示界面的变化,它是单线程的,如果我们需要在用户自己新建的线程中更新UI,**则需要将所有更新UI的代码通过函数式编程置于一种`UI安全函数`中**,在JAVAFX中就是`platform.runLater` - UI界面的更新是以`异步`的方式进行的,UI线程首先会执行用户代码,然后如果这些代码使得UI界面的数据发生改变,UI线程将委托操作系统对其UI界面进行更新 - UI 线程是单线程的,指的 UI 界面是只通过一个线程来完成它界面的更新,指的不是凡是涉及 UI 的程序只能使用一个线程。UI 应用相比于后台应用,只是多了几个与处理 UI 相关的线程而已,没什么额外的线程个数限制。 - 为防止 UI 界面被阻塞,又因为 UI 线程是单线程的,因此应该选择在其它线程执行非常耗时的操作。可以选择当需要执行非常耗时的操作时,新开一个线程,将此非常耗时的操作放到新开一个线程去执行。 - 在 JavaFX 中使用多线程一般使用两个类:`ExecutorService`、`Task`。`Task` 有一个方法 `call`,可以在这个方法去执行耗时操作。操作示例: ```java ExecutorService executor = Executors.newCachedThreadPool(); Task task = new Task<>() { @Override protected Integer call() { // TODO 执行耗时操作 return null; // 如果需要结果反馈,可以在此处提供反馈值 } }; /** * 如果不需要结果反馈,也可以直接使用 executor.execute(task); * * 可以使用 result.get() 来获取上面的反馈值。但这个方法是同步阻塞的 */ ``` ### 4.3 JAVAFX中保证线程安全 - 在 JavaFX 中,可以在 UI 之外的线程中,使用方法 `Platform.runLater` 来执行与 UI 直接相关的操作。如下: ```java Platform.runLater(() -> {/* // TODO 更新 UI 数据的代码 */}); ``` - 注意:为保证 UI 界面的流畅,只需将与 UI 直接相关的代码置入上述的方法 `Platform.runLater` 中,**不要在此方法中放多余的代码**,否则就失去了使用多线程的意义 ### 4.4 关于暂停动画功能的编写 - 暂停,首先在主观上就是画面的更新UI效果被暂停了,同时我们希望达到以下两点 - 画面UI停止更新,同时后端执行线程的数据更新也暂停,我采用的办法是在程序中加入一个标志位`isPaused`,当`isPaused`为true的时,线程就阻塞了`while(isPaused)`,以一段代码示例: ```java vector.addElement("我是第"+(this.getPhiId()+1)+"号哲学家,我正在思考"); this.thinking(); while(isPaused){this.waitSelf();} vector.addElement("我是第 "+(this.getPhiId()+1)+"号哲学家,我在等待叉子"); this.waiting(); while(isPaused){this.waitSelf();} forks[this.getPhiId()].getLock().acquire(); this.getLeftFork(this.getPhiId()); while(isPaused){this.waitSelf();} forks[(this.getPhiId()+1)%FORK_NUMBER].getLock().acquire(); this.getRightFork((this.getPhiId()+1)%FORK_NUMBER); while(isPaused){this.waitSelf();} vector.addElement("我是第"+(this.getPhiId()+1)+"号哲学家,我得到了左右的叉子,正在就餐"); this.eating(); while(isPaused){this.waitSelf();} forks[this.getPhiId()].getLock().release(); this.releaseLeftFork(); while(isPaused){this.waitSelf();} forks[(this.getPhiId()+1)%FORK_NUMBER].getLock().release(); this.releaseRightFork(); while(isPaused){this.waitSelf();} vector.addElement("我是第"+(this.getPhiId()+1)+"号哲学家,我吃完了,正在放下叉子"); this.finish(); ``` ​ 这方法的弊端在于,暂停的时候无法完美衔接暂停前的状态,比如说我现在一个线程sleep了1000ms,这时候我按了暂停,暂停的时候不会再这个时刻产生断点,而是sleep完剩下的2000ms后再暂停。(能力有限,希望有更好方案的同学指教!) - 暂停后,点击继续后,能够恢复之前执行的状态,调用`notify()`即可 ### 4.5 关于返回主菜单功能的编写 - 需要关闭当前所有线程,使用`interrupt()`进行线程的关闭,需要明确的一点的是:interrupt() 方法并不像在 for 循环语句中使用 break 语句那样干脆,马上就停止循环。调用 `interrupt()` 方法仅仅是在当前线程中打一个停止的标记,并不是真的停止线程。 ```java public boolean Thread.isInterrupted() //判断是否被中断 public static boolean Thread.interrupted() //判断是否被中断,并清除当前中断状态 ``` - 当前界面关闭,主菜单界面显示出来 ## 5. 总结