Java线程的生命周期和状态控制

来源:互联网 发布:淘宝上分期付款买手机 编辑:程序博客网 时间:2024/05/16 23:59

线程的生命周期

新建状态

用new关键字和Thread类或其子类建立一个线程对象后,该线程对象就处于新建状态。处于新生状态的线程有自己的内存空间,通过调用start()进入就绪状态

  • 注意:不能对已经启动的线程再次调用start(),否则会出现java.lang.IllegalThreadStateException异常。

就绪状态

处于就绪状态的线程已经具备了运行条件,但还没有分配到CPU,处于线程就绪队列(尽管是采用队列形式,事实上,把它称为可运行池而不是可运行队列。因为cpu的调度不一定是按照先进先出的顺序来调度的),等待系统为其分配CPU。等待状态并不是执行状态,当系统选定一个等待执行的Thread对象后,它就会从等待执行状态进入执行状态,系统挑选的动作称之为“cpu调度”。一旦获得CPU,线程就进入运行状态并自动调用自己的run方法。

运行状态

处于运行状态的线程最为复杂,它可以变为阻塞状态就绪状态死亡状态

处于就绪状态的线程,如果获得了cpu的调度,就会从就绪状态变为运行状态,执行run()方法中的任务。
如果该线程失去了cpu资源,就会又从运行状态变为就绪状态。重新等待系统分配资源。
也可以对在运行状态的线程调用yield()方法,它就会让出cpu资源,再次变为就绪状态。

当发生如下情况时,线程会从运行状态变为阻塞状态

  • 线程调用sleep方法主动放弃所占用的系统资源
  • 线程调用一个阻塞式IO方法,在该方法返回之前,该线程被阻塞
  • 线程试图获得一个同步监视器,但更改同步监视器正被其他线程所持有
  • 线程在等待某个通知(notify)
  • 程序调用了线程的suspend方法将线程挂起。不过该方法容易导致死锁,所以程序应该尽量避免使用该方法

当线程的run()方法执行完,或者被强制性地终止,例如出现异常,或者调用了stop()、desyory()方法等等,就会从运行状态转变为死亡状态

阻塞状态

处于运行状态的线程在某些情况下,如执行了sleep(睡眠)方法,或等待I/O设备等资源,将让出CPU并暂时停止自己的运行,进入阻塞状态。

在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续运行:

  • 调用sleep()方法的线程经过了指定时间
  • 线程调用的阻塞式IO方法已经返回
  • 线程成功的获得了试图取得的同步监视器
  • 线程正在等待的某个通知时,其他线程发出了一个通知(signal)
  • 处于挂起状态的线程被调用了resume()恢复方法

死亡状态

当线程的run()方法执行完,或者被强制性地终止,就认为它死去。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生。 如果在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。

生命周期图
这里写图片描述

状态控制

线程睡眠——sleep()

如果我们需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread的sleep方法。

注意
1.sleep是静态方法,最好不要用Thread的实例对象调用它,因为它睡眠的始终是当前正在运行的线程,而不是调用它的线程对象,它只对正在运行状态的线程对象有效,看如下例子:

    public static void main(String[] args) throws InterruptedException {          System.out.println(Thread.currentThread().getName());          MyThread myThread=new MyThread();          myThread.start();          myThread.sleep(1000);//这里sleep的就是main线程,而非myThread线程          Thread.sleep(10);          for(int i=0;i<100;i++){              System.out.println("main"+i);          }      } 

2.Java线程调度是Java多线程的核心,只有良好的调度,才能充分发挥系统的性能,提高程序的执行效率。但是不管程序员怎么编写调度,只能最大限度的影响线程执行的次序,而不能做到精准控制

因为使用sleep方法之后,线程是进入阻塞状态的,只有当睡眠的时间结束,才会重新进入到就绪状态,而就绪状态进入到运行状态,是由系统控制的,我们不可能精准的去干涉它,所以如果调用Thread.sleep(1000)使得线程睡眠1秒,可能结果会大于1秒。

线程让步——yield()

yield()方法和sleep()方法有点相似,它也是Thread类提供的一个静态的方法,它也可以让当前正在执行的线程暂停,让出cpu资源给其他的线程。

但是和sleep()方法不同的是,它不会进入到阻塞状态,而是进入到就绪状态。yield()方法只是让当前线程暂停一下,重新进入就绪的线程池中,让系统的线程调度器重新调度器重新调度一次,完全可能出现这样的情况:当某个线程调用yield()方法之后,线程调度器又将其调度出来重新进入到运行状态执行。

线程合并——join()

线程的合并的含义就是将几个并行线程的线程合并为一个单线程执行,应用场景是当一个线程必须等待另一个线程执行完毕才能执行时,Thread类提供了join方法来完成这个功能,注意,它不是静态方法。

线程的优先级

每个线程执行时都有一个优先级的属性,优先级高的线程可以获得较多的执行机会,而优先级低的线程则获得较少的执行机会。与线程休眠类似,线程的优先级仍然无法保障线程的执行次序。只不过,优先级高的线程获取CPU资源的概率较大,优先级低的也并非没机会执行。

每个线程默认的优先级都与创建它的父线程具有相同的优先级,在默认情况下,main线程具有普通优先级。

守护线程

守护线程与普通线程写法上基本么啥区别,调用线程对象的方法setDaemon(true),则可以将其设置为守护线程。

所谓守护线程,是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。

守护线程使用的情况较少,但并非无用,举例来说,JVM的垃圾回收、内存管理等线程都是守护线程。还有就是在做数据库应用时候,使用的数据库连接池,连接池本身也包含着很多后台线程,监控连接个数、超时时间、状态等等。

wait(),notify()和notifyAll()

首先这三个方法必须在同步方法或同步块中调用。

然后介绍两个概念:锁池等待池

锁池

假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。

等待池

假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁后,进入到了该对象的等待池中。

wait()

如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。

notify()和notifyAll()

当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁

也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。

综上,所谓唤醒线程,另一种解释可以说是将线程由等待池移动到锁池,notifyAll调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而notify只会唤醒一个线程。

wait()与sleep()的区别

  • wait方法是Object类提供的,而sleep方法是Thread类提供的。

  • wait方法必须在同步锁中使用,且wait后,线程释放了CPU资源与锁资源。而sleep方法,没有限定必须在同步锁中使用。且sleep后,只会释放CPU资源,而不会释放同步锁资源。

参考:
1.Java多线程(二)、线程的生命周期和状态控制
2.java中的notify和notifyAll有什么区别?
3.Java多线程学习(三)—线程的生命周期
4.线程间协作:wait、notify、notifyAll

阅读全文
1 0
原创粉丝点击