「JAVA」线程生命周期分阶段详解,哲学家们深感死锁难解

每一个事物都有其生命周期,也就是事物从出生开始最终消亡这中间的整个过程;在其整个生命周期的历程中,会有不一样阶段,每一个阶段对应着一种状态,好比:人的一辈子会经历从婴幼儿、青少年、青壮年、中老年到最终死亡,离开这人世间,这是人一辈子的状态;一样的,线程做为一种事物,也有生命周期,在其生命周期中也存在着不一样的状态,不一样的状态之间还会有互相转换。java

人的生命周期

在上文中,咱们提到了线程通讯,在多线程系统中,不一样的线程执行不一样的任务;若是这些任务之间存在联系,那么执行这些任务的线程之间就必须可以通讯,共同协调完成系统任务。linux

在本文中,咱们接着来讲说线程通讯中的线程的生命周期。程序员

线程的生命周期

咱们先来查看jdk文档,在Java 中,线程有如下几个状态:编程

jdk 中的线程状态

Java 中,给定的时间点上,一个线程只能处于一种状态,上述图片中的这些状态都是虚拟机状态,并非操做系统的线程状态。线程对象的状态存放在Thread类的内部类(State)中,是一个枚举,存在着6种固定的状态:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATEDwindows

状态之间的转换以下图所示:服务器

线程状态运行流程

下面就来对这些状态一一解释:网络

1.新建状态(new): 使用new建立一个线程对象,仅仅在堆中分配内存空间,在调用start方法以前的线程所处的状态;在此状态下,线程还没启动,只是建立了一个线程对象存储在堆中;好比:多线程

Thread t = new Thread(); //  此时t就属于新建状态

当新建状态下的线程对象调用了start方法,该线程对象就重新建状态进入可运行状态(runnable);线程对象的start方法只能调用一次,屡次调用会发生IllegalThreadStateException并发

2.可运行状态(runnable):又能够细分红两种状态,readyrunning,分别表示就绪状态和运行状态ide

  • 就绪状态: 线程对象调用start方法以后,等待JVM的调度(此时该线程并无运行),还未开始运行;
  • 运行状态: 线程对象已得到JVM调度,处在运行中;若是存在多个CPU,那么容许多个线程并行运行;

就绪状态和运行状态

3.阻塞状态(blocked):处于运行中的线程由于某些缘由放弃CPU时间片,暂时中止运行,就会进入阻塞状态;此时JVM不会给线程分配CPU时间片,直到线程从新进入就绪状态(ready,才有可能转到运行状态;

阻塞状态只能先进入就绪状态,进而由操做系统转到运行状态,不能直接进入运行状态阻塞状态发生的两种状况:

  1. A线程处于运行中,试图获取同步锁时,但同步锁却被B线程获取,此时JVM会把A线程存到共享资源对象的锁池中,A线程进入阻塞状态;
  2. 当线程处于运行状态,发出了IO请求时,该线程会进入阻塞状态;

4.等待状态(waiting):运行中的线程调用了wait方法(无参数的wait方法),而后JVM会把该线程储存到共享资源的对象等待池中,该线程进入等待状态;处于该状态中的线程只能被其余线程唤醒;

5.计时等待状态(timed waiting):运行中的线程调用了带参数的wait方法或者sleep方法,此状态下的线程不会释放同步锁/同步监听器,如下几种状况都会进入计时等待状态:

  1. 当处于运行中的线程,调用了wait(long time)方法,JVM会把当前线程存在共享资源对象等待池中,线程进入计时等待状态;
  2. 当前线程执行了sleep(long time)方法,该线程进入计时等待状态;

6.终止状态(terminated):也能够称为死亡状态,表示线程终止,它的生命走到了尽头;线程一旦终止,就不能再重启启动,不然会发生IllegalThreadStateException;有如下几种状况线程会进入终止状态:

  1. 正常执行完run方法而退出,寿终正寝,属于正常死亡;
  2. 线程执行遇到异常而退出,线程中断,属于意外死亡;

线程控制

线程休眠:让运行中的的线程暂停一段时间,进入计时等待状态

方法:static void sleep(long millis)

调用sleep后,当前线程放弃CPU时间片,进入计时等待状态,在指定时间段以内,调用sleep的线程不会得到执行的机会,此状态下的线程不会释放同步锁/同步监听器

该方法更多的用于模拟网络延迟,让多线程并发访问同一个资源的错误效果更明显;也有让程序的执行便于观察的调用:

public static void main(String[] args) {                
    for (int i = 5; i > 0; i-- ) {                    
        System.out.println("还剩 " + i + " 秒");                  
        Thread.sleep(1000);        
    }              
    System.out.println("时间到");  
}

联合线程

线程的 join方法 表示一个线程等待另外一个线程完成后才执行;join方法被调用以后,调用join方法的线程对象所在的线程处于阻塞状态,调用join方法的线程对象进入运行状态。之因此把这种方式称为联合线程,是由于经过join方法把当前线程和当前线程所在的线程联合成一个线程。

public class JoinThreadDemo {
    public static void main(String []args) throws Exception {
           System.out.println("开始");
        JoinThread join = new JoinThread();
        for (int i = 0; i < 50; i++) {
            System.out.println("i : " + i);
            if ( i == 10) {
                join.start();
            }
            if (i == 20) {
                join.join();
            }
        }
        System.out.println("结束");
    }
}

class JoinThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            System.out.println("join : " + i);
        }
    }
}

