知识点

相关文章

更多

最近更新

更多

一步一步掌握线程机制(五)---等待与通知机制

2019-03-13 16:17|来源: 网络

      在之前我们关于停止Thread的讨论中,曾经使用过设定标记done的做法,一旦done设置为true,线程就会结束,一旦为false,线程就会永远运行下去。这样做法会消耗掉许多CPU循环,是一种对内存不友好的行为。

      java中的对象不仅拥有锁,而且它们本身就可以通过调用相关方法使自己成为等待者和通知者。

     Object对象本身有两个方法:wait()和notify()。wait()会等待条件的发生,而notify()会通知正在等待的线程此条件已经发生,它们都必须从synchronized方法或块中调用。

     这种等待-通知机制的目的究竟是为何?

     等待-通知机制是一种同步机制,但它更像是一个通信机制,能够让一个线程与另一个线程在某个特定条件下进行通信。但是,该机制却没有指定特定条件是什么。

     等待-通知机制能否取代synchronized机制吗?当然不行,等待-通知机制并不会解决synchronized机制能够解决的竞争问题,实际上,这两者是相互配合使用的,而且它本身也存在竞争问题,这是需要通过synchronzied来解决的。

private boolean done = true;

public synchronized void run(){
      while(true){
            try{
                 if(done){
                       wait();
                 }else{
                       repaint();
                       wait(100);
                 }
            }catch(InterruptedException e){
                  return;
            }
      }
}

public synchronized void setDone(boolean b){
     done = b;
     if(timer == null){
          timer = new Thread(this);
          timer.start();
     }
     if(!done){
          notify();
     }
}

     这里的done已经不是volatile,因为我们不只是设定个标记值,我们还需要在设定标记的同时自动发送一个通知。所以,我们现在是通过synchronized来保护对done的访问。
     run()方法不会在done为false时自动退出,它会通过调用wait()方法让线程在这个方法中等待,直到其他线程调用notify()方法。

     这里有几个地方值得我们注意。

     首先,我们这里通过使用wait()方法而不是sleep()方法来使线程休眠,因为wait()方法需要线程持有该对象的同步锁,当wait()方法执行的时候,该锁就会被释放,而当收到通知的时候,线程需要在wait()方法返回前重新获得该锁,就好像一直都持有锁一样。这个技巧是因为在设定与发送通知以及测试与取得通知之间是存在竞争的,如果wait()和notify()在持有同步锁的同时没有被调用,是完全没有办法保证此通知会被接收到的,并且如果wait()方法在等待前没有释放掉锁,是不可能让notify()方法被调用到,因为它无法取得锁,这也是我们之所以使用wait()而不是sleep()的另一个原因。如果使用sleep()方法,此锁就永远不会被释放,setDone()方法也永远不会执行,通知也永远不会送出。

     接着就是这里我们对run()进行同步化。我们之前讨论过,对run()进行同步是非常危险的,因为run()方法是绝对不可能会完成的,也就是锁永远不会被释放,但是因为wait()本身就会释放掉锁,所以这个问题也被避免了。

     我们会有一个疑问:如果在notify()方法被调用的时候,没有线程在等待呢?

     等待-通知机制并不知道所送出通知的条件,它会假设通知在没有线程等待的时候是没有被收到的,因为这时它也只是返回且通知也被遗失掉,稍后执行wait()方法的线程就必须等待另一个通知。

     上面我们讲过,等待-通知机制本身也存在竞争问题,这真是一个讽刺:原本用来解决同步问题的机制本身竟然也存在同步问题!其实,竞争并不一定是个问题,只要它不引发问题就行。我们现在就来分析一下这里的竞争问题:

     使用wait()的线程会确认条件不存在,这通常是通过检查变量实现的,然后我们才调用wait()方法。当其他线程设立了该条件,通常也是通过设定同一个变量,才会调用notify()方法。竞争是发生在下列几种情况:

1.第一个线程测试条件并确认它需要等待;

