本文将会研究关于JavaSim的一个仿真实例,即工厂作业加工仿真。首先看一下需要的类。
注意到:在MachineShop、Breaks、Arrivals和Machine中都提供了一个关于run方法的实现。因为它们都继承自SimulationProcess类, 因此,需要提供run方法完成具体的任务。
1. Main
先看一下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方法。
2. MachineShop
先看一下MachineShop的await方法的具体实现。
public void await ()
{
this.resumeProcess();//恢复线程运行,第一次调用,防止线程阻塞
SimulationProcess.mainSuspend();//阻塞主进程
}
根据之前的介绍,SimulationProcess类继承了Thread类,resumeProcess的主要作用是恢复线程的运行。SimulationProcess.mainSuspend方法的主要作用是阻塞主进程,使主进程进入等待状态。
SimulationProcess对象的resumeProcess方法
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方法。
MachineShop的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 方法
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 方法
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 方法
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中。
3. Machine
下面看一下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;
}
4. Arrivals
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();
}
}
5.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)
{
}
}
}
6.Breaks
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();//将其从队列中删除