项目需求:
模拟实现银行业务调度系统逻辑。
银行内有6个业务窗口:1 - 4号窗口为普通窗口,5号窗口为快速窗口,6号窗口为VIP窗口。
三种对应类型的客户:VIP客户,普通客户,快速客户(办理如交水电费、电话费之类业务的客户)。
异步随机生成各种类型的客户,生成各类型用户的概率比例为:VIP客户 :普通客户 :快速客户 = 1 :6 :3。
客户办理业务所需时间有最大值和最小值,在该范围内随机设定每个VIP客户以及普通客户办理业务所需的时间,快速客户办理业务所需时间为最小值(提示:办理业务的过程可通过线程Sleep的方式模拟)。
各类型客户在其对应窗口按顺序依次办理业务。
当VIP(6号)窗口和快速业务(5号)窗口没有客户等待办理业务的时候,这两个窗口可以处理普通客户的业务,而一旦有对应的客户等待办理业务的时候,则优先处理对应客户的业务。
随机生成客户时间间隔以及业务办理时间最大值和最小值自定,可以设置。
不要求实现GUI,只考虑系统逻辑实现,可通过Log方式展现程序运行结果。
功能增强:
在以上项目需求的基础上,本文章又做了功能扩展,可以改变业务窗口的数量,和随机产生用户的时间。
同时使用反射和JavaBean技术将程序的可扩展性提高,假如程序需要增加一个业务窗口的种类,比如汇款窗口,那么仅仅只需要从NumberMachine派生一个新类,改写对应常量工具类里的常量数据,或是直接从配置文件读取,即可对程序再次功能扩展。
项目分析:
1.异步随机生成各种类型的客户1:3:6需要使用3个定时线程触发器,1号触发器模拟生成普通用户每1秒运作一次,2号触发器关模拟生成速客户每3秒触发一次,3号触发器模拟生成vip客户每6秒触发一次。触发器每一次触发将产生一个新的客户,将客户以数字为代号装入一个集合队列中。
2.银行内6个业务窗口,可以同一时间为客户服务,所以每一个窗口必然是独立的线程机制,可以同时运作独立完成工作,互不影响。
3.使用三个队列管理器,分别对应,普通,快速,贵宾。每产生一个新的用户以有序方式储存,业务窗口空闲时间先从该窗口对应业务队列尾部获取用户排号并删除该用户排号,如果快速与贵宾窗口取队列时为空,那么转向取普通队列的数据。
4.队列可使用ArrayList与LinkedList集合。由于每次取的是队列尾数据,使用数组结构会导致每次处理一个用户,当前所有队列数据须要全部前移,而且队列用户量不稳定,ArrayList无法自行回收已经使用的内存,所以这里使用LinkedList链表结构,头删尾增仅仅只是节点操作,效率最高,尤其是当用户量非常大的情况下。
5.数据存取,6个窗口并发取用户,3个触发器并发存用户,那么该数据会存在同时被9个线程操作的情况,所以数据必须同步化。
6.客户办理业务所需时间,读取一个时间最大值与最小值在这中间使用Random随机产生业务用时。
类分布大纲:
从名词抽取抽象类:
Bank 银行类,有业务窗口必须要先有一个银行即银行的抽象表现形式
NumberManager 队列管理类,用于封装队列信息即队列的存取功能的抽象
ServiceWindow 服务窗口类,各种类型服务窗口抽象
从功能分划封装类:
MainClass 主函数类,用于建立银行即程序入口
CustomerType 枚举类型,将业务类型映射成文字用于输出
ConstTool 静态数据类用于装载静态数据即静态功能函数
NumberMachine 封装所有业务类型 单列设计模式
代码流程大纲:
Main函数内建立Bank类对象,通过银行类功能,创建业务窗口createWindow,创建三个模拟用户生成触发器createCustomerGenerator。一个用于生成普通客户,一个用于生成快速客户,一个用于生成贵宾客户。
三个触发器是三个独立的线程,按照事先规定好的时间比例定时运行,每一次触发产生一个新的模拟用户,并将其装入对应的集合队列中。
触发器模拟产生新用户,必须有相对应的用户类型信息即用户类型对应的实例对象,而触发器又必须是独立的运行线程,所以创建一个继承自Thread的类,复写run函数,在该类创建的时候将对应的业务以字符串形式传递进去,在类内部通过静态工具类的反射功能函数,JavaBean功能函数等获取到对应的用户类型和业务类型实例对象。
使用字符串做为参数的主要原因是因为可以将其设置为常量,可以从配置文件读取该字符串常量,增强程序的可扩展性。只须要继承业务类型的单列设计类,然后从配置文件读取相关常量信息就可以完成程序的扩展。
建立一个NumberMachine类,封装三个业务类型NumberManager类的对象,分别是commonManager与expressManager和vipManager。将NumberMachine类设计为单例模式,并为NumberManager类的三个对象建立JavaBean方法。触发器将通过该类调用相应的NumberManager对象为不同的用户需要队列添加新的模拟客户。
业务窗口独立封装成一个类,每一个业务窗口为该类的一个实例对象,将窗口的业务类型和编号封装进类内部,窗口类中设计一个内部类ServiceThread实现于Runnable并复写run方法用于线程运行。在run方法内switch判断当前对象所属的业务类型,然后将该类型以字符串形式传递给service方法做为要处理的业务类型标记。
service方法内通过反射JavaBean功能获取字符串参数对应的NumberManager类对象,以及当前处理业务的类型。将当前线程所属业务类型编号等信息打印,使用if判断对应的用户队列中是否有需要办理业务的用户,如果有调用startService方法为该用户服务。
在startService方法中通过设定好的常量最大时间与最小时间,获取一个随机时间值,使用sleep方法将线程冰结,并打印相关提示信息,线程冰结时间超时恢复运行并打印业务完成。
在service方法中如果当前业务窗口所属类型对应的用户队列是空,那么进去if else根据传递过来的字符串判断当前窗口正在处理的业务是不是普通任务,如果是那么sleep冻结线程1秒模拟窗口休息状态。如果当前窗口正在处理业务不是普通业务,那么最后else再次调用service方法将要处理的业务标记为普通业务,让非普通业务窗口去处理普通业务。
建立一个枚举,并复写toString方法,将对应的枚举成员转换成字符串表现形式,用于打印输出。
建立一个常量工具类,用于存放时间常量与三种不同业务类型的字符串常理,并且在该静态类内封装上列多线程时需要用到的反射方法,JavaBean方法等,将这些方法定为静态以便重复调用。
实例代码:
Bank类:
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* 银行类,在创建银行后,应当调用createWindow与createCustomerGenerator创建对应的业务窗口与模拟用户触发器
* @author Slatop
*
*/
public class Bank {
//是否已经创建了业务窗口
private boolean windowFlag = false;
//是否已经创建了用户触发器
private boolean generatorFlag = false;
/**
* 创建一个银行
*/
public Bank() {
System.out.println("您的银行建立成功了,请接下来创建服务窗口与用户触发器!");
}
/**
* @param commonWinCount 创建普通业务窗口数量
* @param expressWinCount 创建快速业务窗口数量
* @param vipWinCount 创建贵宾业务窗口数量
*/
public void createWindow(int commonWinCount, int expressWinCount, int vipWinCount) {
if (windowFlag) {
System.out.println("服务窗口只能建立一次,如果需要重新建立服务窗口请将银行先拆掉!");
return;
}
windowFlag = true;
//循环创建四个普通窗口
for (int i = 1; i < commonWinCount + 1; i++) {
ServiceWindow commonWindow = new ServiceWindow();
commonWindow.setWindowId(i);
commonWindow.start();
}
//创建一个快捷窗口
for (int i = 1; i < expressWinCount + 1; i++) {
ServiceWindow expressWindow = new ServiceWindow();
expressWindow.setWindowId(i);
expressWindow.setType(CustomerType.EXPRESS);
expressWindow.start();
}
//创建一个贵宾窗口
for (int i = 1; i < vipWinCount + 1; i++) {
ServiceWindow vipWindow = new ServiceWindow();
vipWindow.setWindowId(i);
vipWindow.setType(CustomerType.VIP);
vipWindow.start();
}
}
/**
* @param commonGenTime 普通用户触发器延时,以秒为单位
* @param expressGenTime 快速用户触发器延时,以秒为单位
* @param vipGenTime 贵宾用户触发器延时,以秒为单位
*/
public void createCustomerGenerator(int commonGenTime, int expressGenTime, int vipGenTime) {
if (generatorFlag) {
System.out.println("用户触发器只能建立一次,如果需要重新建立触发器请将银行先拆掉!");
return;
}
generatorFlag = true;
//普通用户触发器
Executors.newScheduledThreadPool(1).scheduleAtFixedRate(
new CustomerGenerator(ConstTool.COMMON), 0, commonGenTime, TimeUnit.SECONDS);
//快速用户触发器
Executors.newScheduledThreadPool(1).scheduleAtFixedRate(
new CustomerGenerator(ConstTool.EXPRESS), 0, expressGenTime, TimeUnit.SECONDS);
//贵宾用户触发器
Executors.newScheduledThreadPool(1).scheduleAtFixedRate(
new CustomerGenerator(ConstTool.VIP), 0, vipGenTime, TimeUnit.SECONDS);
}
//内部线程类,每一个该线程类对象代表一个用户生成触发器
private class CustomerGenerator extends Thread{
//当前用户类型字符串表现形式
private String type = null;
public CustomerGenerator(String type) {
this.type = type;
}
@Override
public void run() {
//获取对应的NumberManager对象
NumberManager manager = ConstTool.getJavaBeanValue(type);
//使用当前NumberManager对象增加新用户到列队中
Integer number = manager.generateNewManager();
//获取当前用户的类型
CustomerType clientType = ConstTool.getClientType(type);
System.out.println(number + "号" + clientType + "客户等待服务!");
}
}
}
NumberManager类:
import java.util.LinkedList;
import java.util.List;
/**
* 客户队列
* @author Slatop
*
*/
class NumberManager {
//客户编号
private int lastNumber = 0;
//所有已申请服务客户队列集合
private List<Integer> queueNumber = new LinkedList<Integer>();
//添加新客户
/**
* 在当前客户类型下添加新的客户到队列集合中
* @return 返回新客户编号
*/
public synchronized int generateNewManager() {
//将新客户添加进等待集合中
queueNumber.add(++ lastNumber);
//返回该客户编号
return lastNumber;
}
//获取用户队列元素
/**
* 在当前客户类型下删除最早加入到队列集合中的客户
* @return 返回删除客户编号
*/
public synchronized Integer fetchServiceNumber() {
//如果队列不为空那么获取最早进队的元素
if(queueNumber.size() > 0) {
return queueNumber.remove(0);
}
return null;
}
}
NumberMachine单例类:
/**
* 用于管理所有客户类型
* @author Slatop
*
*/
class NumberMachine {
//普通客户对象
private NumberManager commonManager = new NumberManager();
//快捷客户对象
private NumberManager expressManager = new NumberManager();
//贵宾客户对象
private NumberManager vipManager = new NumberManager();
//隐藏构造,强制机器类为单一实例类
private NumberMachine(){}
//号码机器实例
private static NumberMachine instance = new NumberMachine();
//获取本类唯一实例对象
public static NumberMachine getInstance() {
return instance;
}
//设置普通客户
public void setCommonManager(NumberManager commonManager) {
this.commonManager = commonManager;
}
//设置快捷客户
public void setExpressManager(NumberManager expressManager) {
this.expressManager = expressManager;
}
//设置贵宾客户
public void setVipManager(NumberManager vipManager) {
this.vipManager = vipManager;
}
//获取普通客户
public NumberManager getCommonManager() {
return commonManager;
}
//获取快捷客户
public NumberManager getExpressManager() {
return expressManager;
}
//获取贵宾客户
public NumberManager getVipManager() {
return vipManager;
}
}
ServiceWindow类:
/**
* 业务窗口类,在创建一个该类对象后,应调用setType设置窗口对应的类型,setWindowsId设置窗口对应的编号,
* 如果未进行设置,那么窗口默认为普通类型窗口,编码为1。
* 调用start将该对象加入到新的线程队列中动行,否则不起任何作用。
* @author Slatop
*
*/
class ServiceWindow {
//代表当前线程业务窗口类型
private CustomerType type = CustomerType.COMMON;
//当前窗口的编号
private int windowId = 1;
/**
* 设置当前窗口业务类型
* @param type 窗口类型
*/
public void setType(CustomerType type) {
this.type = type;
}
/**
* 设置当前窗口的编号
* @param windowId 窗口编号
*/
public void setWindowId(int windowId) {
this.windowId = windowId;
}
/**
* 开启内部线程
*/
public void start() {
new Thread(new ServiceThread()).start();
}
/**
* 用于判断当前业务类型窗口有没有对应的业务可以处理
* @param propertyName 用于当前服务类型的字符串表现形式,函数内使用该参数多次参与反射,传值取谨慎。
*/
private void service(String propertyName) {
//获取对应的窗口类型对象
NumberManager manager = ConstTool.getJavaBeanValue(propertyName);
//使用当前窗口类型对象获取当前用户类型队列中的数据
Integer number = manager.fetchServiceNumber();
//获取当前用户的类型
CustomerType clientType = ConstTool.getClientType(propertyName);
//定义业务窗口类型与编号打印字符串
String WindowsName = "第" + windowId + "号[" + type + "]窗口";
//打印正在获取的服务类型
System.out.println(WindowsName + "正在获取" + clientType + "任务!");
//判断当前窗口对应的客户队列集合中是否有元素
if (number != null) {
//开始服务
startService(propertyName, number, clientType, WindowsName);
}
//如果当前对应队列为空,那么判断当前是否为普通服务窗口
else if(propertyName.equals(ConstTool.COMMON)){
//普通队列为空提示休息信息
System.out.println(WindowsName + "没有取到任何服务任务,暂停1秒!");
try {
//当前窗口休息1秒
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
//如果当前对应队列为空,又不是普通窗口,那么调用普通队列任务
else {
System.out.println("当前" + type + "客户队列为空" + WindowsName + "临时转为普通服务窗口!");
service(ConstTool.COMMON);
}
}
/**
* 当前类型窗口为number号clientType类型客户开始服务
* @param serviceType 当前业务窗口的服务类型字符串表现形式
* @param number 当前获取到的客户编号
* @param clientType 当前客户所需服务类型
* @param WindowsName 当前业务窗口打印信息
*/
private void startService(String serviceType, Integer number,
CustomerType clientType, String WindowsName) {
//获取处理业务需要的时间
long serviceTime = 0;
//如果当前类型为快速服务窗口时间为最小
if (serviceType.equals(ConstTool.EXPRESS)) {
serviceTime = ConstTool.MIN_SERVICE_TIME;
}
//否则时间为定义好范围的随机数
else {
serviceTime = ConstTool.getRandomTime();
}
//打印提示服务开始
System.out.println(WindowsName + "开始为第" + number + "个[" + clientType + "]客户服务!");
try {
//sleep当前线程模拟服务延时
Thread.sleep(serviceTime);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//打印提示服务结束
System.out.println(WindowsName + "为第" + number + "个[" + clientType +
"]客户完成任务,耗时大约" + serviceTime/1000 + "秒");
}
//内部线程类,每一个该线程类对象代表一个业务窗口
private class ServiceThread implements Runnable {
@Override
public void run() {
String propertyName = null;
while(true) {
switch (type) {
case COMMON:
propertyName = ConstTool.COMMON;
break;
case EXPRESS:
propertyName = ConstTool.EXPRESS;
break;
case VIP:
propertyName = ConstTool.VIP;
break;
}
service(propertyName);
}
}
}
}
ConstTool静态工具类:
import java.beans.PropertyDescriptor;
import java.util.Random;
/**
* 提供了相应的工具函数以及常量值设置
* @author slatop
*
*/
class ConstTool {
private ConstTool(){}
//最大服务时间
public static int MAX_SERVICE_TIME = 10000;
//最小服务时间
public static int MIN_SERVICE_TIME = 1000;
//字符串参数信息,用于反射
public static String COMMON = "commonManager";
public static String EXPRESS = "expressManager";
public static String VIP = "vipManager";
public static String TYPE_ENDSWITH = "Manager";
/**
* 随机返回一个最大与最小时间中的任意一个时间毫秒值
* @return 时间差内的随机值
*/
public static long getRandomTime() {
//获取max到min间的时间差
int maxRand = MAX_SERVICE_TIME - MIN_SERVICE_TIME;
//以时间差为基数生产0到maxRand之间的随机数,产生的随机数包含0不包含参数,所以参数加1
return new Random().nextInt(maxRand + 1) + MIN_SERVICE_TIME;
}
/**
* 用于反射三种不同窗口类型的JavaBean属性
* @param propertyName 从字符串表现形式反射当前服务窗口的类型对象
* @return 返回该窗口的类型对象
*/
public static NumberManager getJavaBeanValue(String propertyName) {
NumberMachine machine = NumberMachine.getInstance();
NumberManager Manager = null;
try {
//通过反射的JavaBean属性,根据字符串变量的值获取NumberMachine单一对象内三种类型窗口的对象
Manager = (NumberManager)new PropertyDescriptor(propertyName, machine.getClass()).getReadMethod().invoke(machine);
} catch (Exception e) {
throw new RuntimeException("获取窗口类型失败!");
}
return Manager;
}
/**
* 使用反射获取当前客户的类型
* @param propertyName 从字符串表现形式反射枚举客户类型
* @return 返回该枚举客户类型
*/
public static CustomerType getClientType(String propertyName) {
//当前用户的类型
CustomerType clientType = null;
//根据传递过来的字符串参数获取一个对应的客户类型大写单词,用于以下反射
String clientTypeStr = propertyName.replaceFirst(TYPE_ENDSWITH, "").toUpperCase();
try {
//反射出枚举中对应的当前服务用户类型,话说反射太好用了比函数指针都牛哈哈
clientType = (CustomerType) CustomerType.class.getField(clientTypeStr).get(null);
} catch (Exception e) {
//反射出现异常用户类型默认为普通用户
System.out.println("获取客户类型出错,该客户被默认为普通客户!");
clientType = CustomerType.COMMON;
}
return clientType;
}
}
CustomerType枚举:
/**
* 用户类型的枚举类
* @author Slatop
*
*/
enum CustomerType {
//枚举元素,普通,快捷,贵宾
COMMON, EXPRESS, VIP;
@Override
//复写toString将枚举元素映射成文字
public String toString() {
switch (this) {
case COMMON:
return "普通";
case EXPRESS:
return "快捷";
case VIP:
return "贵宾";
default:
return null;
}
};
}
Main函数程序入口类:
public class MainClass {
/**
* 程序入口
* @param args 无
*/
public static void main(String[] args) {
Bank bank = new Bank();
bank.createWindow(8, 2, 2);
bank.createCustomerGenerator(1, 3, 6);
}
}