同步块的设置和锁的选择
首先总结下锁的知识:
-
内置锁 (Synchronized)
-
Synchronized 方法:通过在方法声明上加
synchronized
关键字,可以确保在同一时刻最多只有一个线程能够执行该方法。 -
Synchronized 块:通过 synchronized 关键字和一个锁对象来创建一个同步块,只有获得了锁对象的锁定,线程才能执行这个块内的代码。
-
显示锁 (java.util.concurrent.locks.Lock)
-
ReentrantLock:是一个可重入的互斥锁,提供了广泛的锁操作,比内置锁更灵活。
-
ReadWriteLock:读写锁允许多个线程同时读取共享资源,但只有一个线程可以写入。
-
Condition:与 Lock 配合使用,可以分离出不同的 Object.wait() / Object.notify() 队列。
第一次作业中,我们只需要考虑inputThread写入waitmap与DispatcherThread读取waitmap的冲突以及DispatcherThread写入电梯的outlist和电梯的读取自身的outlist的冲突即可,所以我直接给WaitList类的读写方法加了synchronized控制块,同时,在某些需要多次访问WaitList类的地方加上了synchronized控制块。具体可见下图:
如上所示,生产者InputThread从输入中将乘客请求加入大盘子waitmap中,然后消费者DispatcherThread从大盘子中取出乘客请求;之后DispatcherThread转变角色成为生产者并将乘客请求调度给某部电梯线程消费
那么为什么给waitlist上锁呢?(这里的waitlist包括了上图的waitmap和outlist)
-
waitmap作为输入线程和调度器线程的托盘资源,可能涉及这两个线程的同时读写,因此需加上同步锁来保证线程安全
-
同时waitlist作为调度器线程和电梯线程的托盘资源,必然涉及调度器线程和电梯线程的同时读写,因此需加上同步锁来保证线程安全
第2,3次作业中,我只新增了对elevator对象或elevators容器(包括doubleEleThreads)上锁。具体代码如下:
synchronized (elevators) { for (Map.Entry<Integer, ElevatorThread> entry : elevators.entrySet()) { ElevatorThread elevatorThread = entry.getValue(); elevatorThread.getOutlist().setIsEnd(1); } }
synchronized (doubleEleThreads) { for (Map.Entry<String, DoubleEleThread> entry : doubleEleThreads.entrySet()) { DoubleEleThread doubleEleThread = entry.getValue(); doubleEleThread.getOutlist().setIsEnd(1); } }
代码架构![](https://i-blog.csdnimg.cn/blog_migrate/a6307fda492af65a9b58fbfd951676ba.png)
生产者InputThread从输入中将乘客请求加入大盘子waitmap中,然后消费者DispatcherThread从大盘子中取出乘客请求;之后DispatcherThread转变角色成为生产者并将乘客请求调度给某部电梯线程消费。对于reset请求和resetDc请求,我在ElevatorThread线程中设置了属性reset,doubleEle,当两种重置请求被输入线程解析后,会将对应电梯的两种属性设1,并在电梯中新增属性:newSpeed,newCapacity;然后在电梯线程处理reset请求
那么,在什么时候设置线程的等待状态wait以及什么时候唤醒?当输入未结束但调度器线程无法从大盘子waitmap获得一个乘客请求时,显然此时调度器线程必须等待,直到输入线程将新的乘客请求加入到大盘子waitmap中并将其唤醒;当电梯停在某一层,电梯内外队列都无乘客且外部队列outlist还有可能接收到来自调度器调度的乘客请求时,此时电梯线程必须等待,直到调度器线程将新的乘客请求加入到对应电梯的小盘子outlist中并将其唤醒;
此外,程序何时结束呢?考虑到程序输入结束时,waitmap已经无任何新增的乘客请求,此时可以结束输入线程,同时设置waitmap的属性isEnd为1,代表waitmap已经无任何新增的乘客请求;当waitmap为空且waitmap已经无任何新增的乘客请求时,此时可以结束调度器线程并设置每个电梯线程对应的outlist的属性isEnd为1,代表各个outlist已经无任何新增的乘客请求;当电梯内外都无人时且outlist已经无任何新增的乘客请求时,我们可以结束电梯线程;至此,所有程序结束
作业变化
第一次作业
第一次作业在调度策略上不难,因为由输入指定了对应电梯,重点是在电梯运行策略上,电梯运行采用Look策略,采用走一步看一步的方法,类似于获得建议然后根据建议执行行动。
本单元作业我采用的是生产者--消费者模型,生产者为输入线程,消费者为调度器线程,之后调度器线程又充当生产者,电梯线程充当消费者。在这次作业中,我只对waitlist类加锁;对于锁的大小要适当,如果锁太大,会降低并发性和效率,如果锁太小,可能引发线程安全问题。因此我们要使得锁与同步块中的语句密切相关,比如,waitmap锁只锁住与waitmap密切相关的语句,具体代码如下:
public int getWaitListSize() { lock.readLock().lock(); try { return waitList.size(); } finally { lock.readLock().unlock(); } } public ArrayList<Passenger> getWaitList() { lock.readLock().lock(); try { return waitList; } finally { lock.readLock().unlock(); } }
第二次作业
第二作业由于在调度策略方面不再指定电梯,因此我们需要涉及自己的调度策略,我采用的是按优先级分配策略,为了尽量满足同向捎带要求,这样可以极大的提高性能,具体来说:如果请求乘客的方向与某一电梯方向相同,并且该电梯还未到达请求可的楼层,且该电梯内部乘客和外部乘客之和不少于最大容量,则将该电梯作为优先电梯,如果没有这样的电梯,则均匀分配一个电梯;
在电梯运行策略上与第一次变化不大,只是新增了对reset请求的处理流程;
第三次作业
第三次作业新增了双轿厢电梯,我的处理方法是resetDc请求传递到电梯线程后,先踢出电梯里外的人并将其返回调度器,然后将普通电梯线程删除并新增双轿厢电梯线程,同时为了避免双轿厢电梯相撞,我们设置一个Flag类并添加属性occupied,如果此时双轿厢电梯有一个占用了换乘层,则设置occupied为1,同时若另一电梯想要进入换乘层,则让其wait直到另一电梯离开换乘层。在本次作业中,电梯结束的条件也是最难判断的一点,但好在经过不断地尝试,还是成功了。
bug以及debug方法
-
电梯超载问题:在判断乘客可以进电梯时忘记考虑容量限制
[1.6]RESET-DCElevator-1-3-3-0.6 [1.6]RESET-DCElevator-2-3-3-0.6 [1.6]RESET-DCElevator-3-3-3-0.6 [1.6]RESET-DCElevator-4-3-3-0.6 [1.6]RESET-DCElevator-5-3-3-0.6 [1.6]RESET-DCElevator-6-3-3-0.6 [49.9]1-FROM-3-TO-11 [49.9]2-FROM-3-TO-11 [49.9]3-FROM-3-TO-11 [49.9]4-FROM-3-TO-11 [49.9]5-FROM-3-TO-11 [49.9]6-FROM-3-TO-11 [49.9]7-FROM-3-TO-11 [49.9]8-FROM-3-TO-11 [49.9]9-FROM-3-TO-11 [49.9]10-FROM-3-TO-11 [49.9]11-FROM-3-TO-11 [49.9]12-FROM-3-TO-11 [49.9]13-FROM-3-TO-11 [49.9]14-FROM-3-TO-11 [49.9]15-FROM-3-TO-11 [49.9]16-FROM-3-TO-11 [49.9]17-FROM-3-TO-11 [49.9]18-FROM-3-TO-11 [49.9]21-FROM-2-TO-11 [49.9]22-FROM-2-TO-11 [49.9]23-FROM-2-TO-11 [49.9]24-FROM-2-TO-11 [49.9]25-FROM-2-TO-11 [49.9]26-FROM-2-TO-11 [49.9]27-FROM-2-TO-11 [49.9]28-FROM-2-TO-11 [49.9]29-FROM-2-TO-11 [49.9]30-FROM-2-TO-11 [49.9]31-FROM-2-TO-11 [49.9]32-FROM-2-TO-11 [49.9]33-FROM-2-TO-11 [49.9]34-FROM-2-TO-11 [49.9]35-FROM-2-TO-11 [49.9]36-FROM-2-TO-11 [49.9]37-FROM-2-TO-11 [49.9]38-FROM-2-TO-11 [49.9]71-FROM-1-TO-11 [49.9]72-FROM-1-TO-11 [49.9]73-FROM-1-TO-11 [49.9]74-FROM-1-TO-11 [49.9]75-FROM-1-TO-11 [49.9]76-FROM-1-TO-11 [49.9]77-FROM-1-TO-11 [49.9]78-FROM-1-TO-11 [49.9]79-FROM-1-TO-11 [49.9]80-FROM-1-TO-11 [49.9]81-FROM-1-TO-11 [49.9]82-FROM-1-TO-11 [49.9]83-FROM-1-TO-11 [49.9]84-FROM-1-TO-11 [49.9]85-FROM-1-TO-11 [49.9]86-FROM-1-TO-11 [49.9]87-FROM-1-TO-11 [49.9]88-FROM-1-TO-11 [49.9]89-FROM-1-TO-11 [49.9]90-FROM-1-TO-11 [49.9]91-FROM-1-TO-11 [49.9]92-FROM-1-TO-11
-
程序无法结束的问题:程序结束条件没有正确设置以及电梯一直处于wait状态没有正确唤醒
//样例一 [0.6]RESET-Elevator-3-6-0.4 //样例二 [0.2]RESET-DCElevator-2-7-5-0.5 [0.2]RESET-DCElevator-3-7-5-0.5
-
双轿厢电梯相撞问题:在结合样例debug后发现双轿厢电梯线程语句执行顺序出现问题,如下图,正确的顺序时先睡,再改变pos并设置occupied,如果反过来,则容易在sleep期间相撞
if(pos==transferFloor){ if(eid.charAt(eid.length()-1)=='A'){ sleep(Math.round(speed * 1000)); pos--; TimableOutput.println("ARRIVE-"+pos+"-"+eid); isOccupied.setRelease(); }else{ sleep(Math.round(speed * 1000)); pos++; TimableOutput.println("ARRIVE-"+pos+"-"+eid); isOccupied.setRelease(); } }
debug方法:
-
结合打的print输出判断在哪个子模块出问题
-
目瞪法,再梳理一遍程序的逻辑
-
结合评测机
心得体会
线程安全:
线程间的共享对象锁的范围要适当,否则要么出现死锁,要么线程不安全;
层次化设计:
类与线程的分离:主要分为输入线程,调度器线程,电梯线程,然后Main负责启动这些线程;
电梯线程的策略类与电梯类分离;
心声:
这次作业主要是hw7完成的不是很理想,反思一下原因,主要是对相应知识的理解不到位,过于依赖评测机(实际上评测机也无法保证自己完全对),以上两点都说明本人写代码之前思考不是那么充分,容易导致一些细节问题的过失。总而言之,这还是警示自己在借鉴的同时不要忘记自己的思考,这样才能成为并超越吧!