1 实验目标概述
本次实验训练学生的并行编程的基本能力,特别是 Java 多线程编程的能力。 根据一个具体需求,开发两个版本的模拟器,仔细选择保证线程安全(threadsafe) 的构造策略并在代码中加以实现,通过实际数据模拟,测试程序是否是线程安全 的。另外,训练学生如何在threadsafe和性能之间寻求较优的折中,为此计算吞 吐率和公平性等性能指标,并做仿真实验。
- Java多线程编程
- 面向线程安全的 ADT 设计策略选择、文档化
- 模拟仿真实验与对比分析
2 实验环境配置
操作系统:macOS Mojave 10.14.5
硬件环境:CPU:Intel Core i7-7920HQ@3.1GHz
RAM:16GB LPDDR3
开发、测试、运行环境:IntelliJ IDEA Ultimate 2019.1.3,Oricle JDK 11.0.3
GitHub Lab_6 URL:unavailable
3 实验过程
3.1 ADT设计方案
ADT只有Monkey
和Ladder
。
3.1.1 Monkey
/** Monkey ID. */
private final int id;
/** Direction. */
private final String direction;
/** Velocity. */
private final int velocity;
/** Generated turn. */
private final int startTurn;
对于可以直接阅读的内容,介绍省略。其中startTurn
记录了猴子生成的回合(时间),便于后面计算公平度。Monkey
是immutable ADT。
getter
方法介绍省略;
重载了equals
和hashCode
, toString
。
3.1.2 Ladder
Ladder
是最重要的部分之一,承载了大多核心功能的实现。
/** ID. */
private final int id;
/** Length. */
private final int length;
/** Current access Direction. */
private String direction;
/** Monkeys on current ladder. */
private Monkey[] monkeys;
对于梯子,使用了本身存储方向的设计。
direction
代表梯子的方向,没有猴子时设为null
,有一只猴子时,方向设置为该猴子的方向。
其中monkeys记录了当前梯子中猴子的位置,数组长度代表梯子长度,null
代表当前位置没有猴子。
getter
方法省略。
重载了equals
和hashCode
, toString
。
主要方法如下:
public void addMonkey(final Monkey monkey)// 添加猴子,在monkeys初始位置设置猴子,初始位置根据另外的方法,自动判断方向,计算初始位置,最后根据当前状态更新梯子方向
public void remove(final Monkey monkey)//移除猴子,遍历数组,找到猴子后设置为null,并根据当前状态更新梯子方向
private int getMonkeyNumber()//得到猴子数量,遍历数组即可
public int getPosition(final Monkey monkey) //得到猴子位置,本质是index(数组中位置) + 1
public void move(final Monkey monkey, final int offset)//重要方法,辅助移动猴子,根据偏移量offset,自动判断方向,移动相应位置
private void changeAccess()//辅助方法,自动更换梯子方向,没有猴子时设为null,有一只猴子时,方向设置为该猴子的方向
private int getInitPosition(final Monkey monkey)//得到初始位置,如果方向为L→R则初始位置为1,否则为length
public boolean isAvailable()//判断方法,当没有猴子时,梯子便是available
private boolean hasMonkey(final Monkey monkey)//辅助方法,判断时候存在给定猴子,遍历monkeys数组即可
public int getRealVelocity()//重要方法,计算梯子真实速度,根据方向,计算出最近的两只猴子的距离,并返回后方猴子和距离的最大值,这代表当前新加入的猴子的极限速度
public int indexOf(final Monkey monkey)//重要方法,给出猴子的位置,遍历monkeys数组即可
private void checkRep()//保证每只猴子与梯子速度相同
3.2 Monkey线程的run()的执行流程图
3.3 至少两种“梯子选择”策略的设计与实现方案
3.3.1 策略1
随意选择所有没有被占用的梯子,如果没有,就随意选择相同方向的梯子。
3.3.2 策略2
在所有同方向或未占用梯子中,选择真实速度最大的梯子。
3.3.3 策略3
在所有同方向或未占用梯子中,选择真实速度大于猴子的梯子中速度最小的梯子,如果没有就选择其中真实速度最大的梯子。具体可见代码内注释。
3.4 “猴子生成器”MonkeyGenerator
根据配置参数,有总数,每隔几轮生成猴子,每次生成几只猴子。
在主方法中有计数器,用取余数的方法每隔几轮就可以调用一次生成方法,记录已生成猴子数量,判断是否当前再生成每轮猴子数量后超过总数,如果是,则计算余数,循环该次数,用给定参数和随机数生成猴子,否则循环每轮猴子数量次数。
生成猴子本身只涉及到参数和构造器,由于简单无需介绍。
3.5 如何确保threadsafe?
Cross
线程中,首先在run()
中将该线程设为阻塞状态,给当前线程上锁,在suspend
为true
的情况下,直到在主方法中对每个线程都解除阻塞,解除阻塞就是在相应线程中将suspend
解除。接下来对主类中的LADDERS
上同步锁,防止运行过程中其他线程同时修改造成错误。
在每个猴子移动过程中,参考的状态均是上一回合结束后的状态,使用status
保存该状态,这样就确保了猴子不会采用上帝视角观察其他猴子的行为。如果多个猴子选择一个梯子,则在一个猴子选上该梯子后,其他猴子均不能再进行选择,只能在下一轮再次选择。
3.6 系统吞吐率和公平性的度量方案
对于吞吐率,计算非常简单,只需将总数/总的回合数即可;
对于公平性,在生成每个猴子时记录当前回合数,猴子到对岸时记录总用时,单独存储到另外的status
中。使用参考公式计算,用两次for
循环,可以容易地算出公平度。
3.7 输出方案设计
对于日志,使用了Lab4中的方案,直接套用即可,在每次线程进行操作时,记录操作和状态。存储在application的log.log中。
GUI部分,对于每一轮,先使用line画出梯子(位置由相应参数自动计算),接下来每次画出status
中保存的猴子信息,在相应位置用string画出猴子ID。每一轮都repaint即可。结束时,最终用JOptionPane.showMessageDialog
给出配置参数、吞吐率和公平度。
3.8 猴子过河模拟器v1
3.8.1 参数如何初始化
使用了读取配置文件的方式,初始化6个基本参数。文件读取方法使用了Lab3到Lab5中的方法,每次都用正则表达式进行分析,提取参数。
3.8.2 使用Strategy模式为每只猴子选择决策策略
在线程Cross
中直接使用了3个策略方法,没有使用Strategy design pattern,因为这样做反而不便,首先是策略数量不是很多,其次,使用策略需要获取猴子的信息,这样还需要额外的参数,反而使得程序的可读性和内聚度降低。
3.9 猴子过河模拟器v2
在不同参数设置和不同“梯子选择”模式下的“吞吐率”和“公平性”实验结果及其对比分析。
3.9.1 对比分析:固定其他参数,选择不同的决策策略
使用如下参数测试:
n = 5
h = 20
t = 2
N = 100
k = 5
MV = 10
3.9.1.1 策略1
吞吐率 | 1.5625 | 1.4706 | 1.6949 | 1.5385 | 1.6949 |
---|---|---|---|---|---|
公平性 | 0.5014 | 0.6444 | 0.5010 | 0.2840 | 0.3572 |
3.9.1.2 策略2
吞吐率 | 1.5625 | 1.4706 | 1.6393 | 1.5873 | 1.7857 |
---|---|---|---|---|---|
公平性 | 0.3556 | 0.4857 | 0.6283 | 0.5345 | 0.6251 |
3.9.1.3 策略3
吞吐率 | 1.6393 | 1.6949 | 1.5385 | 1.6667 | 1.5625 |
---|---|---|---|---|---|
公平性 | 0.5366 | 0.5665 | 0.5358 | 0.3661 | 0.6679 |
总对比:
3.9.2 对比分析:变化某个参数,固定其他参数
标准参数:
n = 10
h = 20
t = 2
N = 100
k = 10
MV = 10
3.9.2.1 n
n | 吞吐率 | 公平性 |
---|---|---|
2 | 0.8772 | 0.8917 |
4 | 1.5152 | 0.8428 |
6 | 2.2727 | 0.6590 |
8 | 2.3810 | 0.7325 |
10 | 2.5000 | 0.6263 |
3.9.2.2 t
t | 吞吐率 | 公平性 |
---|---|---|
1 | 2.6316 | 0.8521 |
2 | 2.5641 | 0.7131 |
3 | 2.0408 | 0.6267 |
4 | 2.0000 | 0.3984 |
5 | 1.4925 | 0.3370 |
3.9.2.3 N
N | 吞吐率 | 公平性 |
---|---|---|
100 | 2.5000 | 0.4262 |
200 | 3.3898 | 0.4206 |
300 | 3.7975 | 0.4382 |
400 | 4.0816 | 0.2748 |
500 | 4.1322 | 0.2387 |
3.9.2.4 k
k | 吞吐率 | 公平性 |
---|---|---|
5 | 1.6949 | 0.4352 |
10 | 2.5641 | 0.6646 |
15 | 2.7027 | 0.7463 |
20 | 2.6316 | 0.8598 |
25 | 2.7027 | 0.9236 |
3.9.2.5 MV
MV | 吞吐率 | 公平性 |
---|---|---|
2 1.9231 | 0.7063 | |
4 | 2.4390 | 0.6804 |
6 | 2.5641 | 0.6772 |
8 | 2.7778 | 0.5980 |
10 | 2.4390 | 0.6432 |
3.9.3 分析:吞吐率是否与各参数/决策策略有相关性?
根据测试结果,显然可以发现其中的规律性:例如梯子数增大,则吞吐率增加,公平性降低;产生间隔增大时,吞吐率降低,公平性下降;猴子总数增加,吞吐率随之增大而公平性下降;每次产生猴子数增加,则吞吐率在猴子数较少时增加,通行能力饱和时,吞吐率不再发生明显变化,而公平性随之提升;最大速度增加时,吞吐率随之增加而公平性略有下降。
3.9.4 压力测试结果与分析
3.9.4.1 高密度
标准参数:
n = 5
h = 20
t = 1
N = 100
k = 50
MV = 10
吞吐率 | 1.5625 | 1.4706 | 1.6393 | 1.5385 | 1.5385 |
---|---|---|---|---|---|
公平性 | 0.9939 | 0.9956 | 0.9935 | 0.9927 | 0.9952 |
3.9.4.2 高速度差异
|–|--|–|--|–|--|
吞吐率 | 1.5625 | 1.4706 | 1.6393 | 1.5385 | 1.5385 |
---|---|---|---|---|---|
公平性 | 0.9939 | 0.9956 | 0.9935 | 0.9927 | 0.9952 |
3.10 猴子过河模拟器v3
针对教师提供的三个文本文件,分别进行多次模拟,记录模拟结果。
吞吐率 | 公平性 | |
---|---|---|
Competiton_1.txt | ||
第1次模拟 | 2.0833 | 0.7856 |
第2次模拟 | 2.1429 | 0.8001 |
第3次模拟 | 2.0979 | 0.7996 |
第4次模拟 | 2.0979 | 0.7924 |
第5次模拟 | 2.0548 | 0.8108 |
第6次模拟 | 2.0690 | 0.8080 |
第7次模拟 | 2.0548 | 0.8109 |
第8次模拟 | 2.1127 | 0.7868 |
第9次模拟 | 2.0408 | 0.8257 |
第10次模拟 | 2.0690 | 0.8055 |
平均值 | 2.0823 | 0.8025 |
Competiton_2.txt | ||
第1次模拟 | 4.0984 | 0.7999 |
第2次模拟 | 4.2373 | 0.7970 |
第3次模拟 | 4.2017 | 0.7976 |
第4次模拟 | 4.0650 | 0.7933 |
第5次模拟 | 4.3478 | 0.7803 |
第6次模拟 | 4.0984 | 0.7964 |
第7次模拟 | 4.2017 | 0.8107 |
第8次模拟 | 4.2735 | 0.7776 |
第9次模拟 | 4.1667 | 0.8007 |
第10次模拟 | 4.1322 | 0.7930 |
平均值 | 4.1823 | 0.7947 |
Competiton_3.txt | ||
第1次模拟 | 1.0417 | 0.8008 |
第2次模拟 | 1.0417 | 0.7754 |
第3次模拟 | 1.0526 | 0.7701 |
第4次模拟 | 1.0638 | 0.7895 |
第5次模拟 | 1.0417 | 0.8044 |
第6次模拟 | 1.0638 | 0.7943 |
第7次模拟 | 1.0417 | 0.8004 |
第8次模拟 | 1.0000 | 0.7612 |
第9次模拟 | 1.0417 | 0.7984 |
第10次模拟 | 1.0638 | 0.7952 |
平均值 | 1.0453 | 0.7890 |
4 实验过程中遇到的困难与解决途径
遇到的难点 | 解决途径 |
---|---|
多线程的建立与同步 | 网络搜索与学习 |
猴子移动的具体细节存在争议 | 与其他人讨论 |
6 实验过程中收获的经验、教训、感想
6.1 实验过程中收获的经验和教训
多线程的debug是比较困难的。需要控制好线程同步,并且在此基础上实现具体功能,如果出现了问题需要从底层到上层依此排查,反复测试。有时,在较为简单的情况下,发现不了一些隐藏较深的bug,需要在实际测试过程中设置一些极限情况,便于发现问题。
6.2 针对以下方面的感受
- 多线程程序比单线程程序复杂在哪里?你是否能体验到多线程程序在性能方面的改善?
多线程可以模拟每个物体的一系列动作并能够与其他线程进行交流。复杂之处主要在于它的同步,需要具备一定的执行次序和规则。该实验似乎并不能够体现多线程的性能改善。 - 你采用了什么设计决策来保证threadsafe?如何做到在threadsafe和性能之间很好的折中?
在每次循环时一次激活线程,并在每个线程执行过程中给所有梯子上锁。 - 你在完成本实验过程中是否遇到过线程不安全的情况?你是如何改进的?
没有遇到。 - 关于本实验的工作量、难度、deadline。
工作量相比lab3以后的实验明显减小,难度适中,但是需要深入一些细节。 - 到此为止你对《软件构造》课程的意见和建议。
对Java进行了较为系统的教学,除了网络部分几乎完整覆盖了。