昨天oo第五次作业,在ddl前半小时发现了一个bug,我猜到可能与sunchronized同步互斥的范围没有调整好有关系,但是通过IDEA的debug模式和JProfile的视图都没有很快找出bug。最后不出所料的话,这次的作业又将是一片红。以后OO作业一定要周二晚或者周三早上就开始,在周五中午前要完成代码。剩余的一天半用来构建测评机和debug。
再说一说,我最后是用print大法找出了bug。其实我想到用print之后,很快就发现了其实原因如下。
private LinkedBlockingQueue<Person> pout;
private LinkedBlockingQueue<Instruction> instrs;
private Elevator elevator;
...
synchronized (pout) {
while (hasPerson()) {
try {
look();
pout.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
pout.notifyAll();
}
...
本次我的设计中,只有input.run方法, elevator实例的executeInstuction方法和controller实例的look方法能改变pin, pout 和 instrs这三个LinkedBlockingQueue队列的值。
当hasPerson()为True时,进入while循环体,look方法有elevator的锁保证同步互斥。但是仍然有可能出错:进入循环体后,在进入look方法前,elevator进程的executeInstruction方法占用了elevator锁,然后将pout清空,之后再释放锁,当look方法得到elevator锁时,会发现pout队列已经是空的,因此执行可能出错。
我们再看看另一个多线程导致的错误。
例如执行以下方法add()时,可能实际虚拟机执行的是注释里的三行代码,如果这时同时用两个进程A\B都调用了同一个实例的add方法,那么如果执行顺序是A1, B1, A2, A3, B2, B3, 那么最后cnt的值只加了一而不是预料中的加二。
...
public add() {
this.cnt =+ 1;
// int a = this.cnt;
// a = a + 1;
// this.cnt = a;
}
我们再想想,多线程为什么容易出bug?原因是多个线程同时修改或者访问某一个变量(实例域),而结果与执行顺序相关,而执行顺序本就是不确定的,导致实际操作不符合我们预料中的逻辑。
反过来说两个线程结果确定性的充分条件是:两个线程的要写的变量没有交集;并且一个线程要写的变量和另一个线程要读的变量没有交集。
但还有导致结果错误的关键的一环没有说出来: 多个进程访问同一变量,导致整个代码块的逻辑不能顺畅被执行,因此导致错误。
因此,避免多线程错误的基本方法就是:将前后逻辑相关的代码块整个划分出来,并保证这个代码块的原子性。
我在这次作业中犯的错误就是判断hasPeron与look方法是一个完整的逻辑,我却将其分为了两部分,只给look方法上了锁。修改如下:
private LinkedBlockingQueue<Person> pout;
private LinkedBlockingQueue<Instruction> instrs;
private Elevator elevator;
...
synchronized (pout) {
while (True) {
synchronized (elevator) {
if (hasPerson) {
try {
look();
pout.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
pout.notifyAll();
}
...
小结:多线程debug方法:
- IDEA的debug功能
- JProfile的识图
- 出现前两者难以发现的bug时,可以尝试print大法
下一章将讲述具体怎么保证代码块的原子性.