面向对象设计与构造第二单元总结

  为期三周的魔鬼电梯终于结束了,老师口中OO难度的顶峰也终于过去了。让我来好好分析一下这三周的作业吧。


 

第一部分——设计策略

  第一次作业

  这次作业由于只是傻瓜单电梯,故在难度上远不及第三次嵌套多项式。不过这次作业对于初次接触多线程的我还是有很多困难,主要困难在于理解线程,不在算法设计。

  我设计了两个线程,输入线程也就是主线程(Main),用于接收请求并存入调度队列。电梯线程(Elevator)用于从调度队列取出请求并执行。另外还有一个类用于实现调度队列的功能(MyQueue)。为了做到线程安全,在MyQueue类里使用了饿汉式单例模式,代码如下

 1 public class MyQueue {
 2     private ArrayList<int[]> myArray;
 3     private static final MyQueue myQueue = new MyQueue();
 4 
 5     private MyQueue() {
 6         myArray = new ArrayList<>();
 7    ...
      
8 public static MyQueue getInstance() { 9 return myQueue; 10 } 11 }

  此外,我在MyQueue中的每个方法前都加了synchronized,确保一个时刻只能有一个线程访问调度队列。这样就基本做到了多线程的协同和同步。

  第二次作业

  这次作业在第一次作业的基础上主要增添了捎带,负楼层的需求。但基本的框架没有什么大变动,主要变化就是在于如何捎带,这里面也就能体现出性能上的差距

  首先,为了解决负楼层的问题,我在接收请求的时候判断是否有负楼层的请求,如果有,让该负楼层加一(即-3变为-2,-2变为-1,-1变为0)。然后在中间处理过程加入了-2,-1,0层。最后输出结果的时候判断将非正楼层减一再输出。这样就避免了将电梯线程复杂化。

  其次就是如何实现捎带。标准的ALS捎带条件过于苛刻,于是我稍微优化了一下,对于主请求的判别方法和标准ALS的一样。不过在判断能否捎带时有点不同,具体如下

    1.在去接主请求的过程中的楼层如果要上人,满足以下条件之一才让上

      1.1该目标楼层在该初始楼层到主请求初始楼层间

      1.2该目标楼层在主请求初始楼层到主请求目标楼层间

      1.3该目标楼层在主请求初始楼层到主请求目标楼层外,且该请求运行方向和主请求运行方向相同

    2.接到主请求后,满足以下条件之一才让上

      2.1该目标楼层在主请求初始楼层到主请求目标楼层间

      2.2该目标楼层在主请求初始楼层到主请求目标楼层外,且该请求运行方向和主请求运行方向相同

  我的这种捎带判断方式肯定比不上大佬们的贪心算法,但为了确保正确性,我还是选取了较为稳妥的方法。这次作业多了一个电梯队列,存放的是电梯内的请求。由于也属于共享对象,于是也要使用懒汉式单例模式,并将所有方法加锁,代码如下。

 1 public class ElevatorQueue {
 2 
 3     private ArrayList<int[]> myArray;
 4     private static final ElevatorQueue ELEVATOR_QUEUE = new ElevatorQueue();
 5 
 6     private ElevatorQueue() {
 7         myArray = new ArrayList<>();
 8     ...
 9     ...
10     public static ElevatorQueue getInstance() {
11         return ELEVATOR_QUEUE;
12     }
13 }

   第三次作业

  这次作业号称是OO课程难度的顶峰。私以为,对于优化党来说,可能是,但对于像我这样以正确性为主的人来说,不是。三部电梯,每部电梯运行时间,运行楼层,载重量都不同。不过这些不同在代码中只需要加几个条件即可,并没有真正的困难。如果说真要有什么困难,那就是请求的拆分

  例如一个请求  233-FROM--3-TO-3,需要拆为父请求  233-FROM--3-TO-1和子请求  233-FROM-1-TO-3

  拆完就行了吗?肯定不是,这两条请求是由一定依赖关系的,只有当第一条请求的人在1楼从A电梯out后才能在1楼in-C电梯。那么我采用的是将请求封装成一个新的类(Request),每个Request对象都有一个指向子请求的引用,如果没有子请求,则该指针为空。每个Request对象还有一个valid属性,valid为1或者表明该请求没有父请求指向它,或者表明该请求的父请求已执行完毕。

  还有一个问题是将请求分配给哪个电梯去执行。我考虑到这次测试数据是随机生成的,并且A电梯最快,于是我依次判断ABC电梯能否完成该请求,能则分配给该电梯。三个电梯没有什么关联,各自执行各自的请求,也就是三个独立的ALS电梯。至此,这次作业的难点也就基本解决了。线程协同和同步也与前两次基本一样,共享对象采用单例模式并将方法加锁。

 


 

 

第二部分——基于度量分析自己的程序结构  

  第一次作业

  UML图

  

  从类图上可以很清晰的看出,主线程创造了电梯线程。主线程和电梯线程共享MyQueue的对象。可见此次作业不复杂。

  代码统计图

          

从这两幅图都可以看出这次作业复杂度较低。

  第二次作业

  UML图

  

  第二次作业的类较第一次作业多了两个。但总体也不复杂。主线程创建电梯线程。主线程和电梯线程共享Myqueue和ElevatorQueue对象。两个共享对象之间也有关联,具体来说就是在MyQueue中判断捎带,如能捎带,从MyQueue中移除,加入到ElevatorQueue中。Handle类被三个类调用,承担输出时修改楼层的任务。总体结构复杂度不高。

  代码统计图

  

  

  由上图可见,第二次作业由于要判断能否捎带,因此方法数量有较大的增量。方法复杂度较高的方法也集中在判断捎带的方法,例如hasIn,hasOut等。这也导致了,电梯线程run方法的复杂度标红。

  第三次作业

  UML图

  

  好吧,从UML图就能看出我这次作业写得惨不忍睹。为什么呢,因为我居然写了3个电梯类,3个电梯队列类,3个请求队列类。当然我这么写也有我的原因,贯彻落实单例模式,一个类只有一个实例,因此我写了一共写了6个共享对象类。实质上这些类的内容大体相似,只不过一些名称,属性有所不同。其实事后反思一下,完全没这个必要,只需在一个类里创建另外两个实例就行。

  代码统计

  

  

  

  可以看出,这次方法数量多的吓人,而且我这只是列举了A电梯的相关方法数据。这次作业可以说我在设计时就产生了很大的问题,不肯放弃第二次作业的架构,而导致这添一点,那删一点。结果就是写出来的架构乱七八糟。这也是我在以后作业中会去尽量避免的。也就是说。不要逃避重构!!

 

 


 

  

第三部分——自己程序的bug

  这三次作业我很幸运,在强测和互测中都没有被发现bug。但自己在测试的时候发现过几个bug

  第一:第三次作业时,会出现父请求还未执行完,子请求就会被执行的情况,后发现时在判断捎带时,没有判断请求的valid位是否为1。从而导致出错。   

  第二:第三次作业时,会出现输出串行的情况,后发现是输出的接口不是线程安全的,故我又加了一个对象,利用synchronized确保一个输出执行完才会执行下一个输出

  总而言之上述的两个bug都是由于我第三次写作也时设计上的问题而产生的。没有重构,在第二次作业的基础上填填补补,导致考虑不周全。

  目前为止还没有发现其他的bug。

 


 

第四部分——发现别人的bug采用的策略

  前两次作业由于不容易产生bug,事实证明前两次互测屋中所有人都没有被成功hack。第三次作业就不一样了,四个线程,电梯情况复杂,因此产生bug的可能性大大增高。恰好身边几个同学写了个评测机,我就借过来一用。借助评测机我发现了互测屋中一个人的bug,那就是CPU时间过高,也就是程序中有轮询存在。在用评测机运行他的代码是,CPU占用率居然能达到50%,推测一下就是CPU时间超时。

  在此我要特别感谢一下写评测机的同学(陈克勤,金陆洋,向旭杰,郭梦琦,舒梓铧等),我用他们的评测机发现了我自己好几个bug,要不是他们,我可能连互测都进不去。再次感谢!!

 


 

 

第五部分——心得体会

  1.线程安全

  线程安全这一部分其实在电梯这三次作业中体现的很少。个人感觉线程安全说到底就是互斥和同步两大方面

  先讲讲互斥,主要就是对共享资源的访写问题,一旦访问和写入出现了冲突,就会出现线程不安全。这一点在实验课上的题目有所体现。怎么实现互斥?——加锁,也就是synchronized(object)。synchronized用法比较坑,因为锁住的不是代码段,而是括号中的对象。不过这样也有好处,就是可以用一个对象实现好多个进程之间的互斥。例如多电梯的输出环节,我就是在所有输出语句外套了一层synchronized(Handle.getInstance()),这样就能确保输出的正确性。另外synchronized还能将一个类的所有对象都锁柱,这样就互斥了所有访问同一个类的对象的线程,用法如下

1 synchronized(ClassName.class) {
2          // todo
3 }

  当然这几次作业都不需要将类锁住。

  在网上查了查,发现其实synchronized其实没有我们想象的那么万能,它也有几点缺陷,会影响效率,例如:

    1.由于我们没办法设置synchronized关键字在获取锁的时候等待时间,所以synchronized可能会导致线程为了加锁而无限期地处于阻塞状态。

    2.使用synchronized关键字等同于使用了互斥锁,即其他线程都无法获得锁对象的访问权。在读写问题中,这会造成读者不能同时进行读的操作,严重影响程序效率

  那么为了弥补这些功能上的不足,java也为我们提供了lock机制。lock相比于synchronized,用法灵活很多,这里就不赘述了。百度一下,相关内容很多

  2.设计原则

    2.1单例模式:

    这一模式我三次作业都用于共享对象的建立于管理,单例模式的意图就是保证一个类仅有一个实例,并提供一个访问它的全局访问点。当然单例模式也有它的优缺点

    优点:

      1.在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例

    缺点:

      1.没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。

    不过这三次作业为了保证线程安全,我没有过多考虑效率方面。于是就采用了单例模式

    2.2不可变对象:

    定义:对象一旦被创建它的状态(对象的数据,也即对象属性值)就不能改变,任何对它的改变都应该产生一个新的对象。例如java自带的String类型就是一个不可变对象。不可变对象优缺点如下

    优点:

      1.线程安全

      2.可以被重复使用

    缺点:

      1.每次对对象状态的变更都要新建一个对象,开销太大。

  3.线程安全类

    在网上查了查资料,发现java有的类是线程安全的,即对该类实例的操作是原子的,例如Vector,BlockingQueue。这些在访问的时候都不需要加锁。

  


 

   终于要和电梯告一段落了,初识多线程,还是有很多地方没有了解透彻,希望在接下来的学习中能努力巩固自己的知识网络。下一单元的OO,它会带给我惊喜还是惊讶呢,让我拭目以待!!

posted @ 2019-04-23 16:24  炼丹师zjh  阅读(152)  评论(0编辑  收藏  举报