OO第二单元总结
第二单元总结
Tags: OO
线程同步方法
1)为什么需要线程同步?
你是否曾经遇到过这样的情形,尽管自己的房间很乱,但是自己依然能够在乱哄哄的房间里找的自己的某件物品,而当你的母亲帮你收拾了房间之后,许多东西都找不到了。
让我们分析这个过程,对于某件物品来说,它的位置对你是一个信息,如果你每一次移动了它之后都能记住它的位置,那么你永远都能够找到它,但是问题在于,如果它在你不知情的情况下发生了移动,那么你依据上次记住的位置去寻找它变化产生迷惑。
线程同步的来源也类似于此,并发的多个线程可能对同一些数据进行操作,如果不加以协调,那么有些时候,某些线程由于数据变动便会茫然无措,发生运行错误。这时,便需要程序员来对不同线程来进行协调,使得它们相互之间不致影响。
2)如何进行线程同步?
进行线程同步的一个思路是,既然一个数据可能被并发的多个线程以未知的顺序进行读写,那么我们是否可以要求线程以固定的顺序执行,答案是:不行。原因是并发线程之间的执行顺序是未知的,被封装于操作系统的黑盒之中。那么现在不能对线程进行要求,我们便可以尝试对数据的访问和修改进行限制。在Java中提供的方法便是锁机制。其关键想法是线程在访问某些被共享的数据时,对数据上“锁”使之无法被除上锁的线程以外的线程访问。这样的做法保证了在某个时刻只有一个线程对共享数据进行操作。(但是依然无法保证操作的顺序)
3)具体实现
在Java所提供的语法中,synchornize关键字用来实现对指定对象加锁,它既可以直接指定某个具体的锁对象,也可以作为某个方法的关键字来实现线程安全类。
调度器设计
我在三次作业中都实现了调度器,调度器本身作为一个线程存在,它读取总的请求队列中的请求再分发给各个电梯,在最开始的时候,我没有考虑太多,按照ALS算法设计了调度器,导致许多个测试点直接超时,在优化了调度器的算法之后,修复了第一次电梯作业里的bug。这里调度器线程与电梯线程的交互方式是互斥访问requestQueue这个共享变量,也就是说,对于电梯来说,它只能感受到requestQueue的变化,并不能知道调度器的存在,在这之后的两次作业中,这一方式都没有发生改变,这也导致了电梯的性能有限,原因是它并不是实时读取标准输入的,而需要经过调度器的转发并且处理完当前请求才会再次从waitQueue中读取请求,再好的调度算法也只是尝试着预测未来,但是这些都远不如把握当下(指实时响应请求)。
调度器代码如下:
private void arrangeRequest() {
while (true) {
if (tryToReturn()) {
return;
}
ArrayList<PersonRequest> thisRequest = getPassenger();
synchronized (elevators) {
while (elevators.size() > requestQueues.size()) {
generateNewElevator();
}
elevators.notifyAll();
}
if (thisRequest == null) {
waitForAllArrive();
if (waitQueue.getSize() == 0) {
waitQueue.setEmpty();
notifyAllElevator();
return;
}
thisRequest = getPassenger();
}
dealRequest(thisRequest);
if (waitQueue.isEnd() && waitQueue.getSize() == 0) {
waitForAllArrive();
if (waitQueue.getSize() == 0) {
waitQueue.setEmpty();
}
}
}
}
private ArrayList<PersonRequest> getPassenger() {
long startTime = System.currentTimeMillis();
if (arrivePattern.equals("Morning")) {
waitForPassenger(startTime, 6000);
} else if (arrivePattern.equals("Random")) {
waitForPassenger(startTime, 2000);
} else {
synchronized (waitQueue) {
while (!waitQueue.isEnd()) {
try {
waitQueue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
waitQueue.notifyAll();
}
}
PersonRequest personRequest = waitQueue.getRequest();
if (personRequest == null) {
return null;
} else {
waitQueue.addRequest(personRequest);
return choosePassenger();
}
}
private ArrayList<PersonRequest> choosePassenger() {
ArrayList<PersonRequest> thisRequest = new ArrayList<>();
ArrayList<PersonRequest> upPassenger = new ArrayList<>();
ArrayList<PersonRequest> downPassenger = new ArrayList<>();
synchronized (waitQueue) {
for (PersonRequest person : waitQueue.getRequestQueue()) {
if (isUpPassenger(person)) {
upPassenger.add(person);
} else {
downPassenger.add(person);
}
}
if (upPassenger.size() < downPassenger.size()) {
for (PersonRequest person : downPassenger) {
thisRequest.add(person);
waitQueue.getRequestQueue().remove(person);
}
} else {
for (PersonRequest person : upPassenger) {
thisRequest.add(person);
waitQueue.getRequestQueue().remove(person);
}
}
waitQueue.notifyAll();
}
return thisRequest;
}
private void dealRequest(ArrayList<PersonRequest> personRequests) {
pushRequestToC(personRequests);
if (personRequests.size() == 0) {
return;
}
pushRequestForB(personRequests);
if (personRequests.size() == 0) {
return;
}
pushRequestForA(personRequests);
if (personRequests.size() != 0) {
for (PersonRequest personRequest : personRequests) {
waitQueue.addRequest(personRequest);
}
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
架构设计的可扩展性
第三次的架构设计,经过前两次强测和自测之后,在安全性方面已经做了很多了,唯一遗憾的是没有实现实时响应请求的功能,这里介绍以下我的调度策略,也许就能展示我的电梯为什么无法实时处理请求的原因了。我的电梯调度策略是每一次都让电梯处理一个方向的请求,在它处理某个方向的请求时,尽量将另一个方向的请求分发给它,这样就不会让它来回跑,电梯在处理完该方向请求后,才会查看调度器是否分发了新的请求,而不是在行走的过程中实时查看调度器分发来的请求。这样简化了(其实也不一定)电梯程序,但是也很大程度上牺牲了性能。
本次作业的UML类图如下:
UML顺序图(主线程仅仅实例化和启动了各个线程):
由于采用了策略和机制相分离的设计思想,也就是调度策略和电梯运行机制相分离,该架构的可扩展性很强,只需要单独改动Elevator或调度器中的方法即可实现不同的调度策略。
程序中的bug
1)第一次作业
第一次作业中的bug全部是由于电梯运行过慢导致的,电梯过慢的原因是我的调度算法是ALS调度,还写得比data_checker的慢,在稍稍改动了调度算法后便通过了测试点。
2)第二次作业
第二次作业中的bug一个是由于线程安全问题引发的,另外两个是在极限数据中超时,同时还发现了由于线程同步导致了某些电梯线程提前死亡,这样使得性能变得很差。
3)第三次作业
第三次作业中没有产生bug,但是性能由于前面所述的原因很差。
分析他人程序中存在的bug的策略
我主要是构造极限样例的方法来找出bug,比如,在第一次作业的时候,在Random模式下构造让电梯在1-20层来回跑的数据,再构造一组全部由10层出发,前往1或20层的数据,以此类推,看看程序会不会超时或者有线程安全问题。到了第二次和第三次作业,我就根据强测数据的构造方式,产生类似的数据进行测试。
心得体会
经历了这一次作业后,我更加深入的认识了面向对象的威力所在,同时我还发现UNIX设计哲学在程序同样有大用处,这样设计出来的程序可以非常优美,我想最高境界就是所谓的“代码即注释”。
UNIX设计哲学的信条有以下几条:
- 模块原则,对应面向对象中对象用消息相互通信的思想;
- 隔离原则,也就是策略和机制相分离,本次作业中电梯的运行机制和调度器的调度策略是想分开的;
- 表达原则,用数据结构表达逻辑,而不是用过程控制表达逻辑;
本文作者:Max_Season
本文链接:https://www.cnblogs.com/maxorao/p/14710922.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步