0%

模拟银行(多线程程序)

模拟银行的一天从早上8点开门到晚上五点关门,顾客不断的到来,多线程控制四个银行服务窗口为顾客进行业务办理。关门后输出一天的日志以及统计信息。

(模拟银行,多线程程序)java作业~

模拟银行(多线程程序)

要求

  • 模拟客户可以随机办理银行提供的8种业务中的一种;期中办理时间规划为(基准时间自行配置):

    业务 业务序号 办理时间范围
    存款 1 0.5基准时间—1.5基准时间
    取款 2 0.5基准时间—1.5基准时间
    缴纳罚款 3 1.2基准时间—2基准时间
    开通网银 4 5基准时间—8基准时间
    交水电费 5 1.5基准时间—2基准时间
    购买基金 6 2基准时间—3基准时间
    转账汇款 7 3基准时间—4基准时间
    个贷还款 8 2基准时间—4基准时间
  • 程序模拟多个窗口服务,窗口服务具有一定的差异化,现在窗口的开放情况如下

    窗口类型 能办理业务类型 说明
    A类窗口 1,2,3,4,5,6,7,8 所有客户可以办理
    B类窗口 1,2,4,5,7 所有客户可以办理
    V类窗口 1,2,3,4,5,6,7,8 VIP客户优先

    系统模拟窗口开放情况:A类:1个 B类:2个 V类:1个

  • 顾客模拟要求

    • 顾客分为普通客户和VIP客户,普通用户和VIP客户比例自行确定
    • 顾客到达随机模拟
  • 模拟程序模拟一天的营业情况,在完成当天的营业(模拟为上午8:00-下午17:00)后打印出客户列表,包括以下项目:
    (客户名称,客户到达时间,客户办理业务类型,客户所用时间)

  • 统计出以下数据

    • 当天的所有顾客平均办理时间(从到达到办理完成时间定义为顾客办理时间)
    • 不同业务在所有办理业务中所占的比例
  • 程序能够参数化设定当天到达客户办理不同业务的比例(比如业务1占20%等)

代码思路

基本结构类

业务类

