一、从多线程的协同和同步控制方面,分析和总结自己三次作业的设计策略。
-
第一次作业
-
多线程的协同:本次作业共有三个线程和请求队列(托盘),其中
MianTread
线程用于开始电梯线程EleThread
(消费者)和输入线程EleInputThread
(生产者)然后结束,输入线程输入请求至托盘,电梯线程从托盘中取出请求并执行,直至输入线程结束,并且电梯线程通过读取结束信号结束线程。 -
同步控制:电梯线程和输入线程共享请求队列,请求队列设置为线程安全类,主要是采用Synchronize关键字进行方法级别上的同步控制。
-
-
第二次作业:
-
多线程的协同:本次作业相对于上一次作业,主要是在主线程中开始了若干个电梯线程,其他线程协同方面均与上一次作业相同。
-
同步控制:与第一次作业相同。
-
-
第三次作业:
-
多线程的协同:本次作业将主线程当作输入线程、另外还有电梯线程以及请求队列。本次作业要求动态增加电梯,本人的解决方案是,每当读入一个新增电梯请求,主线程会开始新的电梯请求。
-
同步控制:所有的电梯请求和输入请求共享请求队列,对请求队列的数据进行写时上锁,读时不加锁,由此实现线程之间的同步,各个电梯线程从请求队列中读取请求放入各自的请求队列进行调度,通过输入线程修改请求队列的结束信号,各个电梯线程不断判断结束信号是否被修改,并结合自身调度情况判断是否结束。
-
二、从功能设计与性能设计的平衡方面,分析和总结自己第三次作业架构设计的可扩展性
-
功能设计:
-
第三次作业为多部多线程可稍带调度电梯: 本次作业满足SOLID原则中除ISP外的各项原则。本次作业采用了Work-Thread模式。优点:可扩展性较好。缺点:部分方法逻辑复杂。
扫描二维码关注公众号,回复: 10917646 查看本文章-
整体架构:构造父类
ElevatorTemplate
类对电梯线程建模,父类实现ALS调度,通过继承的方式,实现三类电梯,满足不同电梯的个性化要求(比如可停靠楼层、人员数量等)(OCP原则)。构建父类Person
,NonStopPerson
和TransferPerson
继承自Person,ElevatorTemplate
的调度依赖于抽象Person类,而不是依赖于具体的某类请求(DIP原则),请求队列中用父类Person统一管理(LSP原则)。线程协同和之前基本一致,每个电梯线程去争抢请求队列中的Person,放入各自的局部队列,进行调度。同时,电梯类、请求类、输入类,各个类以及各个类的方法有单一的明确的职责。基本满足SRP原则。 -
可扩展性:这样的架构可扩展性较好,无论是如果需要新增其他的个性化请求电梯,只需继承电梯模版类,同理,如果需要新增新的乘客需求,也只需继承
Person
类,其余地方都不需要改变。
-
-
-
性能设计
-
电梯调度策略:由于求稳,单部电梯沿用了第一作业的ALS调度策略。
-
请求队列分配策略:主要涉及对主请求的分配,采用的是每个线程自己去抢的策略:如果是直达请求,由相应类型的电梯去接,如果是换乘请求,由满足其出发楼层的任意一个电梯去接。
-
换乘的策略:本人的换乘策略是每一个换乘请求都在1楼或15楼换乘,具体是1楼还是15楼,由换乘请求的出发楼层决定。在完全随机的情况下,这样的调度方式性能应该还ok。
-
三、基于度量来分析自己的程序结构
-
第一次作业
-
各个类的规模
-
方法级度量
可以看出,其中
midWayTaskeFromCurFloor
方法的基本复杂度比较高,分析知主要是由于在这个方法中if语句括号里即完成了条件判断,有进行了计算操作。-
UML类图
优点:典型生产者-消费者模式,简洁易懂。
缺点:可以不需要主线程类,直接将EleInputThread当作主线程。
-
UML sequence diagram
-
-
第二次作业
-
各个类的规模
-
方法级度量
-
UML类图
优点:典型生产者-消费者模式,简洁易懂。
缺点:可以不需要主线程类,直接将EleInputThread当作主线程。
-
-
第三次作业
-
各个类的规模
图片
图片
图片
-
方法级度量
图片
可以看出,其中
midWayTaskeFromCurFloor
方法的基本复杂度比较高,分析知主要是由于在这个方法中if语句括号里即完成了条件判断,有进行了计算操作。-
UML类图
图片
优点:典型生产者-消费者模式,简洁易懂。
缺点:可以不需要主线程类,直接将EleInputThread当作主线程。
-
四、分析自己程序的bug
-
本人三次作业在强测中均未发现bug
-
但是在第二、三次互测中,均有一个bug,且每次就这个bug被别人刀的很惨。本人在第三的互测中只考虑了新增电梯的编号为X1、X2、X3三种情况,导致bug。本人在第二次bug中遇到了死锁的问题,下面将重点分析死锁产生的原因。
-
原因分析:经过仔细分析自己的代码,发现本次作业死锁的产生是因为进程之间的彼此通信造成的。当多个电梯线程同时开启,且此时请求较少时,不妨假设目前有三个电梯线程A、B、C,1个请求R,本人采取的是抢请求的设计,抢请求前,三个电梯线程A、B、C均处于等待获取请求的状态,当请求R输入后,假设电梯线程A获得请求R,并通过notifyAll唤醒了处于等待状态的B、C线程,由于之后仍然没有请求,B、C线程将继续等待,但是当不在有请求时,即
noMoreRequest = true
时,并没有唤醒处于等待状态的B、C线程,B、C线程将永远等待下去,产生了死锁。 -
解决方法:在方法
setNoMoreRequest()
中添加notifyAll语句,并将唤醒后的线程立即执行return null
语句,通过判断返回值是否不null
来杀死线程。public synchronized void setNoMoreRequest(boolean noMoreRequest) {
notifyAll();
this.noMoreRequest = noMoreRequest;
}while (globalRequest.isEmpty()) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
-
五、分析自己发现别人程序bug所采用的策略
-
本人采取的策略是手动构造边界数据、可能超时的数据和可能产生线程安全的数据的策略。在本单元hack中分别hack成功1次、0次、3次
-
本人主要使用了
JProfiler
来观察是否有暴力轮寻,各个线程线程的状态,判断是否可能产生死锁现象,其中在第二次和第三次作业中,经常会有线程不能正常结束的现象。 -
差异:个人感觉多线程的测试比较玄学,第一单元测试主要是黑盒测试,只需关注最后的结果是否正确,但是本单元多线程的测试,由于线程进行顺序的不确定性,每次跑出来的结果可能都不一样,这一无疑给测试带来了巨大的困难,构造测试数据也从第一单元重点构造边界数据的数据转向可能造成线程不安全的数据。
六、心得体会
-
线程安全方面:本单元令我影响很深刻的一个是死锁、另一个是操作的原子性。在进行多线程编程的时候,需要理解锁这个概念,在Java中,每一个对象多有一把锁,在对共享资源进行访问时,先要获取这把锁,一旦某个线程获取了锁,其他的线程必须等待。线程之间的通信一旦没有处理好,极有可能造成死锁现象的产生,一旦两个线程互相等待通知时,或者没有线程去唤醒处于等待队列中的线程时,都会产生死锁。
-
设计原则(SOLID)方面:通过该单元的三次作业,本人对SOLID中的OCP、ISP、LSP、DIP三个原则有了更深的理解,也将其运用到了自己的作业当中。第一次作业像是基石一样,第二次、第三次作业就好似在基石上添砖加瓦。本人也从本单元的作业中深刻体会到了迭代的乐趣。第二次作业,就是新开了若干个电梯线程其他几乎没变,第三次作业,对电梯统一建模,构建
ElevatorTemplate
类,采用继承的方法,构建三类电梯(OCP),每一个电梯要做的都是对请求的execute,对请求进行抽象(依赖于抽象DIP),再通过继承实现各种特殊的请求,如非换乘请求、换乘请求,通过抽象的请求统一管理(ISP、LSP),从而使得我们程序的健壮性更好。