3.2 Monkey线程的run()的执行流程图···· 1
3.3 至少两种“梯子选择”策略的设计与实现方案···· 2
3.8.2 使用Strategy模式为每只猴子随机选择决策策略··· 2
3.9.1 对比分析:固定其他参数,选择不同的决策策略···· 3
3.9.2 对比分析:变化某个参数,固定其他参数···· 3
3.9.3 分析:吞吐率是否与各参数/决策策略有相关性?···· 3
本次实验训练学生的并行编程的基本能力,特别是 Java 多线程编程的能力。
根据一个具体需求,开发两个版本的模拟器,仔细选择保证线程安全(threadsafe)
的构造策略并在代码中加以实现,通过实际数据模拟,测试程序是否是线程安全
的。另外,训练学生如何在 threadsafe 和性能之间寻求较优的折中,为此计算吞
吐率和公平性等性能指标,并做仿真实验。
⚫ Java 多线程编程
⚫ 面向线程安全的 ADT 设计策略选择、文档化
⚫ 模拟仿真实验与对比分析
简要陈述你配置本次实验所需环境的过程,必要时可以给出屏幕截图。
特别是要记录配置过程中遇到的问题和困难,以及如何解决的。
实验环境设置请参见 Lab-0 实验指南。
本次实验在 GitHub Classroom 中的 URL 地址为:
https://github.com/ComputerScienceHIT/Lab6-1170300724
请仔细对照实验手册,针对三个问题中的每一项任务,在下面各节中记录你的实验过程、阐述你的设计思路和问题求解思路,可辅之以示意图或关键源代码加以说明(但千万不要把你的源代码全部粘贴过来!)。
-
- ADT设计方案
新建了一个包,里面放adt类。
设计了三个类,其中Monkey中包含一个内部类。
Kuan.java
对num有设置和访问的操作,代表的是梯子上的板子。
Ladder.java
每个梯子中,有各自的板子list,方法中有对自己板子的初始化,主要是给板子加上编号,方法主要分为两种,一种是判断是否有猴子在板子的特殊位置,另一种是获取各种属性,整体长度,第几个板子,和这个梯子的编号。
判断位置包括是否有猴子,是否有向某个方向的猴子,是否在开始位置,也就是第一个板子和最后一个板子是否有猴子。还有一个方法是设置梯子的编号。
里面有整个梯子的list,方便调用各个梯子。
Monkey.java
猴子类,包含了内部类state,和速度,过河策略的编号。方法中有开始猴子过河线程,初始化猴子(设置猴子id,猴子速度,猴子方向),在策略方案给出梯子之后上梯子的初始位置,去到下一个位置(同时判断前面是否有猴子,具体能到哪里),输出猴子的状态(主要是写日志)。
内部类MonkeyState
记录出生时间(好像没用上),生存时间,id,在哪个位置上(梯子的哪块板子上),这里的state是过河状态(1表示在岸上,2表示在梯子上,3表示到达对岸),方法有各种设置和访问的函数,还有增加生存时间和更改过河状态。
具体说一下开始猴子线程和上梯子和下一步
public void startCrossRiverThread()
ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
service.scheduleAtFixedRate
利用service实现线程开始。
先对猴子的过河状态进行判断,2和3比较简单,2就是下一步然后增加生存时间和写日志,3就是结束,写日志。1如下(省略写日志的地方)
else if (state.getstate() == 1) {
if (this.getcrossStrategy() == 1) {
Strategy1 stra = new Strategy1();
stra.setdirection(this.state.direction);
while (stra.getresult() == 0) {
stra.run();
// 正在左(右)岸等待,离出生已?秒
}
if (stra.getresult() == 0) {
state.live();
}
this.Jump2Ladder(stra.getresult());
}
}
判断猴子的策略是多少,然后用策略给出该上哪个板子,其中策略1需要猴子的方向,所以需要先把方向传进去。如果策略的结果是0,说明没有找到合适的板子,需要等待,等待的1秒写在了策略里,所以只需要判断是否为0即可。获得板子之后就可以调用上板子的函数了。
public void Jump2Ladder(int labberm)
synchronized (Ladder.Ladderall)利用锁保证线程安全
上板子先判断猴子走的方向,不同方向上的板子初始位置不同,给出一个方向的代码(省略写日志)
if (this.state.getdirection() == "LR") {
while (Ladder.Ladderall.get(labberm - 1).fmonkeyonRL()) {
try {
Ladder.Ladderall.wait(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Strategy1 stra = new Strategy1();
stra.setdirection(this.state.direction);
while (stra.getresult() == 0) {
stra.run();
if (stra.getresult() == 0) {
state.live();
}
labberm = stra.getresult();
}
}
this.state.setLocation(Ladder.Ladderall.get(labberm - 1),
Ladder.Ladderall.get(labberm - 1).getKuanLR(1));
this.state.nextstate();
}
Labberm是要上的板子的编号,为了保证安全,所以在上板子的地方又加了一次判断,保证不会出现死锁状况。(之后又进行了修改,增加了判断条件,还防止了起始位置有许多猴子的状况,但是太长了不适合粘贴上来)
public void nextStep()
synchronized (monkeylist)保证线程安全
还是,两种方向的写法差不多,只给一边的没日志的代码
Ladder l = new Ladder();
l = this.state.getladder();
for (i = 1; i <= l.getKuanlength(); i++) {
if (l.getKuanLR(i) == this.state.getkuan()) {
break;
}
}
// 1 2 3 4 5 6 7 8 9 10
if (this.state.getdirection() == "LR") {
for (int c = 0; c < MonkeyGenerator.monkeyall.size(); c++) {
if (MonkeyGenerator.monkeyall.get(c).state.getladder() == this.state.getladder()) {
monkeylist.add(MonkeyGenerator.monkeyall.get(c));
}
}
monkeylist.remove(this);
for (int x = 1; x <= this.v; x++) {
for (int c = 0; c < monkeylist.size(); c++) {
if (monkeylist.get(c).state.getkuan().getnum() == this.state.getkuan().getnum() + x) {
this.state.setLocation(l, l.getKuanLR(i + x - 1));
flag = 0;
break;
}
}
if (flag == 0) {
break;
}
}
if (flag == 1) {
this.state.setLocation(l, l.getKuanLR(i + this.v));
}
if (this.state.getkuan().getnum() > Ladder.hlength) {
this.state.nextstate();
Ladder ladder = new Ladder();
ladder.setLaddernum(-1);
this.state.setLocation(ladder, this.state.getkuan());
}
monkeylist.clear();
}
首先找到这个猴子现在在哪(其实不用遍历,因为内部类有这个方法,但是忘了,也懒得改了),知道方向之后,找这个梯子上,在他前面的猴子在哪,根据猴子的速度,如果前面有猴子会挡着它(在他速度覆盖的板子之中),就会到这个猴子未行动之前的位置的后面一个板子上。函数有利用锁每秒运行的方法。不写出来了。
-
- Monkey线程的run()的执行流程图
。
优先选择没有猴子的梯子,若所有梯子上都有猴子,则优先
选择没有与我对向而行的猴子的梯子;若满足该条件的梯子有很多,
则随机选择;
采用了指导书的方案2,完成了。代码太长了,写一部分不重复的。
synchronized (Ladder.Ladderall) {
for (int i = 0; i < Ladder.Ladderall.size(); i++) {
if (!Ladder.Ladderall.get(i).fmonkeyon()) {
list.add(Ladder.Ladderall.get(i));
}
}
if (list.size() == 0 && this.result == 0) {
if (direction == "LR") {
for (int k = 0; k < Ladder.Ladderall.size(); k++) {
if (Ladder.Ladderall.get(k).fmonkeyonLR()) {
list2.add(Ladder.Ladderall.get(k));
}
}
int temp = list2.size();
if (temp == 0) {
} else {
int tempm = (int) (Math.random() * (temp));
this.result = list2.get(tempm).getLaddernum();
}
} else {
for (int k = 0; k < Ladder.Ladderall.size(); k++) {
if (Ladder.Ladderall.get(k).fmonkeyonRL()) {
list2.add(Ladder.Ladderall.get(k));
}
}
int temp = list2.size();
if (temp == 0) {
} else {
int tempm = (int) (Math.random() * (temp));
this.result = list2.get(tempm).getLaddernum();
}
}
这是最长的一部分,list是一个包含着空梯子的表,把当时的空梯子放进来,根据这个list的大小,来给出不同的结果,这部分是没有空梯子的情况,没有空梯子就用list2,找到同向的梯子,然后随机上一个同向的梯子,如果没找到结果,要等待1秒后再进行判断。
如果list大小只有1,那就返回这个梯子编号就行
如果list大小大于1,就随机选择一个在里面的梯子编号返回。
if (this.result == 0) {
try {
Ladder.Ladderall.wait(1000);
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
以防万一在最后加了判断,如果这个函数运行一遍还没有找到,就等待1秒后再找。
-
-
- 策略2
-
优先选择没有猴子的梯子,若所有梯子上都有猴子,则在岸
边等待,直到某个梯子空闲出来;
public void run() {
List<Ladder> list = new ArrayList<Ladder>();
synchronized (Ladder.Ladderall) {
for (int i = 0; i < Ladder.Ladderall.size(); i++) {
if (!Ladder.Ladderall.get(i).fmonkeyon()) {
list.add(Ladder.Ladderall.get(i));
}
}
if (list.size() == 0) {
this.result = 0;
} else {
int temp = list.size();
int tempm = (int) (Math.random() * (temp));
this.result = list.get(tempm).getLaddernum();
}
if (this.result == 0) {
try {
Ladder.Ladderall.wait(1000);
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
}
}
还是利用list的大小来判断是否有空梯子
public void run() {
for (int i = 0; i < K; i++) {
Monkey monkey = new Monkey();
monkey.state.setid(id);
id++;
temp = (int) (Math.random() * (1 + 1));
if (temp == 0) {
monkey.state.setdirection("LR");
} else {
monkey.state.setdirection("RL");
}
temp = (int) (1 + Math.random() * (mv - 1 + 1));
monkey.setv(temp);
temp = (int) (1 + Math.random() * (3 - 1 + 1));
monkey.setcrossStrategy(1);
monkey.state.nextstate();
monkeyall.add(monkey);
monkey.startCrossRiverThread();
}
这里是单一策略的猴子生成器,就是new一个猴子加到整体猴子列表里面。
Timer timer3 = new Timer();
timer3.schedule(new TimerTask() {
public void run() {
monkeyGenerator.run();
if (MonkeyGenerator.monkeyall.size() >= N) {
timer3.cancel();
}
}
}, 0, 1000);
在主函数这样调用,实现每秒生成k个猴子的功能
。
-
- 如何确保threadsafe?
在不安全的线程中加入了锁,在adt里面说了
在上梯子时多加了一个判断,避免出现不知道为什么的bug
if (this.state.getdirection() == "LR") {
while (Ladder.Ladderall.get(labberm - 1).fmonkeyonRL() ||Ladder.Ladderall.get(labberm - 1).fmonkeyonLRB() ) {
try {
Ladder.Ladderall.wait(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Strategy1 stra = new Strategy1();
stra.setdirection(this.state.direction);
while (stra.getresult() == 0) {
stra.run();
if (stra.getresult() == 0) {
state.live();
}
labberm = stra.getresult();
}
}
Timer lol = new Timer();
lol.schedule(new TimerTask() {
public void run() {
int flag = 1;
if (MonkeyGenerator.monkeyall.size() >= N - 1) {
for (int i = 0; i < MonkeyGenerator.monkeyall.size(); i++) {
flag = 1;
if (MonkeyGenerator.monkeyall.get(i).state.getstate() != 3) {
flag = 0;
break;
}
}
if (flag == 1) {
long endTime = System.currentTimeMillis(); // 获取结束时间
System.out.println((double) N / ((double) (endTime - startTime) / 1000));
double temp = 0;
for(int i = 0 ; i<MonkeyGenerator.monkeyall.size() ; i++)
{
for(int k = i + 1 ;k<MonkeyGenerator.monkeyall.size();k++)
{
if((MonkeyGenerator.monkeyall.get(k).state.getbeartime() - MonkeyGenerator.monkeyall.get(i).state.getbeartime() ) * (MonkeyGenerator.monkeyall.get(k).state.getlifetime() - MonkeyGenerator.monkeyall.get(i).state.getlifetime()) >=0) {
temp = temp + 1;
}
else {
temp = temp -1;
}
}
}
double tempp = 1;
tempp = N * (N-1) / 2;
System.out.println(temp/tempp);
lol.cancel();
}
}
}
}, 20000, 1000);
利用timer给吞吐率和公平值一个启动时间,尽量别让他们一直在那判断,20s只是针对了一个情况,可以更改。利用指导书上的算法可以很容易的写出计算的代码。
日志:
使用了自己设计一个log类来实现类似log的方法。
自己写的log可以存储String,打印,和写入一个文件之中。
Log.txt写在了根目录里面
GUI:
利用了table生成梯子,虽然难看了一点,但是还凑合能用。
刷新部分是查的网上的方法,就是重写repaint函数,把gui的函数整个复制进去就行,然后在主函数里面利用timer不停的每秒调用一次。为了方便看,没有设置自动关闭。
public Guig() {
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setBounds(100, 100, 1500, 1500);
contentPane = new JPanel();
contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
contentPane.setLayout(new BorderLayout(0, 0));
setContentPane(contentPane);
JPanel panel = new JPanel();
contentPane.add(panel, BorderLayout.CENTER);
String[] columnNames = new String[Ladder.hlength];
for (int i = 0; i < Ladder.hlength; i++) {
columnNames[i] = " ";
}
Object[][] obj = new Object[Ladder.Laddern][Ladder.hlength];
for (int i = 0; i < MonkeyGenerator.monkeyall.size(); i++) {
if (MonkeyGenerator.monkeyall.get(i).state.getladder().getLaddernum() != -1
&& MonkeyGenerator.monkeyall.get(i).state.getladder().getLaddernum() != 0) {
int templ = MonkeyGenerator.monkeyall.get(i).state.getladder().getLaddernum();
int temph = MonkeyGenerator.monkeyall.get(i).state.getkuan().getnum();
obj[templ - 1][temph - 1] = "█" + MonkeyGenerator.monkeyall.get(i).state.getid();
}
}
JTable table = new JTable(obj, columnNames);
TableColumn column = null;
int colunms = table.getColumnCount();
for (int i = 0; i < colunms; i++) {
column = table.getColumnModel().getColumn(i);
column.setPreferredWidth(50);
}
table.setRowHeight(50);
panel.add(table);
}
)
利用了设置文件,在根目录下创建了一个In.txt,在里面写上配制的参数(只能配制指导书上面的几个)格式如下
每行写一个,不能有空格
-
-
- 使用Strategy模式为每只猴子选择决策策略
-
在adt中和猴子生成器说过了,随机一个数,根据数字来选择策略
-
- 猴子过河模拟器v2
在不同参数设置和不同“梯子选择”模式下的“吞吐率”和“公平性”实验结果及其对比分析。
策略1明显比策略2快,
策略1:
策略2:
改变n:
n=5:
n=10:
改变t和k差不多,就改一个吧:
t=1
t=5
改变MV
Mv = 5
Mv=15
-
-
- 分析:吞吐率是否与各参数/决策策略有相关性?
-
有关系有关系,最大速度肯定是有关系的,速度越快吞吐率相对就大,梯子多,可以上的猴子就多,吞吐率也相对就大,生成器生成的速度太慢的话,吞吐率肯定就小。
策略上来说,1,2策略相差肯定比较明显,肯定是更好的策略吞吐率越大。
结果在上面,分析也在上面
-
- 猴子过河模拟器v3
针对教师提供的三个文本文件,分别进行多次模拟,记录模拟结果。
| 吞吐率 | 公平性 |
Competiton_1.txt |
|
|
第1次模拟 | 2.9235206985265454 | 0.22831661092530658 |
第2次模拟 | 2.7726432532347505 | 0.24749163879598662 |
第3次模拟 | 3.0137426665595113 | 0.29926421404682274 |
第4次模拟 | 2.381103562130929 | 0.0611371237458194 |
第5次模拟 | 2.8790786948176583 | 0.18581939799331104 |
第6次模拟 | 2.748460861917326 | 0.19304347826086957 |
第7次模拟 | 2.9235206985265454 | 0.3435897435897436 |
第8次模拟 | 2.8766492789199143 | 0.22363433667781493 |
第9次模拟 | 3.0930386011217417 | 0.39852842809364547 |
第10次模拟 | 2.9232927970065483 | 0.2923076923076923 |
平均值 | 2.853505 | 0.247313 |
Competiton_2.txt |
|
|
第1次模拟 | 4.835964097802538 | 0.11092585170340681 |
第2次模拟 | 4.908119993717606 | -0.035559118236472946 |
第3次模拟 | 4.952064020283654 | -0.0023567134268537076 |
第4次模拟 | 5.028562233486202 | 0.06678957915831664 |
第5次模拟 | 5.030990903968446 | -0.062364729458917835 |
第6次模拟 | 5.029776275551264 | 0.06678957915831664 |
第7次模拟 | 4.991215460789011 | -0.009811623246492986 |
第8次模拟 | 4.948927072610658 | -0.0631503006012024 |
第9次模拟 | 4.989621587098834 | -0.01769939879759519 |
第10次模拟 | 5.113729340533465 | -0.050180360721442885 |
平均值 | 4.982897 | 0.000338 |
Competiton_3.txt |
|
|
第1次模拟 | 1.3882718791648156 | 0.3236363636363636 |
第2次模拟 | 1.2240501370936154 | 0.05333333333333334 |
第3次模拟 | 1.3882718791648156 | 0.1995959595959596 |
第4次模拟 | 1.235299930823204 | 0.08363636363636363 |
第5次模拟 | 1.3579576317218902 | 0.2694949494949495 |
第6次模拟 | 1.2740801141575782 | 0.05333333333333334 |
第7次模拟 | 1.189117199391172 | 0.07474747474747474 |
第8次模拟 | 1.1147774904129137 | 0.05333333333333334 |
第9次模拟 | 1.3878094815143778 | 0.298989898989899 |
第10次模拟 | 1.3882718791648156 | 0.26464646464646463 |
平均值 | 1.294791 | 0.167475 |
- 实验进度记录
请使用表格方式记录你的进度情况,以超过半小时的连续编程时间为一行。
每次结束编程时,请向该表格中增加一行。不要事后胡乱填写。
不要嫌烦,该表格可帮助你汇总你在每个任务上付出的时间和精力,发现自己不擅长的任务,后续有意识的弥补。
日期 | 时间段 | 计划任务 | 实际完成情况 |
2019.6.10 | 全体 | V1 | 完成 |
2019.6.12 | 全天 | V2 | 完成 |
2019.6.16 | 全天 | V3 | 完成 |
遇到的难点 | 解决途径 |
GUI不会刷新
| 问了一下同学说是要重写repaint |
猴子经常不听话
| 好多for循环忘了加break导致出现问题 |
- 多线程程序比单线程程序复杂在哪里?你是否能体验到多线程程序在性能方面的改善?
多线程不好保证判断条件,可能会出现这个线程在做判断的时候,另外一个线程可能动了,然后导致判断条件出现问题。多线程程序可以同时运行多个函数,比以往的从上至下运行好多了。
- 你采用了什么设计决策来保证threadsafe?如何做到在threadsafe和性能之间很好的折中?
锁,和额外的判断次数,虽然牺牲了性能,但是如果线程不安全那性能就等于0,所以我觉得优先保证线程安全是最重要的。
- 你在完成本实验过程中是否遇到过线程不安全的情况?你是如何改进的?
遇到了,感觉锁没有那么好使,所以多加了一次判断是否正确的情况
- 关于本实验的工作量、难度、deadline。
都挺好,时间也不紧。
- 到此为止你对《软件构造》课程的意见和建议。
木了,反正最后一次了。