对于业务类型我使用了枚举类,在枚举类中添加了他的一些成员变量,比如说id,办理时间范围,还有名称。(getter和setter就不复制进来了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public enum Business {
/**
* 存款
*/
DEPOSIT(1,0.5,1.5,"存款"),
/**
* 取款
*/
WITH_DRAWAL(2,0.5,1.5,"取款"),
/**
* 缴纳罚款
*/
PAY_FINE(3,1.2,2,"缴纳罚款"),
/**
* 开通网银
*/
OPEN_ONLINE_BANKING(4,5,8,"开通网银"),
/**
* 交水电费
*/
PAY_WATER_AND_ELECTRICITY_BILLS(5,1.5,2,"交水电费"),
/**
* 购买基金
*/
PURCHASE_FUND(6,2,3,"购买基金"),
/**
* 转账汇款
*/
TRANSFER_REMITTANCE(7,3,4,"转账汇款"),
/**
* 个贷还款
*/
INDIVIDUAL_LOAN_REPAYMENT(8,2,4,"个贷还款");

protected int id;
protected double lowTimeProportion;
protected double highTimeProportion;
protected String name;

private Business(int id, double lowTimeProportion, double highTimeProportion,String name){
this.id = id;
this.lowTimeProportion=lowTimeProportion;
this.highTimeProportion = highTimeProportion;
this.name = name;
}
}

客户类

创建了一个客户类,类中priority用来判定这个顾客是否为VIP,然后其他的变量就是简单的id,名字,准备办理的业务类型,最后还有到达时间、结束时间、等待时间。(getter和setter就不复制进来了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Customer {
private int priority;
private int id;
private String name;
private Business business;
private long arriveTime;
private long finishTime;
private long waitTime;

public Customer(int id, String name,int priority,Business business) {
this.priority = priority;
this.id = id;
this.name = name;
this.business = business;
}
}

日志类

创建了日志类,之后银行结束后会对应输出这些日志信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Content {
private String customerName;
private int priority;
private long arriveTime;
private long finishTime;
private long waitTime;
private Business businessType;
private long useTime;
private Window serveWindow;

public Content(String customerName, int priority, long arriveTime,long finishTime,long waitTime, Business businessType, long useTime, Window serveWindow) {
this.customerName = customerName;
this.priority = priority;
this.arriveTime = arriveTime;
this.finishTime = finishTime;
this.waitTime = waitTime;
this.businessType = businessType;
this.useTime = useTime;
this.serveWindow = serveWindow;
}
}

窗口类

窗口类型中有窗口id,窗口名,窗口可服务类型(见要求第二点)

1
2
3
4
5
6
7
8
9
10
11
12
public class Window {

private int id;
private String name;
private ArrayList<Integer> serviceType;

public Window(int id,String name, ArrayList<Integer> serviceType) {
this.id = id;
this.name = name;
this.serviceType = serviceType;
}
}

特殊类

比例随机类

在这里分配了8中比率,对应8个业务类型,使用这个类的比例设置来模拟生成业务类型,默认情况也就是不输入任何比例参数,各个业务类型出现的比例都一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class RateRandom {
private double rate1 = 0.125;
private double rate2 = 0.125;
private double rate3 = 0.125;
private double rate4 = 0.125;
private double rate5 = 0.125;
private double rate6 = 0.125;
private double rate7 = 0.125;
private double rate8 = 0.125;

public RateRandom(double rate1, double rate2, double rate3, double rate4, double rate5, double rate6, double rate7, double rate8) {
this.rate1 = rate1;
this.rate2 = rate2;
this.rate3 = rate3;
this.rate4 = rate4;
this.rate5 = rate5;
this.rate6 = rate6;
this.rate7 = rate7;
this.rate8 = rate8;
}

public RateRandom(){

}
public Business getRandomBusiness(){
double randomNumber;
randomNumber = Math.random();
if(randomNumber>0 && randomNumber<=rate1){
return Business.DEPOSIT;
}
else if(randomNumber>rate1 && randomNumber<=rate1+rate2){
return Business.WITH_DRAWAL;
}else if(randomNumber>rate1+rate2 && randomNumber<=rate1+rate2+rate3){
return Business.PAY_FINE;
}else if(randomNumber>rate1+rate2+rate3 && randomNumber<=rate1+rate2+rate3+rate4){
return Business.OPEN_ONLINE_BANKING;
}else if(randomNumber>rate1+rate2+rate3+rate4 && randomNumber<=rate1+rate2+rate3+rate4+rate5){
return Business.PAY_WATER_AND_ELECTRICITY_BILLS;
}else if(randomNumber>rate1+rate2+rate3+rate4+rate5 && randomNumber<=rate1+rate2+rate3+rate4+rate5+rate6){
return Business.PURCHASE_FUND;
}else if(randomNumber>rate1+rate2+rate3+rate4+rate5+rate6 && randomNumber<=rate1+rate2+rate3+rate4+rate5+rate6+rate7){
return Business.TRANSFER_REMITTANCE;
}
else {
return Business.INDIVIDUAL_LOAN_REPAYMENT;
}
}
}

时间映射类

由于根据要求,我们需要将时间映射到真实时间的早上8点到下午5点,所以创建这个类做时间映射。只需要让时间戳的1毫秒对应到正是情况中的60000毫秒即可做到1毫秒对应1分钟。(要求里说的基础时间在这里设置为5毫秒)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class TimeChange {
static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
static long rate = 60*1000;
private long openTime;
private Calendar calendar = Calendar.getInstance();

public TimeChange(long openTime) {
this.openTime = openTime;
calendar.set(Calendar.HOUR_OF_DAY, 8);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
}

public String timeMap(long mapTime) {
long subtract = mapTime - openTime;
long resultTime = calendar.getTimeInMillis()+subtract*rate;
return sdf.format(new Date(resultTime));
}
}

线程类

bank类(主类)

创建了一个bank类,在里面设置了很多静态变量,具体的变量是如何定义如何使用的,接下来用到了再说。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Bank {
static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
static ArrayList<Customer> globalCustomQueue = new ArrayList<Customer>();
static ArrayList<Customer> preCustomer = new ArrayList<>();
static ArrayList<Window> windowsList = new ArrayList<>();
/**
* baseTime会映射为真实的5分钟,也就是1ms对应1分钟
*/
static double baseTime = 5;
static double vipRate = 0.1;
static double totalCustom = 150;
static int flag = -1;
static ArrayList<Content> contents = new ArrayList<>();
static HashMap<Business, Integer> businessCountMap = new HashMap<>();
static long openTime;
static long closeTime;
static long needWorkTime = 540;
static TimeChange timeChange;
static RateRandom rateRandom;

首先需要一个初始化顾客以及初始化窗口的函数。在静态变量中关于顾客的有两个,第一个是preCustomer,这个变量用来存放所有初始化完成的顾客,也就是说顾客最开始会所在这个ArrayLIst中,等于说是把这些顾客放在了一个顾客池里,需要一个顾客就从这里拿一个出来。第二个是globalCustomQueue,这个模拟了银行大厅的等待队列,所有从顾客池中拿出来的顾客都会被塞入这个等待队列。

顾客初始化后就是用Collection的shuffle进行一次顾客池数组的打乱

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 按照比例创建所有的一般用户以及Vip用户,添加到预先数组preCustomer中,最后用shuffle进行顺序打乱
*/
public void makeCustomer() {
for (int i = 0; i < (int)(totalCustom*(1-vipRate)); i++) {
preCustomer.add(new Customer(i, "顾客" + i, 0,rateRandom.getRandomBusiness()));
}
for (int i = (int)(totalCustom*(1-vipRate)); i < totalCustom; i++) {
preCustomer.add(new Customer(i, "顾客" + i, 1,rateRandom.getRandomBusiness()));
}
Collections.shuffle(preCustomer);
}

顾客初始化完成后下一步就是初始化银行窗口,一共四个窗口,我就直接按顺序创建就可以了。

1
2
3
4
5
6
7
8
9
/**
* 创建银行窗口,创建四个窗口 V A B B
*/
public void makeWindow() {
windowsList.add(new Window(0, "V", new ArrayList<Integer>(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8))));
windowsList.add(new Window(1, "A", new ArrayList<Integer>(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8))));
windowsList.add(new Window(2, "B", new ArrayList<Integer>(Arrays.asList(1, 2, 4, 5, 7))));
windowsList.add(new Window(3, "B", new ArrayList<Integer>(Arrays.asList(1, 2, 4, 5, 7))));
}

顾客到达线程

接下来是模拟顾客到达,我创建了一个静态类CustomerComing继承了Runnable接口编程一个线程。在这个线程所继承的run函数中,不断的从顾客池中拿取顾客,然后放置到等待队列里,当然这里还做了一个判断,这个判断。在银行开门的时候会初始化这个openTime,当我们想要拿顾客之前需要看下当前时间是否和openTime相差了540毫秒(540毫秒就对应了早上8点到下午5点),如果超过540毫秒了就跳出循环,说明要下班了,不能再来顾客了。这里bank类的静态变量flag就标志着是否下班。当然在我们从顾客池获取顾客并加入全局队列的时候还需要使用同步锁,防止其他线程的干扰。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
static class CustomerComing implements Runnable {
private int customerSize;
private ArrayList<Customer> customerList;
private final ArrayList<Customer> globalCustomQueue;

public CustomerComing(int customerSize, ArrayList<Customer> customerList, ArrayList<Customer> globalCustomQueue) {
this.customerSize = customerSize;
this.customerList = customerList;
this.globalCustomQueue = globalCustomQueue;
}
@Override
public void run() {
for (int i = 0; i < this.customerSize; i++) {
try {
closeTime = System.currentTimeMillis();
if (closeTime - openTime >= needWorkTime) {
synchronized (globalCustomQueue){
System.out.println("排队的还有"+globalCustomQueue.size()+"位客人");
}
break;
}
customerList.get(i).setArriveTime(System.currentTimeMillis());
synchronized (globalCustomQueue) {
globalCustomQueue.add(customerList.get(i));
}
Thread.sleep(new Random().nextInt((int) Bank.baseTime * 2));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Bank.flag = 1;
}
}

窗口模拟服务线程

现在顾客模拟的线程已经完成了,下一步就是窗口线程的模拟。

窗口线程也和顾客到达线程一样,都继承了Runable的接口,都需要实现run这个函数,但在完成这个函数之前,先实现另外两个方法

1
2
3
4
5
6
7
8
9
10
11
12
static class WindowServing implements Runnable {
private final Window serveWindow;
private final double baseTime;
private final ArrayList<Customer> globalCustomQueue;

WindowServing(Window serveWindow, double baseTime, ArrayList<Customer> globalCustomQueue) {
this.serveWindow = serveWindow;
this.baseTime = baseTime;
this.globalCustomQueue = globalCustomQueue;
}

}

第一个方法用来读取当前银行的队列是否为空,由于判断的时候也会被其他线程干扰,所以这里还需要同步锁加一层。

1
2
3
4
5
6
7
8
9
static class WindowServing implements Runnable {
.......
public Boolean readQueue() {
synchronized (globalCustomQueue) {
return globalCustomQueue.isEmpty();
}
}
.....
}

另一个方法就是从全局队列获取顾客,在这个getCustomerFromQueue函数中,我们直接在最外层套上一个同步锁,然后首先再进行一次队列是否为空的判断,因为有可能这个线程在run中判断到线程不为空,但当他想获取顾客的时候,队列中的顾客又已经被别的窗口取走了,为了避免发生这种情况,这里还是需要加一个判断。

然后当这个队列不为空的时候就可以根据当前的这个窗口类型对顾客进行不同的挑选。

  • 如果银行的窗口名称为A类窗口,就直接从顾客等待队列里拿第一个数据并移除
  • 如果银行的窗口名称为B类窗口,遍历每一个等待队列中的顾客,如果某个顾客办理的业务B窗口无法执行就跳过这个顾客
  • 如果银行的窗口名称为V类窗口,遍历每一个等待队列中的顾客,如果顾客是VIP(priority = 0),就优先返回这个顾客并移除队列,如果没有直接取第一个
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public Customer getCustomerFromQueue() {
synchronized (globalCustomQueue) {
if (!readQueue()) {
if ("A".equals(this.serveWindow.getName())) {
Customer res = globalCustomQueue.get(0);
globalCustomQueue.remove(0);
return res;
} else if ("B".equals(this.serveWindow.getName())) {
for (int i = 0; i < globalCustomQueue.size(); i++) {
Business customer = globalCustomQueue.get(i).getBusiness();
if (customer == Business.PAY_FINE || customer == Business.PURCHASE_FUND
|| customer == Business.INDIVIDUAL_LOAN_REPAYMENT) {
continue;
} else {
Customer res = globalCustomQueue.get(i);
globalCustomQueue.remove(i);
return res;
}
}
return null;
} else if ("V".equals(this.serveWindow.getName())) {
for (int i = 0; i < globalCustomQueue.size(); i++) {
if (globalCustomQueue.get(i).getPriority() == 1) {
Customer res = globalCustomQueue.get(i);
globalCustomQueue.remove(i);
return res;
}
}
Customer res = globalCustomQueue.get(0);
globalCustomQueue.remove(0);
return res;
}
} else {
return null;
}
}
return null;
}

最后再来实现run方法

每个窗口都会运行这个run方法,run方法中会有一个while循环不断的运行,只有当当前银行的排队队列为空而且在顾客到达线程中将flag设置为1表示关门后,各个窗口才会正常关闭。

在正常运行的情况中,窗口调用刚才的getCustomerFromQueue,获取到一个顾客,当然如果没有获取到getCustomerFromQueue会返回null,我们就判断如果拿到的顾客是null就重新运行这个循环。当我们拿到顾客后就可以根据这个顾客需要的业务进行服务,使用Thread.sleep的方法模拟业务办理,办理成功后就记录这一次服务信息添加到全局变量的contents。

在日志信息添加完成后这里还对哈希表进行了一个更新,这个表使用来最后统计今天这一天银行服务过的各个业务类型占比的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Override
public void run() {
System.out.println("窗口" + this.serveWindow.getName() + "窗口id" + this.serveWindow.getId() + "开启");
while (true) {
if (readQueue()) {
if (Bank.flag == 1) {
break;
}
} else {
Customer customer = getCustomerFromQueue();
if (customer == null) {
continue;
}
try {
double low = customer.getBusiness().getLowTimeProportion();
double high = customer.getBusiness().getHighTimeProportion();
int serveTime = new Random().nextInt((int) (baseTime * (high - low))) + (int) (baseTime * low);
Thread.sleep(serveTime);
customer.setFinishTime(System.currentTimeMillis());
customer.setWaitTime(customer.getFinishTime()-customer.getArriveTime()-serveTime);
synchronized (Bank.contents) {
Bank.contents.add(new Content(customer.getName(), customer.getPriority(), customer.getArriveTime(),customer.getFinishTime(),customer.getWaitTime(), customer.getBusiness(), serveTime, this.serveWindow));
if (Bank.businessCountMap.get(customer.getBusiness()) == null) {
Bank.businessCountMap.put(customer.getBusiness(), 1);
} else {
int cnt = Bank.businessCountMap.get(customer.getBusiness());
Bank.businessCountMap.put(customer.getBusiness(), cnt + 1);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Thread.yield();
}
System.out.println("窗口" + this.serveWindow.getName() + "窗口id" + this.serveWindow.getId() + "关闭");
}

平均占比与平均办理时间

这一部分是在银行的flag变量变为1(关门)并且等待队伍中没有顾客窗口也全部关闭的情况下进行数据输出。

首先是输出办理每个业务的比例,这个比例在之前每个窗口服务完一个顾客后会维护一个哈希表,直接拿哈希表中的数据进行运算,简单的进行求和做除法就能得到。

1
2
3
4
5
6
7
8
9
10
11
public void outputRateOfBusiness() {
System.out.println("不同业务在所有办理业务中所占的比例为:");
for (Business b : Business.values()) {
try {
double rate = Bank.businessCountMap.get(b) * 1.0 / Bank.contents.size() * 100;
System.out.printf("%s:%.3f%%\n", b.getName(), rate);
} catch (NullPointerException e) {
System.out.printf("%s:%.3f%%\n", b.getName(), 0.0);
}
}
}

然后是输出客户的平均办理时间,从日志中把所有顾客的完成时间减去到达时间,最后除以日志数量即可

1
2
3
4
5
6
7
public void outputCustomAvgServeTime(){
long sum=0;
for (Content content : contents) {
sum = sum + (content.getFinishTime() - content.getArriveTime());
}
System.out.printf("平均办理时间:%.2fmin\n",sum*1.0/contents.size());
}

开启银行

最后就是bank类的主函数,开启银行(银行开门!!)

首先可以设置rateRandom来设置各个业务类型的预设占比,然后会获取当前时间戳更新银行开启时间(之后的时间操作都以这个开启时间为基准)。接下来调用makeCustomermakeWindow来初始化顾客和窗口。最后创建线程池,将顾客到达线程和四个窗口线程加入线程池运行线程。

程序不断的运行,在最后会有一个whileTrue不断的判断线程池中的线程是否都结束,当完全结束了,表明银行正式关门,然后根据日志的各个成员变量输出银行一天的日志信息,最后再输出一下今日业务的平均占比以及顾客的平均办理时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public void bankOpen() {
System.out.println("银行开门");
// 正常日
// rateRandom = new RateRandom();
// 基准日A
// rateRandom = new RateRandom(0.2,0.2,0.1,0.1,0.05,0.15,0.10,0.10);
// 营业日B
rateRandom = new RateRandom(0.1,0.1,0.05,0.05,0.05,0.40,0.05,0.20);
System.out.println("working.....");
openTime = System.currentTimeMillis();
timeChange = new TimeChange(openTime);
this.makeCustomer();
this.makeWindow();
ExecutorService executorService = Executors.newFixedThreadPool(10);
executorService.execute(new CustomerComing(preCustomer.size(), preCustomer, globalCustomQueue));
for (int i = 0; i < 4; i++) {
executorService.execute(new WindowServing(windowsList.get(i), baseTime, globalCustomQueue));
}
executorService.shutdown();
while (true) {
if (executorService.isTerminated()) {
System.out.println("银行关门啦,汇报进度");
System.out.println("一天的顾客日志如下");
System.out.println("一共服务了:" + contents.size() + "名顾客");
System.out.printf("%-8s %-10s %-15s %-12s %-15s %-12s %-15s %-10s\n",
"客户名称", "是否为vip", "客户到达时间", "客户办理业务类型", "客户所需时间(分钟)", "服务窗口","顾客完成时间","顾客等待时间");
for (Content c : contents) {
System.out.printf("%-10s %-9s %-22s %-17s %-18s %-12s %-22s %-10s\n",
c.getCustomerName(), c.getPriority(), timeChange.timeMap(c.getArriveTime()),
c.getBusinessType().getName(), c.getUseTime(),
c.getServeWindow().getName() + c.getServeWindow().getId(),
timeChange.timeMap(c.getFinishTime()),c.getWaitTime());
}
outputCustomAvgServeTime();
outputRateOfBusiness();
break;
}
}
System.out.println("汇报完毕,正式关门");
}

运行结果

银行日志比较长,这里就不截完整了

完整项目戳这里




======================
全 文 结 束    感 谢 阅 读
======================