2.第二个线程设定此条件;

3.第二个线程调用notify()方法,这并不会被收到,因为第一个线程还没有进入等待;

4.第一个线程调用wait()方法。

      这种竞争就需要同步锁来实现。我们必须取得锁以确保条件的检查和设定都是automic,也就是说检查和设定都必须处于锁的范围内。

      既然我们上面讲到,wait()方法会释放锁然后重新获取锁,那么是否会有竞争是发生在这段期间呢?理论上是会有,但系统会阻止这种情况。wait()方法与锁机制是紧密结合的,在等待的线程还没有进入准备好可以接收通知的状态前,对象的锁实际上是不会被释放的。

      我们的疑问还在继续:线程收到通知,是否就能保证条件被正确的设定呢?抱歉,答案不是。在调用wait()方法前,线程永远应该在持有同步锁时测试条件,在从wait()方法返回时,该线程永远应该重新测试条件以判断是否还需要等待,这是因为其他的线程同样也能够测试条件并判断出无需等待,然后处理由发出通知的线程所设定的有效数据。但这是在只有一个线程在等待通知,如果是多个线程在等待通知,就会发生竞争,而且这是等待-通知机制所无法解决的,因为它能解决的只是内部的竞争以防止通知的遗失。多线程等待最大的问题就是,当一个线程在其他线程收到通知后再收到通知,它无法保证这个通知是有效的,所以等待的线程必须提供选项以供检查状态,并在通知已经被处理的情形下返回到等待的状态,这也是我们为什么总是要将wait()放在循环里面的原因。

      wait()也会在它的线程被中断时提前返回,我们的程序也必须要处理该中断。

      在多线程通知中,我们如何确保正确的线程收到通知呢?答案是不行的,因为我们根本就无法保证哪一个线程能够收到通知,能够做到的方法就是所有等待的线程都会收到通知,这是通过notifyAll()实现的,但也不是真正的唤醒所有等待的线程,因为锁的问题,实质上所有的线程都会被唤醒,但是真正在执行的线程只有一个。

       之所以要这样做,可能是因为有一个以上的条件要等待,既然我们无法确保哪一个线程会被唤醒,那就干脆唤醒所有线程,然后由它们自己根据条件判断是否要执行。

       等待-通知机制可以和synchronized结合使用:

private Object doneLock = new Object();

public void run(){
     synchronized(doneLock){
           while(true){
                if(done){
                      doneLock.wait();
                }else{
                      repaint();
                      doneLock.wait(100);
                }
           }catch(InterruptedException e){
                 return;
           }
     }
}

public void setDone(boolean b){
     synchronized(doneLock){
          done = b;
          if(timer == null){
               timer = new Thread(this);
               timer.start();
          }
          if(!done){
                doneLock.notify();
          }
     }
}

     这个技巧是非常有用的,尤其是在具有许多对对象锁的竞争中,因为它能够在同一时间内让更多的线程去访问不同的方法。
     最后我们要介绍的是条件变量。

     J2SE5.0提供了Condition接口。Condition接口是绑定在Lock接口上的,就像等待-通知机制是绑定在同步锁上一样。

private Lock lock = new ReentrantLock();
private Condition  cv = lockvar.newCondition();

public void run(){
     try{
          lock.lock();
          while(true){
               try{
                   if(done){
                         cv.await();
                   }else{
                         nextCharacter();
                         cv.await(getPauseTime(), TimeUnit.MILLISECONDS);
                   }
               }catch(InterruptedException e){
                     return;
               }
          }
     }finally{
           lock.unlock();
     }
}

public void setDone(boolean b){
    try{
         lock.lock();
         done = b;
         if(!done){
               cv.signal();
         }finally{
               lock.unlock();
         }
    }
}

    上面的例子好像是在使用另一种方式来完成我们之前的等待-通知机制,实际上使用条件变量是有几个理由的:
