首页 课程 师资 教程 报名

Java高级编程实用教程,如何使用线程的休眠

  • 2019-08-12 10:42:43
  • 515次 动力节点

  线程休眠和中断


  我们知道了在编程过程中创建线程,并启动以后,线程会交由操作系统来管理调度执行一个我们指定的计算任务。


  如果没有其它异常情况出现的话,它会持续运行直到我们实现的run()方法执行完毕为止。


  那么我们通常在什么情况下需要创建单独的线程呢?一般是在我们的程序需要执行一个比较耗时的任务,或者需要一个程序不断的重复执行某种动作,这些基本上都属于我们程序的后台来负责处理的问题,比如检查某种状态或者动作,而不影响其它任务的执行时,需要创建单独的线程来操作。


  耗时较长的工作比如需要扫描某个特定的目录结构中所有文件,到数据库中查询一个大容量的数据表等。


  需要持续的周期性间隔的查看某个状态和结果的任务,虽然我们可以直接在借助while循环来执行,但是如此会浪费太多的CPU时间,因为我们的CPU会一次又一次的被切换占用来执行它。这时我们需要让监控循环能够等待一定时间间隔再检查,从而减少CPU无效时间的占用。


  对于此类用例,更好的方法是让执行监控任务的线程能够暂停执行特定间隔,再次期间CPU可以去处理其它计算任务。


  这种情况我们可以调用类java.lang.Thread的sleep()方法来实现,如下例所示:


image.png

  调用sleep()使当前线程进入睡眠状态,而不消耗任何处理时间。


  该方法被调用意味着当前线程从活动线程列表中删除自己,调度程序在下一次执行时不会调度它直到指定的毫秒数过后才会再次调度它执行。


  这里需要注意的是我们传递给sleep()方法的时间只能是作为调度程序的一个指示,实际的执行不一定会绝对准确的按照指定的时间进行。


  主要是因为操作系统在执行调度时,实际执行时可能会出现线程提前或延迟几纳秒或几毫秒返回的情况。


  因此,我们不建议将sleep()方法用于对时间精度有比较高要求的实时调度上。但是对于大多数情况来说,在这种纳秒或毫秒级别达到的精度已经足够了。


  线程的中断现象


  因为中断是操作系统调度CPU任务执行的一个重要机制,是线程交互的一个非常基本的特性,可以理解为一个线程向另一个线程发送的简单中断消息。


  我们知道中断机制是通过中断异常来提供处理入口的。


  所以,当我们在一个线程上调用sleep()方法使其处于睡眠状态时,它可能会被中断,表现为抛出InterruptedException异常。


  在上面的代码示例中,您可能已经注意到sleep()可能抛出InterruptedException。


  而关联线程可以通过调用Thread.interrupted()方法来检查自己是否被中断了。


  或者当它将时间花费在sleep()这样的方法中时就是隐式的中断,也会在中断时抛出异常。


  让我们用下面的代码例子来仔细看看中断:

QQ截图20190812103901.png

  简单分析一下上面的示例代码,我们在main方法中,首先启动一个新线程myThread,如果不中断它,它将休眠很长时间(大约290年)。


  这当然不能这样下去,所以为了能提前完成程序执行,myThread线程通过在main方法中调用它的实例方法interrupt()来执行中断。


  由于myThread线程从开始执行就处在sleep()调用中,调用interrupt()方法结果导致InterruptedException异常发生,被catch捕获,从而在控制台上打印为“myThread线程被异常中断!”


  然后继续执行while循环检查该线程是否被中断,如果没有就执行空循环,如果被中断就继续执行完该线程任务,即执行打印“myThread线程被第二次中断”。


  我们在主线程中多次执行sleep()方法是给予子线程执行机会。


  在记录了异常之后,线程会忙着等待,直到设置了线程上的中断标志。


  这也是通过调用线程实例变量上的interrupt()从主线程设置的。


  总的来说,我们看到控制台的输出如下:

image.png

  从输出中可以看到,调度程序在再次启动myThread之前已经执行了主线程。


  因此,myThread在主线程开始休眠后打印出异常的接收。


  其实,在使用多个线程编程时,通常我们会发现线程的日志输出在某种程度上难以预测,因为很难计算接下来执行哪个线程。


  当我们必须处理更多的线程时,我们就会发现而这些线程的暂停并没有像上面的示例中那样符合预期,常常情况会变得很糟。


  在这些情况下,整个程序得到某种内部动态,这使得并发编程成为一项具有挑战性的任务。


  join线程


  前面我们看到的,在线程执行过程中,我们可以让线程休眠,直到它被另一个线程唤醒。这能够充分的利用CPU的处理时间。


  由于在大量线程同时执行时,线程的暂停已经无法准确的调度线程执行,所以迫切需要线程的另一个重要特性就是线程能够等待另一个线程的终止。


  也就是说将线程按照某种先后顺序排列起来中。


  这里假设我们必须实现某种数字处理操作,该操作可以划分为多个并行运行的线程。


  启动所谓工作线程的主线程必须等待,直到所有子线程都终止。


  在我们的主方法中,我们创建了一个由5个线程组成的数组,这些线程都是一个接一个地启动的。

image.png

  我们先看一下在不使用join()方法的执行情况:

image.png

  我们可以看到,主线程率先执行完成,然后才是各个子线程依次完成。


  而当我们增加Join调用时:

image.png

  一旦启动它们,我们就在主线程中等待它们的终止。线程本身通过计算一个接一个的随机数来模拟一些数字处理。


  一旦完成,就打印出“线程结束!”。最后主线程承认所有子线程的终止:

image.png

  我们将观察到,“完成”消息的顺序因执行而异。如果多次执行该程序,我们可能会发现最先完成的线程并不总是相同的。但是,最后一条语句始终是等待它的子线程的主线程。


  观察上面两次执行结果,我们发现join()的作用就是让主线程等待子线程的执行,而不是采用使主线程休眠的方式。


  这种处理主要用在在某些情况下,我们将不得不等待线程的结束。例如,我们可能有一个程序,它将开始初始化它需要的资源,然后再执行其余的执行。我们可以用多个独立线程的形式运行初始化任务,并等待这些初始化线程执行完成后再开始继续执行程序的其余部分。


  为此,我们可以使用Thread类的join()方法。当我们使用Thread对象调用此方法时,它将暂停调用线程的执行,直到被调用的对象完成执行。


  总结


  对于线程的调度管理,始终是并发编程中最重要的课题,通过上面的说明我们一定要记住,sleep()虽然能够让线程休眠,但是我们提供的休眠时间参数只是个参考值,并不能精确的被采用,而join()方法,主要用于让父线程等待子线程结束之后才能继续运行。


选你想看

你适合学Java吗?4大专业测评方法

代码逻辑 吸收能力 技术学习能力 综合素质

先测评确定适合在学习

在线申请免费测试名额
价值998元实验班免费学
姓名
手机
提交