301
社区成员
发帖
与我相关
我的任务
分享在本单元多线程的学习中,线程对共享对象的竞争是需要解决的一大问题。”锁“使得任意时刻只有一个线程能读写该共享对象,实现线程互斥;”锁“也使得满足某条件时,线程通过wait()方法进入等待队列,再通过notifyAll()将其唤醒,实现线程同步。选择合适的”锁“能实现线程的同步互斥,进而解决线程安全问题。
public synchronized void func() {
// do something
}
synchronized修饰方法,方法内的语句是临界区,只有当线程获得该对象的锁时,才能进入该对象的该方法。
若修饰的是静态方法,只有当线程获得该类的锁才能进入该方法。
synchronized(obj) {
// do something
}
synchronized修饰代码块,代码块内的语句是临界区,只有当线程获得obj的锁,才能进入该代码块。
在OS的学习中,为实现临界区引入了信号量机制。被赋为某初值后,若线程需要分配资源,则semaphore--,若资源耗尽则线程挂起;若线程释放资源,则semaphore++,并唤醒某一线程。
读写锁相对于synchronized锁而言,把读写操作细分。允许readLock被多个线程拥有,但writeLock锁只能被一个线程拥有,且此时不允许线程拥有readLock。调用lock()和unlock(),自定义临界区。
writeLock.lock();
try {
//do something
} finally {
writeLock.unlock();
}
readLock.lock();
try {
//do something
} finally {
readLock.unlock();
}
通过RequestList,输入线程作为生产者向请求池加入顾客请求,调度器线程作为消费者从中拿取请求;再由调度器作为生产者向电梯请求池加入顾客请求,电梯线程作为消费者从中拿取请求。
RequestList是临界资源,作为线程间数据传输的托盘。
reset标志位在RequestList中,代表每个电梯此时此刻的重置状态。输入线程读到重置请求后把reset标志位置1,电梯线程完成重置后把reset标志位置0。
调度器线程通过访问reset标志位,判断电梯是否正在reset,并凭此决定如何分配顾客请求。
输入线程为总请求池setEnd,调度器在总请求池已被setEnd且为空时,为各电梯请求池setEnd,从而完成电梯线程的合法结束。
我的调度策略主要采用random随机分配,并局部考虑电梯性能,如电梯move速度、电梯容量、电梯目前接受的请求数、电梯目前的移动方向和楼层等等,尽量选取电梯性能更好的、电梯目前运行状态更合适的分配请求。
对于正在重置的电梯,调度器会不进行请求的分配,另找新的电梯尝试分配。为防止所有电梯都在reset时,调度器无法分配请求导致的轮询,我在每次分配请求前先检验是否电梯都在重置,若是,调度器就sleep(500),防止忙等待。
对于双轿厢电梯,调度器会把顾客请求分段后分配给双轿厢电梯,便于乘客之后的换乘。
第一次架构确定了输入线程、调度器线程、电梯线程三个主线程,线程间通过请求池进行交互,是一种较标准的生产者-消费者模式。
电梯实际执行部分与策略相分离,使电梯能快速切换不同策略。
在电梯运行策略上一开始我采用的是ALS算法,该算法运送乘客效率较低,后改用LOOK算法。
第二次增添重置请求,在请求池中加入reset标志位,输入线程、电梯线程写reset标志位(setReset),输入线程、调度器线程、电梯线程读reset标志位(isReset),线程交互方式增多。

第三次引入双轿厢电梯和乘客换乘问题,调度器内不仅有普通电梯请求池,还有双轿厢各自的电梯请求池,同时修改顾客类给请求分段。
新增TransferFloor换乘楼层类,使得双轿厢电梯不会相撞。TransferFloor作为临界资源,配备synchronized锁和wait-notify机制,任意时刻最多只允许一个电梯停靠。

