本节继续上一节的讨论。
[一个线程在进入对象的休息室(调用该对象的wait()方法)后会释放对该对象的锁],基于这个原因。在同步中,除非必要,否则你不应用使用Thread.sleep(long l)方法,因为sleep方法并不释放对象的锁。
这是一个极其恶劣的品德,你自己什么事也不干,进入sleep状态,却抓住竞争对象的监视锁不让其它需要该对象监视锁的线程运行,简单说是极端自私的一种行为。但我看到过很多程序员仍然有在同步方法中调用sleep的代码。
看下面的例子:
package debug; class SleepTest{ public synchronized void wantSleep(){ try{ Thread.sleep(1000*60); }catch(Exception e){} System.out.println("111"); } public synchronized void say(){ System.out.println("123"); } } class T1 extends Thread{ SleepTest st; public T1(SleepTest st){ this.st = st; } public void run(){ st.wantSleep(); } } class T2 extends Thread{ SleepTest st; public T2(SleepTest st){ this.st = st; } public void run(){ st.say(); } } public class Test { public static void main(String[] args) throws Exception{ SleepTest st = new SleepTest(); new T1(st).start(); new T2(st).start(); } }
我们看到,线程T1的实例运行后,当前线程抓住了st实例的锁,然后进入了sleep。直到它睡满60秒后才运行到System.out.println("111");然后run方法运行完成释放了对st的监视锁,线程T2的实例才得到运行的机会。
而如果我们把wantSleep方法改成:
public synchronized void wantSleep(){ try{ //Thread.sleep(1000*60); this.wait(1000*60); }catch(Exception e){} System.out.println("111"); }
我们看到,T2的实例所在的线程立即就得到了运行机会,首先打印了123,而T1的实例所在的线程仍然等待,直到等待60秒后运行到System.out.println("111");方法。
所以,调用wait(long l)方法不仅达到了阻塞当前线程规定时间内不运行,而且让其它有竞争需求的线程有了运行机会,这种利人不损己的方法,何乐而不为?这也是一个有良心的程序员应该遵循的原则。
当一个线程调用wait(long l)方法后,线程如果继续运行,你无法知道它是等待时间完成了还是在wait时被其它线程唤醒了,如果你非常在意它一定要等待足够的时间才执行某任务,而不希望是中途被唤醒,这里有一个不是非常准确的方法:
long l = System.System.currentTimeMillis(); wait(1000);//准备让当前线程等待1秒 while((System.System.currentTimeMillis() - l) < 1000)//执行到这里说明它还没有等待到1秒 //是让其它线程给闹醒了 wait(1000-(System.System.currentTimeMillis()-l));//继续等待余下的时间.
这种方法不是很准确,但基本上能达到目的。
所以在同步方法中,除非你明确知道自己在干什么,非要这么做的话,你没有理由使用sleep,wait方法足够达到你想要的目的。而如果你是一个很保守的人,看到上面这段话后,你对sleep方法深恶痛绝,坚决不用sleep了,那么在非同步的方法中(没有和其它线程竞争的对象),你想让当前线程阻塞一定时间后再运行,应该如何做呢?(这完全是一种卖弄,在非同步的方法中你就应该合理地应用sleep嘛,但如果你坚决不用sleep,那就这样来做吧)
public static mySleep(long l){ Object o = new Object(); synchronized(o){ try{ o.wait(l); }catch(Exception e){} } }
放心吧,没有人能在这个方法外调用o.notify[All],所以o.wait(l)会一直等到设定的时间才会运行完成。
虚拟锁简单说就是不要调用synchronized方法(它等同于synchronized(this))和不要调用synchronized(this),这样所有调用在这个实例上的所有同步方法的线程只能有一个线程可以运行。也就是说:
如果一个类有两个同步方法 m1,m2,那么不仅是两个以上线调用m1方法的线程只有一个能运行,就是两个分别调用m1,m2的线程也只有一个能运行。当然非同步方法不存在任何竞争,在一个线程获取该对象的监视锁后这个对象的非同步方法可以被任何线程调用。
而大多数时候,我们可能会出现这种情况,多个线程调用m1时需要保护一种资源,而多个线程调用M2时要保护的是另一种资源,如果我们把m1,m2都设成同步方法。两个分别调用这两个方法的线程其实并不产生冲突,但它们都要获取这个实例的锁(同步方法是同步this)而产生了不必要竞争。
所以这里应该采用虚拟锁。
即将m1和m2方法中各自保护的对象作为属性a1,a2传进来,然后将同步方法改为方法的同步块分别以a1,a2为参数,这样到少是不同线程调用这两个不同方法时不会产生竞争,当然如果m1,m2方法都操作同一受保护对象则两个方法还是应该作为同步方法。这也是应该将方法同步还是采用同步块的理由之一。
package debug; class SleepTest{ public synchronized void m1(){ System.out.println("111"); try{ Thread.sleep(10000); }catch(Exception e){} } public synchronized void m2(){ System.out.println("123"); } } class T1 extends Thread{ SleepTest st; public T1(SleepTest st){ this.st = st; } public void run(){ st.m1(); } } class T2 extends Thread{ SleepTest st; public T2(SleepTest st){ this.st = st; } public void run(){ st.m2(); } } public class Test { public static void main(String[] args) throws Exception{ SleepTest st = new SleepTest(); new T1(st).start(); new T2(st).start(); } }
这个例子可以看到两个线程分别调用st实例的m1和m2方法却因为都要获取st的监视锁而产生了竞争。T2实例要在T1运行完成后才能运行(间隔了10秒)。而假设m1方法要操作操作一个文件 f1,m2方法要操作一个文件f2,当然我们可以在方法中分别同步f1,f2,但现在还不知道f2,f2是否存在,如果不存在我们就同步了一个null对象,那么我们可以使用虚拟锁:
package debug; class SleepTest{ String vLock1 = "vLock1"; String vLock2 = "vLock2"; public void m1(){ synchronized(vLock1){ System.out.println("111"); try { Thread.sleep(10000); } catch (Exception e) {} //操作f1 } } public void m2(){ synchronized(vLock2){ System.out.println("123"); //操作f2 } } } class T1 extends Thread{ SleepTest st; public T1(SleepTest st){ this.st = st; } public void run(){ st.m1(); } } class T2 extends Thread{ SleepTest st; public T2(SleepTest st){ this.st = st; } public void run(){ st.m2(); } } public class Test { public static void main(String[] args) throws Exception{ SleepTest st = new SleepTest(); new T1(st).start(); new T2(st).start(); } }
我们看到两个分别调用m1和m2的线程由于它们获取不同对象的监视锁,它们没有任何竞争就正常运行,只有这两个线程同时调用m1或m2才会产生阻塞。
转载自dev2dev网友axman的go deep into java专栏。