1.条件变量在使用Lock对象时是必须的,因为Lock对象的wait()和notify()是无法运作的,因为这些方法已经在内部被用来实现Lock对象,更重要的是,持有Lock对象并不表示持有该对象的同步锁,因为Lock对象和对象所关联的同步锁是不同的。

2.Condition对象不像java的等待-通知机制,它是被创建成不同的对象,对每个Lock对象都可以创建一个以上的Condition对象,于是我们可以针对个别的线程或者一群线程进行独立的设定,也就是说,对同一个对象上所有被同步化的在等待的线程都得等待相同的条件。

      基本上,Condition接口的方法都是复制等待-通知机制,但是提供了避免被中断或者能以相对或绝对时间来指定时限的便利。

相关问答

更多
  • 会议强调,要用三项重点工作把六项任务统领起来。在做好群众工作中增强民族团结、保障改善民生,组织发动群众、凝聚民心民力;在加强基层组织中强化基层基础、转变干部作风,提升执政能力、构筑坚强堡垒;在推进“去宗教极端化”中促进宗教和谐、维护社会稳定,筑起铜墙铁壁、夯实维稳根基,不断把“访惠聚”活动引向深入。
  • 兄弟连JavaEE战狼班: 第一阶段:Java语言基础 ★ Java语言基础 1、面向对象思维JAVASE 2、(类加载机制与反射,annotation,泛型,网络编程,多线程,IO,异常处理,常用API,面向对象,JAVA编程基础) 3、Java8新特性 第二阶段:数据库 ★ 数据库 1、Oracle(SQL语句、SQL语句原理、SQL语句优化、表、视图 2、序列、索引、Oracle数据字典、Oracle 数据库PL/SQL开发 3、数据库设计原则、 MySQL 、 JDBC 第三阶段:Web基础 ★ W ...
  • 进入这个网址 http://www.verycd.com/topics/2767912/,把韩顺平老师的视频教程下下来学,很不错
  • 最后一个有好几个选项的么。具体是哪一个错了? 说清楚点,好“对症下药”
  • 先画身体,壮壮的! 还有‘恐龙'腿!要画粗一点哦。 紧跟着是尾巴!长一点。 添上眼睛和鼻子! 然后就是身上很有特色的恐龙背刺啦! 最后画上一棵树!不知它吃不吃树叶!
  • 去申请个空间然后再买个域名,然后按提示做就行了 空间1多块钱1兆,域名100元1年,还有各种其它的,你去找电脑老师了解了解,也有专门给人做网页的,不过有点贵。
  • MySQL也是很好安装的,就和APACHE的安装一样。但是大部分人包括我在内,都在配置的最后一步的第三小项目Start Service出现了错误。这十分令人懊恼。由于注册表的原因,我因此重装了系统,XP变身成为2003. MySQL之所以会出现这个错误,原因再简单不过——由于注册表里之前安装过的MySQL信息无法彻底删除导致新的MySQL不能启动。目前的解决方法似乎是将注册表内的旧信息删除,但是由于注册表的目录庞大,若要找到了再删除十分麻烦而且不一定能根除。我试过这一方法,却依然不能启动。最终只有重启了。不 ...
  • 有一定的数据库基础吗?没有的话,建议从SQL语句学起。比较好的教材是Oracle OCP认证的《SQL and PL/SQL》。学习SQL的时候,尽可能坚持使用Oracle自带的工具:SQLPLUS。  有了一定的SQL基础后,就要尽可能的了解Oracle的体系结构,这就涉及到了Oracle管理的内容了。《Oracle10g OCP认证手册》这本书不错。 不过,如果是初学者的话,不建议自己去摸索,因为这样往往会如盲人摸象,不仅会事倍功半,而且会有一些错误的概念。你可以去CUUG报个DBA就业培训班进行学习, ...
  • 使每个方法都采用completionHandler参数的典型示例,例如: func perform1(completionHandler: () -> Void) { doSomethingAsynchronously() { completionHandler() } } func perform2(completionHandler: () -> Void) { doSomethingElseAsynchronously() { completi ...