对于线程的编程,同步已经不在算什么技巧了,而是基础。所以在阅读本篇之前,希望对同步有很深刻的认识。而本篇是朝着技巧来的,将会在今后的阐述中以此为主线将一些简单但常见的线程编程问题解释一下。
这段代码虽然短小精悍,但是在多线程编程中极为有用。这里举个简单的例子以助于理解。比如公司桌上有个盒子,有些人将自己带的吃的放进去,有些人则拿出自 己想吃的东西。这时候其实我们是有这样的约束条件的:盒子不空则能取出东西吃,盒子不满则能向盒子中放东西。这个约束条件就是这里的 doCondition,套用过来就是说盒子空了就要等待直到盒子里有东西才能取出来吃,同样,盒子满了就要等待空出一个位置才能放东西进去。于是把盒子 作为一个资源临界区,我们有如下的代码:
public class Box{ ...... public synchronized void put(Object something){ while(isFull){//can't put try{ wait(); } catch(InterruptedException){ } } doPut(something); notifyAll(); } public synchronized Object get(){ while(isEmpty){ try{ wait(); } catch(InterruptedException){ } } Object something = doGet(); notifyAll(); return something; } ...... } |
我们来分析一下get,如果盒子是空的话线程就一直等待,而要线程继续执行循环之外的语句其实有两个条件,条 件一:有其它线程将其唤醒,条件二:唤醒后盒子至少有一件东西在里面。这里用这个while循环保证了这两个条件。之后才是事实上的从盒子去东西出来,然 后有一个notifyAll(),在这里的意思是对那些准备put却因为盒子满的线程而做的,将所有线程唤醒,一旦那些put线程得到控制权那么一定满足 两个条件第一是被唤醒了第二至少有一个空间能放东西。循环锁保证了这一整个过程的安全性,因此这是个非常重要的技巧。
试想,如果while改成if还安全吗?当然不安全了,假设当前存在几个线程同时在get上 等待(这是可能的,假设盒子已为空),这时一个线程put并唤醒了全部等待的线程,第一个线程没有检查就开始执行doGet()(因为用了if),庆幸的 是它是正确的,但第二个线程同样没检查就doGet(),由于被第一个线程把盒子最后一件物品也取走了,所以第二个线程是不可能取到东西的,这时就会报错 了,因此换成if是不安全的,这也正式循环锁的意义锁在。
那如果将try{}catch提到循环外呢?这个程序还是安全的吗?答案仍然是否定的。解释 其实同上面一样,还记得我强调过InterruptedException这个异常,一旦被唤醒,是从catch后的下一个语句开始执行,如果try在 while里,catch后的下一个语句其实是while的条件判断;如果try在while外面,catch的下一个语句就是直接执行doGet,其实 就成了刚刚那种情况。
还有一点,循环中的wait也不能变成sleep,之前特别强调过wait在进入之前会获得对象锁,而进入后就释放了,因此能够给其他线程进入这段代码的 机会,而sleep则是在睡觉过程中还死握着锁不放,不妨将上例两个wait换成sleep,会发现开始线程就一直握着锁,醒了判断条件一定不满足,然后 又睡,又醒……它们被放在了死循环中,因为循环条件需要另一个线程持锁完成,可锁却被自己紧紧握着。
3.条件锁。
还用上例做例子,比如我可以将get方法改成如下:
public synchronized Object get(){ if(isEmpty) return; Object something = doGet(); return something; } |
从形式上看跟上例也差不多,只是如果判断出不符合条件就返回回去,而不是让线程在这里空等。而返回去的线程也可以根据需要先做点别的事情,从而提高了吞吐和相应。
当然,这里只是使用这个相同的例子打个比方,使得思路有连贯性,如果真要用if来做这个问题,还需要改动别处的代码,并且对此例来说不如循环锁方便。这里也总结下代码的框架如下:
if(!doCondition) return; doSomething(); |