「BUAA OO」Unit2

一、同步块与锁

  1. 什么时候需要加上同步块?

      经过查阅资料我们可以知道,同步代码块指的是被Java中synchronized关键词修饰的代码块,在Java中,synchronized关键词不仅仅可以用来修饰代码块,与此同时也可以用来修饰方法,是一种线程同步机制,被synchronized关键词修饰的代码块会被加上内置锁。

      然而,synchronized同步代码块是一种高开销的操作,因此我们应该尽量减少被同步的内容。在很多场景下,使用synchronized声明的方法在某些情况下是有弊端的,故我们使用synchronized代码块去优化代码执行时间,也就是通常所说的减少锁的粒度。对于synchronized 的使用:如果修饰的是具体对象:锁的是对象;如果修饰的是成员方法:那锁的就是 this ;如果修饰的是静态方法:锁的就是这个对象.class

      在第五、六次作业中,由于本人对于synchronized的应用并不熟悉,故采用的都是对方法加锁的办法。例如,对于等待队列中的所有操作(增减请求、设置结束标志)和电梯中的运行行为(上下行、左右移动、增减乘客等),都在方法前加上了synchronized,以避免资源的冲突。

      在第七次作业中,开始意识到对方法加锁所造成的资源浪费,故改为对对象object进行上锁:synchronized(非this)代码块的程序与同步方法是异步的,不与其他锁this同步方法争抢this锁,可以大大提高运行效率。

  2. 锁与同步块中处理语句之间的关系

      通过理论课的学习,我们知道锁机制分为物理锁和逻辑锁:物理锁的特点是严格,JVM内置,好理解;逻辑锁则更加灵活,可扩展出自己想要的更多特性。本人在作业中使用的是synchronized,故着重分析前者。

      使用synchronized可以理解为,从“{”开始,进行上锁;遇到对应的“}”后,即将锁释放。需要注意的是,若在“}”之前使用了return语句,则锁不会自行释放,故需要我们自己加上notifyAll语句进行其他线程的唤醒,否则将会产生意想不到的bug。

  3. 如何避免死锁

      产生死锁的四个必要条件:互斥条件、请求与保持条件、不可剥夺条件、 循环等待条件。这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。

      死锁避免的基本思想:系统对进程发出每一个系统能够满足的资源申请进行动态检查,并根据检查结果决定是否分配资源,如果分配后系统可能发生死锁,则不予分配,否则予以分配。这是一种保证系统不进入死锁状态的动态策略。

 

二、调度器设计

  本单元中,我设计的调度器有两个:请求分配调度器和电梯运行调度器。

  1. 请求分配调度器

    第五次作业中,将Input线程处理完后的waitQueue传入请求分配调度器Schedule线程,只要队列不为空,它就负责将请求分配到对应的各栋电梯的控制器中。

    第六次作业中,请求分配调度器Schedule将InputThread分类完后的PersonRequest分别分为Building类和Floor类,并将请求分别分配到对应的主队列中(BuildingWaitingQueue或FloorWaitingQueue)。

    第七次作业与第六次作业的请求分配调度器设计基本相同,需要增加的功能:对传入的请求进行再包装,规划其下一个目的地后再传入对应楼或层。

  2. 电梯运行调度器

    第五次作业中,一个电梯运行调度器ControlRun线程控制一个电梯的运行,根据等待队列中的请求和已在电梯中的请求,控制电梯的上下行、开关门、进出人。当请求队列为空且结束、电梯中不存在乘客时,运行结束。

    第六次作业中,电梯运行调度器分为Building类和Floor类,并在此基础上再分为主调度器和从调度器。相比于第五次作业中的电梯调度器所实现的功能,本次电梯调度器中,主调度器记录了对应楼/层所拥有的电梯,并将该楼/层WaitingQueue中的请求按照策略分配给各个电梯;从调度器的功能仍与第五次作业类似,负责控制电梯的运行。

    第七次作业再第六次作业的基础上,从电梯运行调度器需要增加功能:判断该乘客的目的地是否为最终的目的地,如果是,则释放请求;否则,需要再次传回Schedule线程进行包装。

     

三、线程协同架构

  1. 第五次作业

    (1) 架构设计

    (2) 协作关系

     

    本次作业采用了生产者-消费者模式。

     

  2. 第六次作业

    (1) 架构设计

     

    (2) 协作关系

    本次作业采用了生产者-消费者模式、单例模式。

     

  3. 第七次作业

    (1) 架构设计

    本次作业架构与第六次作业类似,新增自定义的MyPersonRequest类,且Schedule增加再次包装分配请求功能。

    (2) 协作关系

    本次作业采用了生产者-消费者模式、单例模式、流水线模式。

     

四、bug分析

  1. 解决轮询问题

    刚接触多线程时,自己并不明白什么是轮询,只发现提交作业后CPU超时。在查询了一系列资料之后,才大体理解了为什么会造成轮询:以我自己的理解,就是某个循环中并没有做什么事情,却一直在运行,占了极大部分的资源。解决的方法可以使用IDEA自带的一个叫做Profile的工具,点击后即可查看整个CPU的占比,以此缩小范围判断是哪一个部分出现了轮询。

  2. 电梯调度策略的bug

    在互测过程中,发现自己犯了一个非常低级的错误:先进后出。当电梯人数已满,哪怕该层楼有人会下电梯,但由于“进”在前面,能进来的人却无法进来。解决方法比较简单:即调换进出顺序。

  3. 无法正常结束程序

    第七次作业中的线程较多,自己刚开始没能清楚理清它们之间的关系,在提交测试后,发现运行时间超时。在请教助教后,学会了一种debug的方法:在每个使用某方法的调用者处,在print-add之后加上“System.out.println(Thread.currentThread().getName());”进行调试。通过每一处地排查,最终解决了这个问题。

  4. 发现他人bug

    在测试他人代码时,我采用了以下三种思路,最终都成功地hack:

    (1)测试其在规定时间内是否能跑完程序:将指令的输入时间控制在最后几秒中,电梯的楼层跨度从一楼到十楼。

    (2)测试其是否能在极短时间间隔内处理完输入请求:每隔0.1s进行大量的输入请求。

    (3)测试其是否控制了人数,在运行过程中会不会丢失请求:在同一时间内输入大量的同楼层请求。

     

五、心得体会

  再次回顾电梯周的学习,从最初对复杂多线程的担心和害怕,到最后成功完成的惊喜和不可思议,突然发现自己在这个过程中有了很大的成长。很多时候,看起来很困难的事情,原来通过一步一步地去分析、去解决,其实每个人都可以做到。

  这个过程中自己也有很多不足之处需要去改进:习惯将作业拖延、架构没有理清就开始动手、对知识点的掌握不透彻......希望自己在接下来的作业中能改正这些缺点,更加自律。记得荣文戈老师在课上说的:“保留原有的长处,不断地优化自己”。

  最后,我想感谢助教们对我的帮助。很多时候自己提出的问题很低水平,但你们依然很耐心认真地为我解答,给予了我很大的帮助!谢谢你们ww

posted @ 2022-05-04 12:10  Sternstunden  阅读(25)  评论(1编辑  收藏  举报