运行结果打印以下:

联合线程运行结果

能够看到,当i = 20时,join线程对象开始执行,主线程(主函数)进入阻塞状态,暂停执行;join线程对象运行完成后,主线程(主函数)才从新开始执行。那为啥i=10时,join线程对象没有执行呢?缘由是虽然join线程对象调用了start方法,但还未得到JVM调度,因此没有执行。

join方法

后台线程

后台线程,在后台运行的线程,其目的是为其余线程提供服务,也称为“守护线程"。JVM的垃圾回收线程就是典型的后台线程。

Java 中,开发者们经过代码建立的线程默认都是前台线程,若是想要转为后台线程能够经过调用 setDaemon(true) 来实现,该方法必须在start方法以前调用,不然会触发 IllegalThreadStateException 异常,由于线程一旦启动,就没法对其作修改了。

setDaenon 方法和 isDaemon 方法

由前台线程建立的新线程除非特别设置,不然都是前台线程,同理,后台线程建立的新线程也是后台线程。如果不知道某个线程是前台线程仍是后台线程,可经过线程对象调用 isDaemon() 方法来判断。

若全部的前台线程都死亡,后台线程自动死亡,如果前台线程没有结束,后台线程是不会结束的。

后台线程代码示例

线程优先级

每一个线程都有优先级,优先级的高低只与线程得到执行机会的次数多少有关,并不是是线程优先级越高的就必定先执行,由于哪一个线程的先运行取决于CPU的调度,没法经过代码控制。

Java 中,支持了从1 - 1010个优先级,1是最低优先级,10是最高优先级,默认优先级是5jdk 文档中的线程优先级以下图所示:

线程优先级

  • MAX_PRIORITY=10,最高优先级
  • MIN_PRIORITY=1,最低优先级
  • NORM_PRIORITY=5,默认优先级

JavaThread类中提供了获取、设置线程优先级的方法:

int getPriority() :返回线程的优先级

getPriority() :返回线程的优先级

void setPriority(int newPriority) : 设置线程的优先级
setPriority(int newPriority) : 设置线程的优先级

每一个线程在建立时都有默认优先级,主线程默认优先级为5,若是A线程建立了B线程,那么B线程A线程具备相同优先级;虽然Java 中可设置的优先级有10个,但不一样的操做系统支持的线程优先级不一样的,windows支持的,linux不见得支持;因此,通常状况下,不建议自定义,建议使用上述Thread类中提供的三个优先级,由于这三个优先级各个操做系统均支持。

线程礼让

yield方法:表示当前线程对象提示调度器本身愿意让出CPU时间片,可是调度器却不必定会采纳,由于调度器一样也有选择是否采纳的自由,他能够选择忽略该提示。

yield方法

调用该方法以后,线程对象进入就绪状态,因此彻底有可能出现某个线程调用了yield()以后,线程调度器又把它调度出来从新执行。

在开发中不多会使用到该方法,该方法主要用于调试或者测试,好比在多线程竞争条件下,让错误重现现象或者更加明显。

sleep方法yield方法的区别:

  1. 共同点是都能使当前处于运行状态的线程放弃CPU时间片,把运行的机会给其余线程;
  2. 不一样点在于:sleep方法会给其余线程运行机会,可是并不会在乎其余线程的优先级;而yield方法只会给相同优先级或者更高优先级的线程运行的机会;
  3. 调用sleep方法

后,线程进入计时等待状态,而调用yield方法后,线程进入就绪状态

线程组

Java 中,ThreadGroup类表示线程组,能够对属于同组的线程进行集中管理,在建立线程对象时,能够经过构造器指定其所属的线程组。

Thread(ThreadGroup group,String name);

