JavaSim是面向对象的Java离散事件仿真工具包,它是一个原始的C++模拟仿真工具包的Java实现。更多内容请移步至:【javasim】一个java下的建模仿真平台,如果需要,项目文件也可以直接从这里下载。
本文将会研究关于JavaSim的一个仿真实例,即工厂作业加工仿真。首先看一下需要的类。
注意到:在MachineShop、Breaks、Arrivals和Machine中都提供了一个关于run方法的实现。因为它们都继承自SimulationProcess类, 因此,需要提供run方法完成具体的任务。
先看一下Main方法的具体实现。
public static void main (String[] args) { boolean isBreaks = false; for (int i = 0; i < args.length; i++) { if (args[i].equalsIgnoreCase("-help")) { System.out.println("Usage: Main [-breaks] [-help]"); System.exit(0); } if (args[i].equalsIgnoreCase("-breaks")) isBreaks = true; } MachineShop m = new MachineShop(isBreaks); m.await(); System.exit(0); }
可以看到,Main方法的实现相当简单,先是接受命令行的参数,判断是否breaks,如果true,意味着考虑机器故障的情况,否则,不考虑机器故障的情况。接着,创建了一个MachineShop对象,同时将是否breaks传入。再然后,调用MachineShop的await方法。
先看一下MachineShop的await方法的具体实现。
public void await () { this.resumeProcess();//恢复线程运行,第一次调用,防止线程阻塞 SimulationProcess.mainSuspend();//阻塞主进程 }
根据之前的介绍,SimulationProcess类继承了Thread类,resumeProcess的主要作用是恢复线程的运行。SimulationProcess.mainSuspend方法的主要作用是阻塞主进程,使主进程进入等待状态。
SimulationProcess对象的resumeProcess方法恢复一个特定的线程运行。如下所示,该方法通过一个信号量mutex,控制线程同步,通过notify方法,唤醒一个阻塞的SimulationProcess线程运行。其中,count为正时,代表当前可用的资源个数;否则,其绝对值表示阻塞的进程的个数。
/** * Resume the specified process. This can only be called on a process which * has previously been Suspend-ed or has just been created, i.e., the * currently active process will never have Resume called on it. */ protected void resumeProcess () { /* * To compensate for the initial call to Resume by the application. */ if (SimulationProcess.Current == null) { SimulationProcess.Current = this; wakeuptime = SimulationProcess.currentTime(); } if (!terminated) { if (!started) { started = true; start();//开始执行当前线程,Java虚拟机将会调用该线程类的run方法 } else { synchronized (mutex) { count++; if (count >= 0) mutex.notify();//唤醒一个等待的线程,调用wait方法的线程 } } } }
注意到,在调用await方法之前,没有使用Simulation.Start方法启动模拟,所以,此时(第一次调用)会执行start()方法,开辟新的线程, 同时执行当前对象的run方法。
public void run () { try { Breaks B = null; Arrivals A = new Arrivals(8);//作业的到达服从均值为8的指数分布 MachineShop.M = new Machine(8); Job J = new Job();//创建作业,并将作业加入到工厂的加工队列中,同时判断队列是否为空,机器是否空闲,机器是否正常,若是,则将工厂任务添加到调度器Scheduler A.activate();//向队列中加入当前仿真对象Arrival if (useBreaks)//判断是否考虑机器故障 { B = new Breaks(); B.activate(); } Simulation.start();//仿真开始 while (MachineShop.ProcessedJobs < 1000)//加工不少于1000件产品 hold(1000); //输出结果 System.out.println("Current time "+currentTime()); System.out.println("Total number of jobs present " + TotalJobs); System.out.println("Total number of jobs processed " + ProcessedJobs); System.out.println("Total response time of " + TotalResponseTime); System.out.println("Average response time = " + (TotalResponseTime / ProcessedJobs)); System.out .println("Probability that machine is working = " + ((MachineActiveTime - MachineFailedTime) / currentTime())); System.out.println("Probability that machine has failed = " + (MachineFailedTime / MachineActiveTime)); System.out.println("Average number of jobs present = " + (JobsInQueue / CheckFreq)); Simulation.stop(); A.terminate(); MachineShop.M.terminate();//终止 if (useBreaks) B.terminate(); SimulationProcess.mainResume();//恢复主进程 } catch (SimulationException e) { } catch (RestartException e) { } }
可以看出,上述代码的主要工作是创建了Arrival、Breaks、Machine和Job对象,同时,激活Arrival对象和Breaks对象,即将Arrival对象和Breaks对象加入到调度队列Scheduler Queue中。然后,通过Simulation.Start方法启动仿真过程。接着是循环地检查已经处理的作业数量是否超过1000。若是,则将结果打印出来。最后,停止模拟,调用Arrival、Breaks和Machine的terminate方法结束各个子进程的运行。
SimulationProcess 对象的 hold 方法将会调用activateDelay方法将当前进程放入到调度队列Scheduler Queue中,并且在t时刻之后激活,即阻塞当前线程一段时间。这里,需要注意的是,位于调度队列Scheduler Queue中的任务是按照计划开始时间递增的顺序排列,即队头是计划开始时间最早的调度。这里,hold方法可以简单地理解为线程切换与线程阻塞。
/** * Hold the current process for the specified amount of simulation time. */ protected void hold (double t) throws SimulationException, RestartException { if ((this == SimulationProcess.Current) || (SimulationProcess.Current == null)) { wakeuptime = SimulationProcess.NEVER; activateDelay(t, false); suspendProcess(); } else throw new SimulationException("Hold applied to inactive object."); }
SimulationProcess对象的activateDelay的详细实现如下。可以看出,其主要工作是将当前任务插入到调度队列Scheduler Queue中,将当前进程的计划唤醒时间设置为延迟时间加上当前模拟时钟时间,即完成切换和阻塞前的准备工作。
/** * This process will be activated after 'Delay' units of simulation time. * This process must not be running, or on the scheduler queue. 'Delay' must * be greater than, or equal to, zero. If 'prior' is true then this process * will appear in the simulation queue before any other process with the * same simulation time. * * @param Delay the time by which to delay this process. * @param prior indicates whether or not to schedule this process occurs before any other process with the same time. * @throws SimulationException thrown if there's an error. * @throws RestartException thrown if the simulation is restarted. */ public void activateDelay (double Delay, boolean prior) throws SimulationException, RestartException { if (terminated || !idle()) return; if (!checkTime(Delay)) throw new SimulationException("Invalid delay time " + Delay); passivated = false; wakeuptime = Scheduler.getSimulationTime() + Delay; Scheduler.getQueue().insert(this, prior); }
SimulationProcess对象的suspendProcess方法所做的任务是首先进行调度,即从Scheduler Queue中取下一个任务。接着,判断调度是否成功。若调度成功,则进行等待;若调度失败,则不进行任何操作。其主要功能是实现线程的切换与阻塞。
/** * Suspend the process. If it is not running, then this routine should not * be called. */ protected void suspendProcess () throws RestartException { try { if (Scheduler.schedule()) { synchronized (mutex) { count--; if (count == 0) { try { mutex.wait();//阻塞进程 } catch (Exception e) { } } } } } catch (SimulationException e) { } if (Simulation.isReset()) throw new RestartException(); }
其中,Scheduler 的 schedule 方法实现如下所示。 首先,取调度队列Scheduler Queue的队头元素,接着,运行其resumeProcess方法,恢复一个特定的线程运行。返回true,代表调度成功。在进行调度之前首先调用Simulation.isStarted方法检查模拟是否开始。若没有开始,将会抛出异常。如下所示,schedule方法将会把当前运行的任务保存到Simulation.Current静态变量中。这里,可以将schedule方法理解为线程切换。
/** * It is possible that the currently active process may remove itself * from the simulation queue. In which case we don't want to suspend the * process since it needs to continue to run. The return value indicates * whether or not to call suspend on the currently active process. */ static synchronized boolean schedule () throws SimulationException { if (Simulation.isStarted()) { SimulationProcess p = SimulationProcess.current(); try { SimulationProcess.Current = Scheduler.ReadyQueue.remove();//当前运行的任务在SimulationProcess的静态变量Current中保存 } catch (NoSuchElementException e) { System.out.println("Simulation queue empty."); } if (SimulationProcess.Current.evtime() < 0) throw new SimulationException("Invalid SimulationProcess wakeup time.");//判断计划开始时间是否合理 else Scheduler.SimulatedTime = SimulationProcess.Current.evtime();//设置模拟的仿真时间为计划开始时间 if (p != SimulationProcess.Current) { SimulationProcess.Current.resumeProcess();//恢复运行线程 return true; } else return false; } else throw new SimulationException("Simulation not started."); }
线程阻塞:获取调度队列Scheduler Queue,并向其插入当前的线程。下面是SimulationProcess类中的activateDelay方法的内容展示。
线程切换:从调度队列Scheduler Queue中删除队头元素,并放到SimulationProcess.Current静态属性中。下面是Scheduler类中的schedule方法的内容展示。
线程阻塞后,当前线程被插入到调度队列Scheduler Queue中。
下面看一下Machine的run方法的具体实现。while循环判断当前进程是否结束。若结束,则不再执行while循环,并退出run方法,Machine进程结束。首先,设置忙碌标志位为true。接着,循环判断作业队列Job Queue是否为空。若队列非空,计算作业批次和作业总数量,随机等待指数分布的时间,模拟作业的加工。将当前的作业加入到调度队列Scheduler Queue中。接着,计算开机时间,调用Job的finished方法,计算响应时间。设置忙碌标志位为空闲。最后,取消当前任务。
public void run () { double ActiveStart, ActiveEnd; while (!terminated()) { working = true;//忙碌标志位 while (!MachineShop.JobQ.isEmpty()) { ActiveStart = currentTime(); MachineShop.CheckFreq++;//作业批次 MachineShop.JobsInQueue += MachineShop.JobQ.queueSize(); J = MachineShop.JobQ.dequeue(); try { hold(serviceTime());//随机加工指数分布的时间 } catch (SimulationException e) { } catch (RestartException e) { } ActiveEnd = currentTime(); MachineShop.MachineActiveTime += ActiveEnd - ActiveStart;//计算开机时间 MachineShop.ProcessedJobs++;//处理的作业个数加1 /* * Introduce this new method because we usually rely upon the * destructor of the object to do the work in C++. */ J.finished();//作业处理完成,计算响应时间 } working = false; try { cancel(); } catch (RestartException e) { } } }
其中,SimulationProcess 对象的 cancel 方法取消任务的实现具体如下所示。若当前任务在调度队列Scheduler Queue中,则将其从调度队列中删除,否则,将其直接阻塞,即设置当前对象的wakeuptime为SimulationProcess.NEVER。
/** * Cancels next burst of activity, process becomes idle. * * @throws RestartException thrown if the simulation is restarted. */ public void cancel () throws RestartException { /* * We must suspend this process either by removing it from the scheduler * queue (if it is already suspended) or by calling suspend directly. */ if (!idle()) // process is running or on queue to be run { // currently active, so simply suspend if (this == SimulationProcess.Current) { wakeuptime = SimulationProcess.NEVER; passivated = true; suspendProcess(); } else { Scheduler.unschedule(this); // remove from queue } } }
根据cancel方法的调用方式不同,可以分为:主动调用和被动调用。如果是当前运行的线程是Machine对象自身,那么就称其为主动调用,否则,称为被动调用。
调用方式 | 说明 |
主动调用 | 使得当前线程失活,并且唤醒时间设置为SimulationProcess.NEVER,进入被动passive状态,同时阻塞当前线程() |
被动调用 | 将当前进程(非Machine对象)从调度队列Scheduler Queue中删除,同时使其失活,进入被动passive状态 |
线程一旦进入被动模式,除非另一个进程将它带回到队列中,否则它将不会执行任何进一步的操作。
另外,unschedule() 方法和失活方法deactivate() 如下所示。
static synchronized void unschedule (SimulationProcess p) { try { Scheduler.ReadyQueue.remove(p); // remove from queue } catch (NoSuchElementException e) { } p.deactivate(); }
void deactivate () { passivated = true; wakeuptime = SimulationProcess.NEVER; }
Arrivals对象也继承自SimulationProcess类,为了模拟作业的随机产生,采用一个均值为8的指数分布的随机数生成器ExponentialStream对象。每次调用hold方法,将当前对象插入到调度队列Scheduler Queue中,并随机等待一段时间。最后,创建一个作业。
public void run () { while (!terminated()) { //输出当前线程的信息 System.out.println(currentTime() + " " + Thread.currentThread().getName() + ": Arrival"); try { hold(InterArrivalTime.getNumber()); } catch (SimulationException e) { } catch (RestartException e) { } catch (IOException e) { } new Job(); } }
Job的构造函数如下所示。其中,将作业的到达时间记录下来,判断作业队列是否为空,若作业队列为空,则代表机器对象已经执行结束或者机器中途发生了故障,需要重新将Machine对象加入到调度队列Scheduler Queue中。接着,将当前作业加入到作业队列中,同时,作业计数加1。
public Job() { boolean empty = false; ResponseTime = 0.0; ArrivalTime = Scheduler.currentTime();//作业到达时间 empty = MachineShop.JobQ.isEmpty(); MachineShop.JobQ.enqueue(this);//将当前作业加入到队列中 MachineShop.TotalJobs++;//总作业加1 if (empty && !MachineShop.M.processing() && MachineShop.M.isOperational())//若队列为空,同时机器空闲,同时机器正常 { try { MachineShop.M.activate();//将工厂任务添加到调度器Scheduler } catch (SimulationException e) { } catch (RestartException e) { } } }
Breaks对象的run方法如下所示。为了便于调试,笔者输出了当前线程信息。首先,为了模拟机器坏掉,先将当前的线程放入到调度队列Scheduler Queue中,并阻塞一段时间。接着,通知机器坏掉了,并且将其从调度队列Scheduler Queue中删除。然后,判断当前作业队列中是否还有作业,若有,则设置标志interrupt_service。为了模拟机器维修过程,又将当前线程放入到调度队列Scheduler Queue中,并阻塞一段时间。最后,通知维修完成。若interrupt_service为true,则将剩余作业处理完成后,激活线程,否则,直接激活线程。
public void run () { while (!terminated()) { //输出当前线程的信息 System.out.println(currentTime() + " " + Thread.currentThread().getName() + ": Breaks"); try { double failedTime = RepairTime.getNumber(); hold(OperativeTime.getNumber()); MachineShop.M.broken();//通知机器坏掉了 MachineShop.M.cancel();//将其从队列中删除 if (!MachineShop.JobQ.isEmpty())//若当前作业队列中含有其他的作业,则将设置标志,说明正在维修 interrupted_service = true; hold(failedTime); MachineShop.MachineFailedTime += failedTime; MachineShop.M.fixed();//维修完成 if (interrupted_service) MachineShop.M.activateAt(MachineShop.M.serviceTime() + currentTime());//中断服务,代表还有作业需要处理,就先处理机器坏掉之前的作业,再激活 else MachineShop.M.activate(); interrupted_service = false; } catch (SimulationException e) { } catch (RestartException e) { } catch (IOException e) { } } }
注意到上述实现中包含下述的代码,用于模拟机器突然发生故障。此种情况为被动调用cancel方法,因此,将会直接将其从调度队列Scheduler Queue中删除。
MachineShop.M.cancel();//将其从队列中删除