引言:JavaSim简介
JavaSim是面向对象的Java离散事件仿真工具包。它是一个原始的C++模拟仿真工具包的Java实现,仿真模型分为三类,以系统状态随时间变化的方式描述:
连续时间:状态随时间连续变化的系统,通常用一组微分方程来描述。
离散时间:仅在选定的时间点考虑系统。一些经济学模型就是这样的例子,经济学数据是以固定的时间间隔提供的。只有在观察点才注意到状态的变化。通过在观测点之间选择适当的小间隔,可以用离散时间模拟来近似连续时间模拟。
连续时间离散事件:时间参数是连续的,观察期是一个真正的间隔,为了简单起见,通常从零开始。操作路径完全由事件时间序列和在这些时间发生的系统状态离散变化决定。在连续事件时间之间,系统状态可能连续变化。
1. org.javasim
本节描述了Javasim仿真系统的核心元素。注意,本节中描述的所有类都可以在org.javasim包中找到。
1.1 The simulation scheduler
在Javasim中,模拟进程 simulation processes 由调度程序 Scheduler 管理,并放置在调度程序队列 Scheduler Queue(事件列表)中。进程以伪并行方式执行,即只有一个进程在任何实时实例上执行,但许多进程可以在任何模拟时间实例上并发执行。只有对模拟时间的当前实例执行了所有流程后,模拟时钟才会提前。非活动进程被放置到调度程序队列中,当当前活动进程向调度程序生成控制时(因为它已完成或已放回调度程序队列),调度程序将删除队列头的进程并重新激活它。当调度程序队列为空时,即没有其他要执行的进程,调度程序将终止模拟。
如上图所示,调度程序 Scheduler 协调整个模拟运行,有效地监视主动和被动进程,以使其能够确定何时以及哪一个进程激活下一个进程。模拟应用程序不能直接影响调度程序,但只能通过修改调度程序队列间接影响调度程序。注意,一般情况下,Scheduler Queue按照计划开始时间递增的顺序排列,即队头是计划开始时间最早的调度。
/**
* Activate this process at the specified simulation time. This process must
* not be running, or on the scheduler queue. 'AtTime' must be greater than,
* or equal to, the current simulation time. If 'prior' is true then this
* process will appear in the simulation queue before any other process with
* the same simulation time.
*
* @param AtTime the time to activate the 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 activateAt (double AtTime, boolean prior)
throws SimulationException, RestartException
{
if (terminated || !idle())
return;
if (AtTime < SimulationProcess.currentTime())//时间不合法
throw new SimulationException("Invalid time " + AtTime);
passivated = false;
wakeuptime = AtTime;
Scheduler.getQueue().insert(this, prior);
}
public synchronized void insert (SimulationProcess p, boolean prior)
{
// If list is empty, insert at head
if (Head == null)
{
Head = new SimulationProcessCons(p, null);
return;
}
// Try to insert before (if there is anything scheduled later)
SimulationProcessIterator iter = new SimulationProcessIterator(this);
SimulationProcess prev = null;
for (SimulationProcess q = iter.get(); q != null; prev = q, q = iter
.get())
{
if (prior)
{
if (q.evtime() >= p.evtime())//队列按照计划开始时间排序
{
insertBefore(p, q);
return;
}
}
else
{
if (q.evtime() > p.evtime())
{
insertBefore(p, q);
return;
}
}
}
// Got to insert at the end (currently pointed at by 'prev')
insertAfter(p, prev);
}
调度程序队列 Scheduler Queue 可以以多种方式进行构造,包括线性列表或树。队列的实现可以取决于正在进行的模拟的类型。例如,涉及多个并发进程的模拟会遇到使用线性有序队列的问题,该队列通常具有插入和删除例程,开销与队列中的条目数成比例。上述代码展示的是线性列表的方式构造调度队列。然而,线性列表可能最适合于少量的模拟过程。Javasim附带了一套调度程序队列实现,在构建系统时可以选择这些实现。有关线性列表部分的内容,将会在第四节详细介绍。
模拟调度程序是调度程序类 Scheduler 的一个实例,同时该实例采用单例模式创建。调度程序类维护模拟时钟,通过调用 currentTime()方法获取该时钟的当前值。
public class Scheduler extends Thread
{
public static double currentTime ();
}
1.2 同步仿真类
1.2.1 Simulation
要使多个 Simulation (模拟) 运行在一个应用程序中,可以通过调用 Simulation 类的 reset() 方法来重置模拟时钟。这将导致 Simulation 删除当前在调度程序队列中注册的所有进程(Simulation 对象),并对每个进程调用一个特定于类的方法,该方法将重置它们的状态。完成后,Simulation 就可以进行额外的运行了。一个挂起的进程被通知它已经被“重置”,它调用的方法最初挂起它自己,将它自己放在调度程序队列上,同时,引发RestartSimulation异常,对象应该捕获该异常。然后,它必须执行任何必要的工作,以使自己恢复到准备重新启动的Simulation 的状态,然后应在重新启动 Simulation 之前再次挂起自己。进程可以使用 isReset() 来确定 Simulation 是否已重置。start() 和 stop() 操作允许分别停止或恢复 Simulation 。
public class Simulation
{
public static synchronized void reset () throws SimulationException;
public static synchronized boolean isReset ();
public static synchronized void stop ();
public static synchronized void start ();
}
1.2.2 SimulationProcesses
Javasim 还支持面向过程的模拟方法,在这种方法中,每个仿真实体都可以被视为一个单独的流程。因此,在Javasim中,仿真中的实体由流程对象表示。这些Java对象在创建时具有与它们相关联的独立控制线程,允许它们传达参与仿真所必需的活动的概念。在模拟时间的任何点上,流程必须处于以几种状态之一:
状态 | 说明 |
active | 进程已从调度程序队列的头中删除,正在执行其操作。 |
suspended | 进程位于调度程序队列上,计划在指定的模拟时间变为活动状态。 |
passive (被动) | 进程不在调度程序队列中。除非另一个进程将它带回到队列中,否则它将不会执行任何进一步的操作。 |
terminated | 进程不在调度程序队列中,没有要执行的进一步操作。一旦一个进程终止,就不能让它在同一个模拟运行中进一步执行。一个活动的或挂起的进程被称为已调度。 |
SimulationProcesses 类的定义如下所示。由于构造函数受到保护,因此无法创建SimulationProcess类的实例,即必须从此类派生类。进程是线程对象,通常每个线程包都根据优先级调度线程的执行。默认情况下,javasim中的所有进程都以相同的优先级创建,但可以通过调用java.lang.thread的setpriority方法来更改。
public class SimulationProcess extends Thread
{
public final double time ();
public synchronized SimulationProcess nextEv () throws SimulationException, NoSuchElementException;
public final double evtime ();
public void activateBefore (SimulationProcess p) throws SimulationException, RestartException;
public void activateAfter (SimulationProcess p) throws SimulationException, RestartException;
public void activateAt (double AtTime, boolean prior) throws SimulationException, RestartException;
public void activateAt (double AtTime) throws SimulationException, RestartException;
public void activateDelay (double Delay, boolean prior) throws SimulationException, RestartException;
public void activate () throws SimulationException, RestartException;
public void reactivateBefore (SimulationProcess p) throws SimulationException, RestartException;
public void reactivateAfter (SimulationProcess p) throws SimulationException, RestartException;
public void reactivateAt (double AtTime, boolean prior) throws SimulationException, RestartException;
public void reactivateAt (double AtTime) throws SimulationException, RestartException;
public void reactivateDelay (double Delay, boolean prior) throws SimulationException, RestartException;
public void reactivateDelay (double Delay) throws SimulationException, RestartException;
public void reactivate () throws SimulationException, RestartException;
public void cancel () throws RestartException;
public void terminate ();
public synchronized boolean idle ();
public boolean passivated ();
public boolean terminated ();
public static SimulationProcess current () throws SimulationException;
public static double currentTime ();
public static void mainSuspend ();
public static void mainResume () throws SimulationException;
protected void hold (double t) throws SimulationException, RestartException;
protected void passivate () throws RestartException;
protected void setEvtime (double time) throws SimulationException;
protected void suspendProcess () throws RestartException;
protected void resumeProcess ();
}
有五种方法可以激活当前的被动进程,从而使其在调度程序队列中对应于其相关模拟时间的正确位置。如果这是队列的头,那么它将成为活动进程。
方法 | 说明 |
activate() | 这将在当前模拟时间激活进程。 |
activatebefore(simulation process proc) | 这将把进程定位在调度程序队列中的proc之前,并给它相同的模拟时间。如果proc不存在,则将引发模拟异常。 |
activateafter(SimulationProcess Proc) | 这将把进程定位在调度程序队列中,并给它相同的模拟时间。如果proc不存在,则将引发模拟异常。 |
activateat(double at time,boolean prior) | 将进程插入调度程序队列中对应于at time指定的模拟时间的位置。此时间的默认值是当前模拟时间。previous参数用于确定是否应在队列中已经存在的具有相同模拟时间的任何进程之前或之后插入此进程。默认值为假。 |
activatedelay(double attime,boolean prior) | 在指定的延迟(attime)之后激活进程。在新的模拟时间内,将进程插入到队列中,并使用previous参数确定其相对于队列中其他进程的顺序。默认值为假。 |
相应地,有五种重新激活的方法,可以在被动过程或预定过程中工作。如果在对象不是当前活动进程时由该对象调用(例如,通过公共可用的方法),那么它什么也不做。
函数 | 说明 |
evtime() | 返回进程计划激活的时间。 |
nextev() | 返回对计划执行的下一个进程的引用。如果队列为空,则返回空值。 |
current() | 为静态方法,它返回对当前活动进程的引用。 |
currentTime() | 获取当前模拟时间。静态方法,可以在没有SimulationProcess类实例的情况下调用。 |
time() | 获取当前模拟时间。 |
cancel() | 从调度程序队列中删除进程,或者如果该进程是当前活动的进程,则将其挂起。在这两种情况下,进程都被设置为被动状态。 |
passivate() | 和cancel() 函数类似,仅在当前活动的进程上工作,即,如果对象(例如,通过公共可用方法)在它不是当前活动的进程时调用它,那么它什么也不做。 |
terminate() | 从调度程序队列中删除进程,或者如果进程当前处于活动状态,则将其挂起。然后将进程设置为终止状态,并且不能在模拟运行中进一步参与。 |
idle() | 判断进程是否处于活动状态或计划变为活动状态 |
因为SimulationProcess扩展了java.lang.thread类,所以Simulation类需要提供一个run方法的实现,该方法将完成流程的实际工作。如果此方法返回,则线程将被销毁。但是,为了让javasim检测线程的终止,必须使用其中的 terminate() 方法。
示例
为了说明如何从 SimulationProcess 类实现模拟过程,我们将考虑一个到达银行的客户队列的示例。对于本例,这涉及三个类:客户:此类的实例表示队列中的客户。队列:客户所在的队列。到达:这是创建新客户以插入队列的过程。客户类和队列类的实现对于本例不重要。到达类 Arrivals 的实现如下:
class Arrivals extends SimulationProcess
{
public void run ()
{
for (;;)
{
Customer c = new Customer();
queue.insert(c);
hold(20.0);
}
}
}
Starting, ending and controlling a simulation
当在javasim中创建仿真进程对象 SimulationProcess 时,它将以 passive(被动)状态启动,并且必须在它可以参与仿真之前激活它。在编写javasim应用程序时,主线程通常创建一个控制器进程(例如上面的Arrival),该进程负责协调整个模拟运行。这将创建并激活所有模拟实体和调度程序,并提供挂起主线程的方法,从而允许控制器对象执行并退出应用程序。控制器接口示例如下所示,其方法的实现将在以下部分中描述:
public class Controller extends SimulationProcess
{
public void run ();
public void await ();
public void exit ();
}
因为控制器本身就是一个模拟过程 SimulationProcess ,所以它从模拟过程派生并定义一个 run() 方法,该方法将执行模拟的实际控制。它还提供以下方法:await():在主应用程序线程内调用此方法并挂起它,从而有效地传递对控制器进程的控制。exit():调用此方法以退出模拟。
await
当启动线程应用程序时,重要的是要意识到,在创建任何应用程序线程之前,Java虚拟机已经创建了一个运行应用程序。必须先挂起此线程,然后才能运行任何模拟线程。控制器的 await() 方法负责挂起此线程(主线程):
public void await ()
{
resumeProcess();
SimulationProcess.mainSuspend();
}
它必须首先恢复与控制器实例关联的线程(因为控制器是一个模拟进程,所以它以被动状态启动)。直到主线程被对静态mainsuspend方法的调用挂起后,此线程才会执行。main() 方法的代码如下:
public static void main (String[] args)
{
Controller c = new Controller();
c.await();
}
exit
为了退出模拟应用程序,应用程序可以调用System.Exit。但是,如果只需要恢复主线程,那么可以通过使用SimulationProcess类的静态mainResume方法来完成。一旦主线程恢复,它将从挂起的点继续执行。然后,调用mainresume的线程可以根据应用程序要求自行挂起或终止。
public void exit ()
{
if (resumeMainRequired)
{
SimulationProcess.mainResume();
suspend();
}
else
System.exit(0);
}
run
控制器的主体创建并激活其他模拟实体 simulation entities 和调度程序 scheduler ,并控制整个模拟(例如,在连续运行之间重置系统)。
public void run ()
{
/*
* create and activate any other simulation entities before
* moving on to the next step ...
*/
// we must create a scheduler for the simulation to run the simulation.
// starting the simulation does this for us.
Simulation.start();
// do whatever we need to in order to suspend this thread/process.
// print results
Simulation.stop(); // suspends the scheduler
// suspend simulation entities
mainResume();
}
对mainresume的最后一个调用会阻止 run() 退出,我们必须这样做才能确保应用程序在线程实现之间是可移植的。
reset
重置模拟涉及到重置其中涉及的所有对象,这些对象将是后续运行所必需的。在调度程序上调用Reset方法时,这将终止当前的模拟运行,并且当前挂起在调度程序队列上的所有模拟对象都将被唤醒,并向每个对象抛出RestartException。任何需要参与新模拟运行的对象都必须捕获此异常,将自身重置为与另一个模拟开始时一致的状态,然后变为挂起,以等待模拟重新启动。
/**
* 重置
*/
public void reset() {
try {
Simulation.reset();
} catch (SimulationException e) {
e.printStackTrace();
}
}
示例
如果我们采用上面的到达示例并添加重置方法,则代码可以是:
public class Arrivals extends SimulationProcess
{
public void run ()
{
for (;;)
{
try
{
for (;;)
{
Customer c = new Customer();
queue.insert(c);
hold(20.0);
}
}
catch (RestartException e)
{
}
}
}
}
1.3 异步仿真类
上述仅模拟的是同步的情况,即事件发生在特定的时间并形成一个明确的顺序。然而,有时需要模拟异步真实世界事件,例如处理器中断。要做到这一点,需要比调度程序提供的更精细地控制模拟进程的调度;调度程序只是根据模拟时间激活,而异步事件可能具有不同的激活规则,例如,在另一个进程终止时激活。模拟实体类和其他将在以下部分中描述的类为用户提供了所需的控制级别,扩展了使用Javasim可能进行的模拟类型。
Asynchronous Simulation Process (异步模拟过程)是从SimulationEntity派生的,但是,由于这些进程被挂起并在调度程序控制之外恢复,因此可能会发生死锁情况。因此,在使用这些类时必须注意一些。除了模拟过程可以处于的主动、挂起、被动和终止状态外,异步对象还可以处于以下状态:
状态 | 说明 |
等待 | 进程被挂起,等待特定事件发生(例如,要终止的进程)。等待进程未放置在调度程序队列上。 |
中断 | 处于等待状态的进程在等待状态发生之前已从此中断。 |
进程可以等待并由此中断的条件是:
条件 | 说明 |
时间 | 进程可以尝试等待指定的模拟时间段。 |
进程终止 | 进程可以在继续执行之前等待另一个并发进程的终止。 |
信号量 | 模拟的关键区域可以由信号量保护,其中只有一个实体进程可以获取信号量;其他进程被挂起,直到信号量被释放。 |
特定于用户 | 可能会发生上述未涉及的其他异步情况。 |
1.3.1 Asynchronous entities
public class SimulationEntity extends SimulationProcess
{
public void Interrupt (SimulationEntity toInterrupt, boolean immediate) throws SimulationException, RestartException;
public final void trigger ();
public void terminate ();
protected void timedWait (double waitTime) throws SimulationException, RestartException, InterruptedException;
protected void waitFor (SimulationEntity controller, boolean reAct) throws SimulationException, RestartException, InterruptedException;
protected void waitFor (SimulationEntity controller) throws SimulationException, RestartException, InterruptedException;
protected void waitForTrigger (TriggerQueue _queue) throws SimulationException, RestartException, InterruptedException;
protected void waitForSemaphore (Semaphore _sem) throws RestartException;
};
因为SimulationEntity是从SimulationProcess派生的,所以所有常用的模拟方法都是可用的,并且可以与派生类提供的方法一起使用。
方法 | 说明 |
Interrupt(SimulationEntity toInterrupt,Boolean Immediate) | 中断异步进程,该进程不得终止,且必须处于等待状态。toInterrupt成为下一个活动进程(即,它被移动到调度程序队列的头)。如果immediate为true,则当前进程将立即挂起;它计划在当前模拟时间重新激活。否则,当前进程将继续执行,并可以稍后以特定于应用程序的方式挂起。 |
terminate() | 在异步情况下,一个进程可以等待另一个进程终止,因此,terminate() 方法必须与SimulationProcess提供的方法不同。在终止进程结束之前,它将等待进程移动到调度程序队列的头部,然后调用SimulationProcess.terminate() 。 |
hold(double t) | 进程被移动到等待状态。 |
wait(double t) | 与hold方法类似,只是进程被移动到等待状态,并被放置在调度程序队列中。因此,可以在等待期结束之前中断此进程。如果进程中断,则返回true,否则返回false。 |
waitfor(SimulationEntity Controller,Boolean React) | 挂起当前进程,直到控制器终止。进程处于等待状态。如果React为true,则控制器将移动到调度程序队列的头部,成为下一个激活进程,否则(默认行为),应用程序将必须激活控制器。如果等待过程被中断,则方法返回true,否则返回false。控制器和当前进程必须不同,也就是说,进程不可能等待自身。触发队列是由等待特定事件发生的流程模拟系统维护的列表,不在上述范围内。 |
waitfortrigger(trigger queue queue) | 将当前进程放在触发器队列上并对其进行 passivates 。与前面的方法一样,返回值指示进程是被中断还是被触发。 |
waitforsemaphore(Semaphore sem) | 进程还可以等待信号量,例如允许创建监视区域 (monitor regions) 。导致当前进程尝试独占获取信号量。如果不可能,则进程将被挂起。目前,正在等待信号量的进程不能被中断,并且不会被置于等待状态。因此,当这个方法返回信号量时,就已经获得了。 |
1.3.2 Trigger queues
等待同一应用程序控制事件的进程可以组合到一个触发器队列中。
public class TriggerQueue
{
public TriggerQueue ();
public synchronized void triggerFirst (boolean setTrigger) throws NoSuchElementException;
public synchronized void triggerFirst () throws NoSuchElementException;
public synchronized void triggerAll () throws NoSuchElementException;
protected synchronized void insert (SimulationEntity toAdd) throws SimulationException;
protected synchronized SimulationEntity remove () throws NoSuchElementException;
};
发生此事件时,应用程序可以使用两种触发器方法之一来激活队列成员。这涉及到将进程放到调度程序队列的头上。
方法 | 说明 |
triggerall() | 触发队列中的所有成员。 |
triggerfirst( boolean settrigger) | 只触发队列的头部。如果settrigger为true(默认行为),那么也会调用SimulationEntity对象的trigger() 方法。如果队列在被虚拟机垃圾收集时不是空的,那么所有剩余的队列成员都将被触发,并放回调度程序队列。信号量实现通常使用触发器队列。但是,如果直接在派生类中使用 insert() 和 remove() 方法,则应用程序开发人员可以使用它们。 |
1.3.3 Semaphores
通过信号量(信号量类的实例),可以保护应用程序代码不受模拟过程的影响。
public class Semaphore
{
enum Outcome { DONE, NOTDONE, WOULD_BLOCK };
public Semaphore ();
public Semaphore (long number);
public synchronized long numberWaiting ();
public synchronized Outcome get (SimulationEntity toWait) throws RestartException;
public synchronized Outcome tryGet (SimulationEntity toWait) throws RestartException;
public synchronized Outcome release ();
};
信号量可以用来限制可以使用共享资源的进程的数量。创建信号量时,必须向其显示可用的共享资源数。默认情况下,信号量将假定只有一个资源,在这种情况下,信号量由模拟过程独占获取。但是,可以创建具有不同资源计数的信号量。
信号量可以以两种状态之一存在:
状态 | 说明 |
可用 | 可以获取信号量。 |
不可用 | 进程(或几个进程)当前占用了全部的信号量。如果另一个进程试图获取信号量,那么它将自动挂起,直到信号量可用,即直到资源被释放为止。 |
为了能够操作信号量,必须从SimulationEntity类派生一个进程。
方法 | 说明 |
Semaphore (long number) | 声明信号量,并指定资源个数,默认为1。 |
get( SimulationEntity toWait) | 获得信号量,其中towait是调用进程。如果信号量不可用,那么towait引用的进程将被挂起。如果成功获取信号量,则返回Outcome.DONE,否则返回Outcome.NOTDONE。 |
tryGet( SimulationEntity toWait) | 如果进程希望尝试获取信号量,但不希望在信号量当前不可用的情况下阻塞,则可以使用Tryget方法,该方法使用与get相同的参数。但是,与get不同,tryget将返回Outcome.WOULD_BLOCK,如果调用方调用了get(即信号量当前正在使用中)通常会阻塞的情况下。如果信号量没有被使用,那么Tryget将为调用者获取它。 |
release() | 释放信号量。成功释放信号量会返回Outcome.DONE,否则会返回Outcome.NOTDONE 。 |
numberWaiting() | 返回当前挂起等待信号量的进程数。如果信号量被垃圾收集,进程等待它,那么会显示一条错误消息。没有代表这些等待进程尝试进一步操作。 |
示例
下面的示例创建了一个信号量,它保护了3个模拟实体正在访问的2个资源:
public void test () throws Exception
{
Semaphore sem = new Semaphore(2);
DummyEntity e1 = new DummyEntity(10);
DummyEntity e2 = new DummyEntity(20);
DummyEntity e3 = new DummyEntity(30);
assertTrue(sem.numberWaiting() == 0);
Semaphore.Outcome result = sem.get(e1);
assertTrue(result == Semaphore.Outcome.DONE);
result = sem.get(e2);
assertTrue(result == Semaphore.Outcome.DONE);
result = sem.tryGet(e3);
assertTrue(result == Semaphore.Outcome.WOULD_BLOCK);
result = sem.get(e3);
assertTrue(result == Semaphore.Outcome.DONE);
assertTrue(sem.numberWaiting() == 1);
}
可以看到,在创建信号量时,会将资源的数量传递给它。然后我们创建3个模拟实体。在这个阶段,没有实体在等待信号量(numberWaiting返回0)。前两个实体在访问或操作资源之前通过调用semaphore.get() 获得对资源的访问权。在本例中,我们知道,由于还没有实体释放对资源的访问权,下一个试图通过semaphore.get() 获取访问权的实体将被阻止。为了验证这一点,我们可以使用semaphore.tryget() ,它在本例中返回Outcome.WOULD_BLOCK来指示这个事实。无论如何,然后我们尝试获取对资源的访问权,然后该实体将被阻止。这是通过检查等待释放信号量的实体数来验证的,在这种情况下,numberWaiting() 返回1。
2. org.javasim.streams
模拟试图建模的现实世界的许多方面都具有对应于各种分布函数的属性,例如银行队列中客户的到达率。因此,模拟研究需要随机数的来源。理想情况下,这些数据源应该产生无限的此类数据流,但要做到这一点,要么需要专门的硬件,要么需要能够存储事先生成的此类数据的无限大的表。如果没有这些不切实际或一般不可用的辅助手段,另一种方法是使用数值算法。任何确定性算法都不能产生一个具有真正随机序列所有性质的数字序列。然而,就所有实际目的而言,只需要产生的数字是随机的,即通过某些随机性统计测试。虽然这些生成器产生伪随机数,但我们继续调用随机数生成器。生成任意分布函数的起点是生成标准的均匀分布。正如我们将看到的,所有其他分布都可以基于此生成。javasim中的所有分布函数都依赖继承来专门化从统一分布类中获得的行为。这些类可以在org.javasim.streams包中找到。
2.1 RandomStream
实际的均匀分布类称为随机流。这将返回一系列均匀分布在0和1之间的随机数。我们先用几个随机数产生器进行了实验,然后用线性同余产生器对一个乘法产生器进行了随机数产生器的随机洗牌,这个线性同余产生器提供了一个相当均匀的伪随机数流。
public abstract class RandomStream
{
public abstract double getNumber () throws IOException, ArithmeticException;
public final double error ();
protected RandomStream ();
protected RandomStream (long MGSeed, long LCGSeed);
protected final double uniform ();
}
error() 方法返回均匀分布函数上的卡方误差度量。抽象方法 getNumber 必须由派生类提供,并用于获得访问随机数的统一方法。RandomStream类返回一个随机数的大序列,其周期为2的24次幂。但是,除非在创建每个随机分布类时修改种子,否则此序列中的起始位置将始终相同,即获得相同的数字序列。为了防止这种情况发生,从RandomStream派生的每个类都有一个额外的构造函数参数,该参数指示从中开始采样的序列中的偏移量。
2.2 UniformStream
UniformStream类继承自RandomStream,并返回在创建实例时指定范围内均匀分布的随机数。
public class UniformStream extends RandomStream
{
public UniformStream (double lo, double hi);
public UniformStream (double lo, double hi, int StreamSelect);
public UniformStream (double lo, double hi, int StreamSelect, long MGSeed, long LCGSeed);
public double getNumber () throws IOException, ArithmeticException;
};
范围包括由lo和hi指定的间隔。StreamSelect表示随机数序列中开始采样的偏移量,MGSeed和LCGSeed可用于修改RandomStream类使用的种子值。
2.3 ExponentialStream
指数流类返回一个指数分布的随机数流,其平均值由mean指定。
public class ExponentialStream extends RandomStream
{
public ExponentialStream (double mean);
public ExponentialStream (double mean, int StreamSelect);
public ExponentialStream (double mean, int StreamSelect, long MGSeed, long LCGSeed);
public double getNumber () throws IOException, ArithmeticException;
};
2.4 HyperExponentialStream
超指数类返回随机数的超指数分布,具有平均值和标准偏差sd。
public class HyperExponentialStream extends RandomStream
{
public HyperExponentialStream (double mean, double sd);
public HyperExponentialStream (double mean, double sd, int StreamSelect);
public HyperExponentialStream (double mean, double sd, int StreamSelect, long MGSeed, long LCGSeed);
public double getNumber () throws IOException, ArithmeticException;
};
2.5 NormalStream
返回随机数的正态分布,具有平均值和标准偏差sd。
public class NormalStream extends RandomStream
{
public NormalStream (double mean, double sd);
public NormalStream (double mean, double sd, int StreamSelect);
public NormalStream (double mean, double sd, int StreamSelect, long MGSeed, long LCGSeed);
public double getNumber () throws IOException, ArithmeticException;
};
2.6 TriangularStream
Triangularstream返回随机数的三角形分布,其下限A、上限B和模式C,其中A<B和A≤C≤B。
public class TriangularStream extends RandomStream
{
public NormalStream (double a, double b, double c);
public NormalStream (double a, double b, double c, int StreamSelect);
public NormalStream (double a, double b, double c, int StreamSelect, long MGSeed, long LCGSeed);
public double getNumber () throws IOException, ArithmeticException;
};
2.7 Draw
draw类是继承规则的例外,而不是通过委托使用RandomStream(出于历史原因)。这将通过概率prob返回true,否则返回false。
public class Draw
{
public Draw (double p);
public Draw (double p, int StreamSelect);
public Draw (double p, int StreamSelect, long MGSeed, long LCGSeed);
public boolean getBoolean () throws IOException;
};
示例
在本例中,我们将使用NormalStream实例从中获取值,然后将这些值插入柱状图中,这样我们就可以打印出内容和相关数据。
NormalStream str = new NormalStream(100.0, 2.0);
Histogram hist = new Histogram(10);
for (int i = 0; i < 1000; i++)
{
hist.setValue(str.getNumber());
}
System.out.println("NormalStream error: "+str.error());
hist.print();
执行结果如下:
NormalStream error: -0.047600000000002085
Maximum number of buckets 10
Merge choice is MEAN
PrecisionHistogram Data:
Number of buckets: 10
Bucket : < 98.63959917087956, 541 >
Bucket : < 99.41934896772582, 1 >
Bucket : < 99.89948948370221, 17 >
Bucket : < 100.00514051631782, 2 >
Bucket : < 100.26527227806514, 1 >
Bucket : < 100.85347549916973, 3 >
Bucket : < 101.31195503916659, 1 >
Bucket : < 101.38562305997606, 1 >
Bucket : < 101.72199923851397, 432 >
Bucket : < 102.68618953365238, 1 >
Variance : 4.00810615160317
Standard Deviation: 2.002025512225848
Number of samples : 1000
Minimum : 93.19494510640277
Maximum : 105.9001968416066
Sum : 100013.85724011554
Mean : 100.01385724011554
稍后将会进一步了解柱状图类 Histogram 。
3. org.javasim.stats
模拟的目的通常包括收集相关的统计信息,例如队列中花费的平均时间长度。javasim为收集此类信息提供了许多不同的类。这些类可以在org.javasim.stats包中找到。
3.1 Mean
这是从中派生其他类的基本类,收集提供给它的样本的统计信息。
public class Mean
{
public void setValue (double value) throws IllegalArgumentException;
public void reset ();
public int numberOfSamples (); public double min ();
public double max (); public double sum (); public double mean ();
public boolean saveState (String fileName) throws IOException;
public boolean saveState (DataOutputStream oFile) throws IOException;
public boolean restoreState (String fileName) throws FileNotFoundException, IOException;
public boolean restoreState (DataInputStream iFile) throws IOException;
public void print ();
};
可以使用 setValue(double) 方法向Mean类的实例提供新值。已提供的样本数可以从 NumberOfSamples() 中获取。提供的样本的最大值和最小值可以分别从 max() 和 min() 方法中获得。sum() 返回所有样本的总和。mean() 返回所有样本的均值。
可以使用 reset() 方法在样本之间重置mean实例。
如果需要在模拟运行之间保存 Mean 对象的状态,则可以使用任一saveState方法使其保持不变。其中,第一个实例方法将状态保存到文件中,第二个实例方法将状态保存到java.io.DataOutputStream 类的实例中。同样有两种相应的状态恢复方法。print方法将对象的当前状态打印到System.out。
3.2 Variance
该类是从Mean派生的,除了提供上述功能外,还提供以下功能:
public class Variance extends Mean
{
public void setValue (double value) throws IllegalArgumentException;
public void reset ();
public double variance ();
public double stdDev ();
public double confidence (double value);
public void print ();
public boolean saveState (String fileName) throws IOException;
public boolean saveState (DataOutputStream oFile) throws IOException;
public boolean restoreState (String fileName) throws FileNotFoundException, IOException;
public boolean restoreState (DataInputStream iFile) throws IOException;
};
variance() 返回样本的方差。
stdev() 返回样本的标准差,即方差的平方根。
3.3 TimeVariance
TimeVariance类可以确定在模拟时间方面维护特定值的时间长度。实际上,值是根据持有的时间长度加权的,而对于Variance 类,只考虑特定的值。
public class TimeVariance extends Variance
{
public void reset ();
public void setValue (double value) throws IllegalArgumentException;
public double timeAverage ();
public boolean saveState (String fileName) throws IOException;
public boolean saveState (DataOutputStream oFile) throws IOException;
public boolean restoreState (String fileName) throws FileNotFoundException, IOException;
public boolean restoreState (DataInputStream iFile) throws IOException;
};
每当向时间方差类的实例提供值时,也会注意到发生该值的模拟时间。如果某个值发生更改,或者调用了TimeAverage()方法,那么将计算维护该值的时间,并更新统计数据。
均值、方差和时间方差提供了模拟的快照。但是,直方图(Histograms)可以提供关于在模拟运行过程中值范围如何变化的更好信息。可以通过多种方式查看这些信息,但通常以图形形式打印。直方图通常为给定的每个值或值范围保留一个槽。这些槽被称为桶,这些桶的维护和操作方式产生了各种不同的柱状图实现。以下各节详细介绍了各种不同的柱状图类。
3.4 PrecisionHistogram
Precision Histogram类表示从中派生所有其他柱状图的核心柱状图类。这个类保持给它的所有值的精确计数,即为每个值创建一个桶。虽然bucket仅在需要时创建,但在模拟过程中,它仍然可以使用大量资源,因此提供了其他不太精确的柱状图类。
public class PrecisionHistogram extends Variance
{
public void setValue (double value) throws IllegalArgumentException;
public void reset ();
public long numberOfBuckets ();
public double sizeByIndex (long index) throws StatisticsException, IllegalArgumentException;
public double sizeByName (double name) throws IllegalArgumentException;
public boolean saveState (String fileName) throws IOException;
public boolean saveState (DataOutputStream oFile) throws IOException;
public boolean restoreState (String fileName) throws FileNotFoundException, IOException;
public boolean restoreState (DataInputStream iFile) throws IOException;
public void print ();
};
与从中派生它的方差类以及明显可用的方法一样,可以通过setValue(double)方法向柱状图提供值。柱状图维护的存储桶数量可以从NumberOfBuckets() 方法中获得。每个bucket都以其包含的值唯一命名,并且还可以通过其在整个bucket列表中的索引进行访问。因此,有两种方法可以获取存储桶中的条目数:
按存储桶的索引编号:sizeByIndex(long index)。
通过bucket的唯一名称:sizeByName(双名称)。
如果bucket不存在,那么这些方法中的每一个都会抛出IllegalArgumentException。
可以使用print() 方法将柱状图的内容输出到标准输出。
3.5 Histogram
Precision Histogram 类会消耗大量的系统资源,特别是在长时间的模拟过程中。柱状图试图通过呈现一个不太准确但消耗更少资源的柱状图来缓解这一问题。它不为每个单独的值维护一个桶,而是保留固定数量的桶。最初,每个存储桶将存储单独的值,如精度柱状图中所示,但当所需存储桶的数量超过指定的最大数量时,它会合并存储桶对,从而减少它们的总数。合并存储桶时使用的策略,创建时按实例设置。目前的政策是:
ACCUMULATE(累加): 创建一个与两个bucket中最大的bucket同名的新bucket,其条目号为两个旧bucket条目的和。
Mean:创建一个新bucket,其名称为两个旧bucket的平均值,其条目号为两个旧bucket条目的和。
Max:创建一个新的bucket,其名称为两个bucket中最大的一个,条目数相同。
Min:创建一个新bucket,名称是两个旧bucket中最小的一个,条目数相同。
public class Histogram extends PrecisionHistogram
{
public Histogram (long maxIndex, int mergeChoice); public Histogram (long maxIndex);
public void setValue (double value) throws IllegalArgumentException;
public boolean saveState (String fileName) throws IOException;
public boolean saveState (DataOutputStream oFile) throws IOException;
public boolean restoreState (String fileName) throws FileNotFoundException, IOException;
public boolean restoreState (DataInputStream iFile) throws IOException;
public void print ();
};
创建柱状图实例时,必须指定允许的最大存储桶数。还可以提供合并算法,默认为平均策略。
3.6 SimpleHistogram
与上面的柱状图类一样,SimpleHistogram将分配的存储桶数保持在最小值。但是,它是通过在创建bucket时预先创建bucket来实现的,即必须在开始时提供所需bucket的数量。宽度是为每个bucket指定的,并且每当为柱状图类指定一个值时,它就会被放入宽度在其内的bucket中。
public class SimpleHistogram extends PrecisionHistogram
{
public SimpleHistogram (double min, double max, long nbuckets);
public SimpleHistogram (double min, double max, double w);
public void setValue (double value) throws IllegalArgumentException;
public void reset ();
public double sizeByName (double name) throws IllegalArgumentException;
public double width ();
public void print ();
public boolean saveState (String fileName) throws IOException;
public boolean saveState (DataOutputStream oFile) throws IOException;
public boolean restoreState (String fileName) throws FileNotFoundException, IOException;
public boolean restoreState (DataInputStream iFile) throws IOException;
};
当类被实例化时,必须提供它将接收的值的范围。然后,可以给出每个桶的宽度或实际桶数。如果提供了宽度,柱状图将自动计算每个桶的数量,否则它将通过平均划分每个桶之间的范围来计算每个桶的宽度。桶的值可以从sizeByName() 方法获得。每个桶的宽度由 width() 方法提供。
3.7 Quantile
分位数类提供了一种获取值分布的p分位数的方法,即,低于该分布p%的值。
public class Quantile extends PrecisionHistogram
{
public Quantile ();
public Quantile (double q) throws IllegalArgumentException;
public double getValue ();
public double range ();
public void print ();
};
当对象被实例化时,必须指定p分位数的概率范围,并且可以通过 range() 方法得到。实际的分位数值由 getValue() 方法提供。
示例
在这个例子中,与前面讨论流时所看到的类似,取一个正态流,并将从中获得的数据推送到分位数实例中(同样也是一个精度柱状图,如上所示)。
NormalStream str = new NormalStream(100.0, 2.0);
Quantile hist = new Quantile();
for (int i = 0; i < 20; i++)
{
hist.setValue(str.getNumber());
}
System.out.println("NormalStream error: "+str.error());
hist.print();
输出结果:
NormalStream error: -0.12180000000000302
Quantile precentage : 0.95
Value below which percentage occurs 103.2525536140605
PrecisionHistogram Data:
Number of buckets: 20
Bucket : < 95.4184404867909, 1 >
Bucket : < 96.77470267057221, 1 >
Bucket : < 98.06420534765873, 1 >
Bucket : < 98.51326185839879, 1 >
Bucket : < 98.83083271038129, 1 >
Bucket : < 99.02208650724813, 1 >
Bucket : < 99.25398638929686, 1 >
Bucket : < 99.61818993348244, 1 >
Bucket : < 99.69668527826141, 1 >
Bucket : < 99.90066145276452, 1 >
Bucket : < 100.13242192897384, 1 >
Bucket : < 100.16716965330903, 1 >
Bucket : < 100.29000453526925, 1 >
Bucket : < 100.6675785365828, 1 >
Bucket : < 100.79956320347745, 1 >
Bucket : < 100.8830352383034, 1 >
Bucket : < 101.04146043991506, 1 >
Bucket : < 101.30326745585228, 1 >
Bucket : < 103.2525536140605, 1 >
Bucket : < 103.56896793616222, 1 >
Variance : 3.588431232925933
Standard Deviation: 1.8943155051168041
Number of samples : 20
Minimum : 95.4184404867909
Maximum : 103.56896793616222
Sum : 1997.199075176761
Mean : 99.85995375883805
因为分位数也是一个精确的柱状图,所以每个唯一的数字都被分配一个唯一的bucket实例。
4. org.javasim.interal
有关javasim.interal的信息官方文档中没有给出,因此,笔者简要做一下补充。
如上图所示, 该包中仅有三个类。SimulationProcessCons 类和SimulationProcessList 类组成了链表数据结构,其中,SimulationProcessCons 代表链表节点,SimulationProcessList 代表SimulationProcess 对象的链表。在SimulationProcessCons 对象中,Proc代表当前的SimulationProcess对象,Next代表下一个SimulationProcess对象。在SimulationProcessList对象中包含了对链表中节点插入、删除、获取下一个节点等操作。其中,迭代器SimulationProcessIterator通过get方法依次遍历获取列表中的值,具体实现如下。
public final synchronized SimulationProcess get ()
{
if (ptr != null)
{
SimulationProcessCons p = ptr;
ptr = ptr.cdr();
return p.car();
}
return null;
}