若是A线程建立了B线程,若是没有设置B线程的分组,那么B线程会默认加入到A线程的线程组;一旦线程加入某个线程组,该线程就会一直存在于该线程组中直至该线程死亡,也就是说一个线程只能有存在于一个线程组中,在一个线程的整个生命周期中,线程组一经设定,便不能中途修改。当Java程序运行时,JVM会建立名为main的线程组,在默认状况下,全部的线程都归属于该改线程组下。

线程组和定时器

在JDK的java.util包中提供了Timer类,使用此类能够定时执行特定的任务;

Timer 中的定时执行方法

其中有几个经常使用的方法:

// 将指定的任务(task)安排在指定的时间(time)执行
void schedule(TimerTask task, Date time)

// 从指定的时间(firstTime)开始,按照某一周期(period),重复执行定时任务(task)
void    schedule(TimerTask task, Date firstTime, long period)

// 指定的任务在(task)必定的延时(delay)后执行
void    schedule(TimerTask task, long delay)

// 指定的任务(task),在必定的延时(delay)后,按必定周期(period)重复执行
void    schedule(TimerTask task, long delay, long period)

TimerTask类表示定时器执行的某一项任务;

TimerTask类

经过jdk 文档中的描述,不难发现,TimerTask其实也是一个线程,具备线程的属性和操做,因此经过前几篇文章的介绍,这个类的使用已经很熟悉了。就再也不这里赘述了。

线程死锁

线程死锁,A线程在等待由B线程持有的锁时,而B线程也在等待A线程持有的锁,此时,这种线程现象称为线程死锁;因为JVM不检测也不试图避免这种状况的发生,因此程序员必需要避免死锁的发生。

多线程通讯的时候很容易形成死锁,线程死锁没法解决,只能避免; 当多个线程都要访问共享的资源A、B、C时,要保证每个线程都按照相同的顺序去访问他们,好比都先访问A,而后是B,最后才是C

Java 的Thread类存在一些因死锁被废弃过期的方法

  • suspend(): 让正在运行的线程放弃CPU时间片,暂停运行;
  • resume(): 让暂停的线程恢复运行;

由上述两个方法可能致使的的死锁状况:

假设有A、B两个线程,首先A线程得到对象锁,正在执行一个同步方法,若是B线程调用A线程suspend方法,此时A线程会暂停运行,并放弃CPU时间片,可是并不会释放拥有的锁,从而致使A、B两个线程都处于等待中;B在等待A释放锁,而A已暂停,没办法释放锁;这样就会出现不管A、B哪一个线程都不能得到锁。

哲学家就餐问题

哲学家就餐的问题也是一个描述死锁很好的例子,如下是问题描述(内容来源于百度百科):

假设有五位哲学家围坐在一张圆形餐桌旁,作如下两件事情之一:吃饭,或者思考。吃东西的时候,他们就中止思考,思考的时候也中止吃东西。餐桌中间有一大碗意大利面(也能够是其余的食物,好比:米饭,由于吃米饭必须用两根筷子),每两个哲学家之间有一只餐叉。由于用一只餐叉很难吃到意大利面,因此假设哲学家必须用两只餐叉吃东西,且他们只能使用本身左右手两边的那两只餐叉

哲学家历来不交谈,这就颇有可能产生死锁,出现:每一个哲学家都拿着左手的餐叉,永远都在等右边的餐叉(或者相反),永远都吃不到东西,最后饿死。即便没有死锁,也颇有可能耗尽服务器资源。

假设规定当哲学家等待另外一只餐叉超过五分钟后就放下本身手里的那一只餐叉,而且再等五分钟后进行下一次尝试。这个策略消除了死锁(系统总会进入到下一个状态),但又会产生新的问题:若是五位哲学家在彻底相同的时刻进入餐厅,并同时拿起左边或者右边的餐叉,那么这些哲学家就会同时等待五分钟,同时放下手中的餐叉,又再等五分钟,哲学家任然会饿死

哲学家就餐问题

在实际的计算机问题中,缺少餐叉能够类比为缺少共享资源。一种经常使用的计算机技术是资源加锁,保证在某个时刻,资源只能被一个程序或一段代码访问;当一个程序想要使用的资源已经被另外一个程序锁定,它就等待资源解锁。可是当多个程序涉及到加锁的资源时,在某些状况下仍然可能发生死锁。例如,某个程序须要访问两个文件,访问了其中一个文件,另一个文件被其余的线程锁定,这两个程序都在等待对方解锁另外一个文件,但这永远不会发生。

因此在Java 多线程开发中,尽可能避免死锁问题,由于发生这样的问题真的很头疼。尽可能多熟悉,多实践多线程中的理论和操做,从一次次的成功案例中体会Java 多线程设计的魅力。

完结。老夫虽不正经,但老夫一身的才华!关注我,获取更多编程科技知识。

相关文章
相关标签/搜索