避免双轿厢碰撞其实就是避免两个电梯同时在换乘层。我单设了TransferFloor类,并为其中的arriveTransferFloor和leaveTransferFloor方法加锁。
TransferFloor作为临界资源,每当电梯尝试到达换乘层时,若该换乘层已有电梯停留,则需wait,等到另一个电梯离开换乘层后再被notify;若该换乘层无电梯停留,则可以进入换乘层,并设置好doubleCarElevator标志位,防止另一个电梯进入换乘层。
public synchronized void arriveTransferFloor(String doubleCarElevator) {
//this.doubleCarElevator标志现在换乘层的电梯编号:“A","B",null
while (this.doubleCarElevator != null) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.doubleCarElevator = doubleCarElevator;
}
public synchronized void leaveTransferFloor() {
this.doubleCarElevator = null;
notifyAll();
}
一个电梯在换乘层的时间必须是有限的,不然会导致另一个双轿厢电梯的死等wait。因此在双轿厢电梯结束线程和等待请求前,需要判断它是否在换乘层,若在则离开,并唤醒另一个等待进入换乘层的电梯。
强测未错,互测被hack了一刀。在hw_5中我一开始用的是ALS算法,捎带性能较差,若在50s时输入大量请求会导致RTLE(运行130s)。后改为LOOK算法,成功跑进120s(大概90s左右)。
强测未错,互测被hack了两刀,居然是两个bug。感谢我温柔的房友们手下留情,我室友一个bug被hack了10刀。
一个bug是因为我未设置缓冲队列,在电梯重置时,调度器不会向电梯请求池中放入请求。若在50s时输入大量请求且只留一个电梯未在reset,这个电梯将接受输入的全部请求,且即使别的电梯重置完成,依旧无法把请求分给别的电梯,导致RTLE。一个电梯跑的累死其他电梯:”哟老哥,忙呢”。后来我为电梯请求池设置请求池上限,解决了该问题。
另一个bug是因为线程安全。由于我处理重置请求是通过reset标志位,输入线程、调度器线程、电梯线程会对标志位又读又写,导致某些情况下,调度器会误向已在重置的电梯请求池内加入顾客请求。可能发生概率极低所以没被强测hack到但被敏锐的房友hack住了。我的解决方法是把reset标志位放入RequestList类,对标志位的读写都加synchronized锁,解决reset的线程安全问题。
很遗憾由于强测被hack了7刀,没有进入互测。这7刀都是同一个错误CTLE。双轿厢电梯重置后,原本的电梯应该结束线程,但由于我在优化reset处理时未考虑周全,原本的电梯并未结束并且进入了死循环,导致CTLE。解决方法是完善reset的优化过程或者直接把优化删掉,很心痛早知道不优化了我失去的强测分啊啊啊
由于线程安全,多线程可能会出现很多幽灵bug让人百思不得其解。且错误很难再现,可能评测机显示错误但本地跑就是对的,需要本地跑很多趟才能再现错误。
除了错误很难再现的问题,由于多线程不能用行断点debug,只能用System.out.println的方式打印输出线程的运行状态和过程。
/* if (requestList.isEnd()) {
System.out.println(String.format("%d-%s is end",id,doubleCar));
}
if (requestList.isEmpty()) {
System.out.println(String.format("%d-%s requestList is empty",id,doubleCar));
}*/
if (advice == Advice.MOVE) {
//System.out.println(String.format("%d-%s Advice is move",id,doubleCar));
move();
} else if (advice == Advice.REVERSE) {
//System.out.println(String.format("%d-%s Advice is reverse",id,doubleCar));
direction = -direction;
} else if (advice == Advice.WAIT) {
//System.out.println(String.format("%d-%s Advice is wait,resetFlag is %d "
// ,id,doubleCar,requestList.isReset()));
}
...
多线程同时读写带来的线程安全问题像幽灵一样缠绕在三次作业中,处理不好很难debug修正。因此在多线程设计阶段就要选定好临界资源和锁,先在草稿纸上模拟多线程并发,仔细考虑线程不同执行顺序的结果,确保没有线程安全问题后再写代码。
第二次作业出现的reset标志位线程安全问题,使我深化了对临界区的认知,更体会到了锁在多线程设计中的重要性。
生产者-消费者的层次化设计沿用了三次,是很适合这单元作业的架构设计。
电梯单设Strategy类,实现执行与策略部分分离的层次化设计,也使本单元的作业思路更为清晰。
流水线层次化设计,把顾客请求分段,再交给双轿厢电梯执行,这一架构设计使得第三次作业的迭代很容易就能实现,而不用大幅度修改原有电梯的逻辑。
总之,第二单元相较于第一单元的难度更高了,需要考虑的情况更多了,出现的bug也更多了。可能最后一次作业没进互测有些遗憾,但完成本单元的所有作业后,我对多线程、临界资源、临界区、synchronized锁有了初步的认知,这是我近一个月来的最大收获。