Java多线程之线程状态和关键字讲解

1 线程生命周期状态

1.1 进程和线程概念

1.1.1 Java调度模式

现在的操作系统是多任务操作系统。多线程是实现多任务的一种方式

Java编写程序都运行在在Java虚拟机(JVM)中,在JVM的内部,程序的多任务是通过线程来实现的。每用java命 令启动一个java应用程序,就会启动一个JVM进程。在同一个JVM进程中,有且只有一个进程,就是它自己。在这个JVM环境中,所有程序代码的运行都是以线程来运行

Java程序中,JVM负责线程的调度。线程调度是值按照特定的机制为多个线程分配CPU的使用权。
调度的模式有两种:分时调度抢占式调度。分时调度是所有线程轮流获得CPU使用权,并平均分配每个线程占用CPU的时间;抢占式调度是根据线程的优先级别来获取CPU的使用权。JVM的线程调度模式采用了抢占式模式

  • 抢占式调度:指的是每条线程执行的时间、线程的切换都由系统控制,系统控制指的是在系统某种运行机制下,可能每条线程都分同样的执行时间片,也可能是某些线程执行的时间片较长,甚至某些线程得不到执行的时间片。在这种机制下,一个线程的堵塞不会导致整个进程堵塞。
    java使用的线程调度使用抢占式调度Java中线程会按优先级分配CPU时间片运行,且优先级越高越优先执行,但优先级高并不代表能独自占用执行时间片,可能是优先级高得到越多的执行时间片,反之,优先级低的分到的执行时间少但不会分配不到执行时间。
  • 协同式调度:指某一线程执行完后主动通知系统切换到另一线程上执行,这种模式就像接力赛一样,一个人跑完自己的路程就把接力棒交接给下一个人,下个人继续往下跑。线程的执行时间由线程本身控制,线程切换可以预知,不存在多线程同步问题,但它有一个致命弱点:如果一个线程编写有问题,运行到一半就一直堵塞,那么可能导致整个系统崩溃。

1.1.2 进程和线程区别

进程与线程的区别总结:

  • 本质区别进程操作系统 资源分配和调度的一个独立单位,而线程是cpu处理器 任务调度和执行的基本单位。
  • 包含关系: 一个进程至少有一个线程,线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
  • 资源开销: 每个进程都有独立的内存地址空间,进程之间的切换会有较大的开销;线程可以看做轻量级的进程,同一个进程内的线程共享进程的地址空间,每个线程都有自己独立的运行栈程序计数器,线程之间切换的开销小。
    资源分配给进程,同一进程内的所有线程共享该进程的所有资源。 同一进程中的多个线程 共享 代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量。
  • 通信关系:处理器分给线程,即真正在处理器上运行的是线程。线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步
  • 影响关系: 一个进程崩溃后,在保护模式下其他进程不会被影响,但是一个线程崩溃可能导致整个进程被操作系统杀掉,所以多进程要比多线程健壮。

1.2 线程生命状态

线程生命状态图示:
在这里插入图片描述
线程生命状态

  1. 新建状态
    new关键字和Thread类或其子类建立一个线程对象后,该线程对象就处于新生状态。处于新生状态的线程有自己的内存空间,通过调用start方法进入就绪状态(runnable)。
    注意:不能对已经启动的线程再次调用start()方法,否则会出现java.lang.IllegalThreadStateException异常。

  2. 就绪状态
    处于就绪状态的线程已经具备了运行条件,但还没有分配到CPU,处于线程就绪队列(尽管是采用队列形式,事实上,把它称为可运行池而不是可运行队列,因为cpu的调度不一定是按照先进先出的顺序来调度的,每个支持多线程的系统都有一个排程器,排程器会从线程池中选择一个线程并启动它),等待系统为其分配CPU。等待状态并不是执行状态,当系统选定一个等待执行的Thread对象后,它就会从等待执行状态进入执行状态,系统挑选的动作称之为cpu调度。一旦获得CPU,线程就进入运行状态并自动调用自己的run方法。
    提示:如果希望子线程调用start()方法后立即执行,可以使用Thread.sleep()方式使主线程睡眠一伙儿,转去执行子线程。

  3. 运行状态
    处于运行状态的线程最为复杂,它可以变为阻塞状态就绪状态死亡状态
    处于就绪状态的线程,如果获得了cpu的调度,就会从就绪状态变为运行状态,执行run()方法中的任务。如果该线程失去了cpu资源,就会又从运行状态变为就绪状态。重新等待系统分配资源。也可以对在运行状态的线程调用yield()方法,它就会让出cpu资源,再次变为就绪状态
    当发生如下情况是,线程会从运行状态变为阻塞状态:
    ①线程调用sleep方法主动放弃所占用的系统资源
    ②线程调用一个阻塞式IO方法,在该方法返回之前,该线程被阻塞
    ③线程试图获得一个同步监视器,但更改同步监视器正被其他线程所持有
    ④线程在等待某个通知(notify
    ⑤程序调用了线程的suspend方法将线程挂起。不过该方法容易导致死锁,所以程序应该尽量避免使用该方法。

  4. 阻塞状态
    处于运行状态的线程在某些情况下,如执行了sleep(睡眠)方法,或等待I/O设备等资源,将让出CPU并暂时停止自己的运行,进入阻塞状态。
    在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的I/O设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后 从原来停止的位置开始继续运行
    阻塞的情况分三种:
    等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。
    同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
    其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态

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

2 关键字讲解

2.1 线程合并join

线程的合并的含义就是将几个并行线程的线程合并为一个单线程执行,应用场景是当一个线程必须等待另一个线程执行完毕才能执行时可以使用join方法。
join为非静态方法,定义如下:

  • void join():等待该线程终止
  • void join(long millis):等待该线程终止的时间最长为 millis 毫秒。
  • void join(long millis, int nanos):等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。
public class Test { 
        public static void main(String[] args) { 
                Thread t1 = new MyThread1(); 
                t1.start(); 

                for (int i = 0; i < 20; i++) { 
                        System.out.println("主线程第" + i + "次执行!"); 
                        if (i > 2) try { 
                                //t1线程合并到主线程中,主线程停止执行过程,转而执行t1线程,直到t1执行完毕后继续。 
                                t1.join(); 
                        } catch (InterruptedException e) { 
                                e.printStackTrace(); 
                        } 
                } 
        } 
} 

class MyThread1 extends Thread { 
        public void run() { 
                for (int i = 0; i < 10; i++) { 
                        System.out.println("线程1第" + i + "次执行!"); 
                } 
        } 
}
  

2.2 线程让步yield

线程的让步含义就是使当前运行着线程让出CPU资源,但是然给谁不知道
Thread.yield() 方法用于提示当前线程愿意让出 CPU 执行时间,给其他线程执行的机会。调用 Thread.yield() 后,当前线程会进入就绪状态,等待 CPU 调度器重新分配时间片给其他可运行的线程

线程的让步使用Thread.yield()方法,yield() 为静态方法,功能是暂停当前正在执行的线程对象,并执行其他线程。 但是yield唤醒的是相同优先级或者更高优先级的线程

public class Test { 
        public static void main(String[] args) { 
                Thread t1 = new MyThread1(); 
                Thread t2 = new Thread(new MyRunnable()); 

                t2.start(); 
                t1.start(); 
        } 
} 

class MyThread1 extends Thread { 
        public void run() { 
                for (int i = 0; i < 10; i++) { 
                        System.out.println("线程1第" + i + "次执行!"); 
                } 
        } 
} 

class MyRunnable implements Runnable { 
        public void run() { 
                for (int i = 0; i < 10; i++) { 
                        System.out.println("线程2第" + i + "次执行!"); 
                        Thread.yield(); 
                } 
        } 
}
 
线程2第0次执行! 
线程2第1次执行! 
线程2第2次执行! 
线程2第3次执行! 
线程1第0次执行! 
线程1第1次执行! 
线程1第2次执行! 
线程1第3次执行! 
线程1第4次执行! 
线程1第5次执行! 
线程1第6次执行! 
线程1第7次执行! 
线程1第8次执行! 
线程1第9次执行! 
线程2第4次执行! 
线程2第5次执行! 
线程2第6次执行! 
线程2第7次执行! 
线程2第8次执行! 
线程2第9次执行! 

2.3 线程休眠sleep

线程休眠的目的是使线程让出CPU的最简单的做法之一,线程休眠时候,会将CPU资源交给其他线程,以便能轮换执行,当休眠一定时间后,线程会苏醒,进入准备状态等待执行。

线程休眠的方法是Thread.sleep(long millis) Thread.sleep(long millis, int nanos) ,均为静态方法,那调用sleep休眠的哪个线程呢,简单说,哪个线程调用sleep,就休眠哪个线程
sleep方法本身会给其他线程一个运行的机会 但是不考虑优先级,因此会给优先级较低的线程一个机会

public class Test { 
        public static void main(String[] args) { 
                Thread t1 = new MyThread1(); 
                Thread t2 = new Thread(new MyRunnable()); 
                t1.start(); 
                t2.start(); 
        } 
} 

class MyThread1 extends Thread { 
        public void run() { 
                for (int i = 0; i < 3; i++) { 
                        System.out.println("线程1第" + i + "次执行!"); 
                        try { 
                                Thread.sleep(50); 
                        } catch (InterruptedException e) { 
                                e.printStackTrace(); 
                        } 
                } 
        } 
} 

class MyRunnable implements Runnable { 
        public void run() { 
                for (int i = 0; i < 3; i++) { 
                        System.out.println("线程2第" + i + "次执行!"); 
                        try { 
                                Thread.sleep(50); 
                        } catch (InterruptedException e) { 
                                e.printStackTrace(); 
                        } 
                } 
        } 
}
 
线程2第0次执行! 
线程1第0次执行! 
线程1第1次执行! 
线程2第1次执行! 
线程1第2次执行! 
线程2第2次执行! 

从上面的结果输出可以看出,无法精准保证线程执行次序

2.4 线程交互wait,notify,notifyAll

这些操作需要配合synchronized使用

2.4.1 线程交换基础

线程交互主要方法:

  • void notify():唤醒在此对象监视器上等待的单个线程。
  • void notifyAll(): 唤醒在此对象监视器上等待的所有线程。
  • void wait():导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法。
  • void wait(long timeout):导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量。
  • void wait(long timeout, int nanos):导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量(timeout+nanos)

以上这些方法是帮助线程传递线程关心的时间状态。

关于等待/通知,要记住的关键点是:
必须从同步环境内调用wait()、notify()、notifyAll()方法。线程不能调用对象上等待或通知的方法,除非它拥有那个对象的锁。
wait()、notify()、notifyAll()都是Object的实例方法。与每个对象具有锁一样,每个对象可以有一个线程列表,他们等待来自该信号(通知)。线程通过执行对象上的wait()方法获得这个等待列表。从那时候起,它不再执行任何其他指令,直到调用对象的notify()方法为止,如果多个线程在同一个对象上等待,则将只选择一个线程(不保证以何种顺序)继续执行。如果没有线程等待,则不采取任何特殊操作

public class ThreadA { 
    public static void main(String[] args) { 
        ThreadB b = new ThreadB(); 
        //启动计算线程 
        b.start(); 
        //线程A拥有b对象上的锁。线程为了调用wait()或notify()方法,该线程必须是那个对象锁的拥有者 
        synchronized (b) { 
            try { 
                System.out.println("等待对象b完成计算。。。"); 
                //当前线程A等待 
                b.wait(); 
            } catch (InterruptedException e) { 
                e.printStackTrace(); 
            } 
            System.out.println("b对象计算的总和是:" + b.total); 
        } 
    } 
} 
public class ThreadB extends Thread { 
    int total; 

    public void run() { 
        synchronized (this) { 
            for (int i = 0; i < 101; i++) { 
                total += i; 
            } 
            //(完成计算了)唤醒在此对象监视器上等待的单个线程,在本例中线程A被唤醒 
            notify(); 
        } 
    } 
}
运行结果:
等待对象b完成计算。。。 
b对象计算的总和是:5050 

千万注意:
当在对象上调用wait()方法时,执行该代码的线程立即放弃它在对象上的锁。然而调用notify()时,并不意味着这时线程会放弃其锁。如果线程仍然在完成同步代码,则线程在移出之前不会放弃锁。因此,只要调用notify()并不意味着这时该锁变得可用

2.4.2 多个线程在等待一个对象锁时候使用notifyAll()

在多数情况下,最好通知等待某个对象的所有线程。如果这样做,可以在对象上使用notifyAll()让所有在此对象上等待的线程冲出等待区,返回到可运行状态。

下面给个例子:

public class Calculator extends Thread { 
        int total; 

        public void run() { 
                synchronized (this) { 
                        for (int i = 0; i < 101; i++) { 
                                total += i; 
                        } 
                } 
                //通知所有在此对象上等待的线程 
                notifyAll(); 
        } 
}
public class ReaderResult extends Thread { 
        Calculator c; 

        public ReaderResult(Calculator c) { 
                this.c = c; 
        } 

        public void run() { 
                synchronized (c) { 
                        try { 
                                System.out.println(Thread.currentThread() + "等待计算结果。。。"); 
                                c.wait(); 
                        } catch (InterruptedException e) { 
                                e.printStackTrace(); 
                        } 
                        System.out.println(Thread.currentThread() + "计算结果为:" + c.total); 
                } 
        } 

        public static void main(String[] args) { 
                Calculator calculator = new Calculator(); 

                //启动三个线程,分别获取计算结果 
                new ReaderResult(calculator).start(); 
                new ReaderResult(calculator).start(); 
                new ReaderResult(calculator).start(); 
                //启动计算线程 
                calculator.start(); 
        } 
}
 
运行结果:
Thread[Thread-1,5,main]等待计算结果。。。 
Thread[Thread-2,5,main]等待计算结果。。。 
Thread[Thread-3,5,main]等待计算结果。。。 
Exception in thread "Thread-0" java.lang.IllegalMonitorStateException: current thread not owner 
  at java.lang.Object.notifyAll(Native Method) 
  at threadtest.Calculator.run(Calculator.java:18) 
Thread[Thread-1,5,main]计算结果为:5050 
Thread[Thread-2,5,main]计算结果为:5050 
Thread[Thread-3,5,main]计算结果为:5050 

运行结果表明,程序中有异常,并且多次运行结果可能有多种输出结果。这就是说明,这个多线程的交互程序还存在问题。究竟是出了什么问题,需要深入的分析和思考,下面将做具体分析。

实际上,上面这个代码中,我们期望的是读取结果的线程在计算线程调用notifyAll()之前等待即可。 但是,如果计算线程先执行,并在读取结果线程等待之前调用了notify()方法,那么又会发生什么呢?这种情况是可能发生的。因为无法保证线程的不同部分将按照什么顺序来执行。
当读取线程运行时,它只能马上进入等待状态----它没有做任何事情来检查等待的事件是否已经发生。 因此,如果计算线程已经调用了notifyAll()方法,那么它就不会再次调用notifyAll(),----并且等待的读取线程将永远保持等待。这当然是开发者所不愿意看到的问题。

因此针对上面的报错需要把notifyall方法包括在synchronized方法里面,如下:

class Calculator extends Thread {
        int total;
        public void run() {
            synchronized (this) {
                for (int i = 0; i < 101; i++) {
                    total += i;
                }
                //通知所有在此对象上等待的线程
                notifyAll();
            }
        }
    }

2.5 线程锁释放

  1. Java多线程运行环境中,在哪些情况下会使对象锁释放?
    由于等待一个锁的线程只有在获得这把锁之后,才能恢复运行,所以让持有锁的线程在不再需要锁的时候及时释放锁是很重要的。在以下情况下,持有锁的线程会释放锁:
    (1)执行完同步代码块,就会释放锁。(synchronized
    (2)在执行同步代码块的过程中,遇到异常而导致线程终止,锁也会被释放。(exception
    (3)在执行同步代码块的过程中,执行了锁所属对象的wait()方法,这个线程会释放锁,进入对象的等待池。(wait)
  2. 哪些情况不会释放锁?
    除了以上情况外,只要持有锁的线程还没有执行完同步代码块,就不会释放锁。在下面情况下,线程是不会释放锁的:
    (1)执行同步代码块的过程中,执行了Thread.sleep()方法,当前线程放弃CPU,开始睡眠,在睡眠中不会释放锁。sleep方法不考虑线程优先级
    (2)在执行同步代码块的过程中,执行了Thread.yield()方法,当前线程放弃CPU,但不会释放锁。yield方法只给相同优先级或者更高优先级线程机会
    (3)在执行同步代码块的过程中,其他线程执行了当前线程对象的suspend()方法,当前线程被暂停,但不会释放锁

2.6 sleep,yield,wait区别

sleep()Thread类的方法,使当前运行中的线程睡眼一段时间,进入不可运行状态,这段时间的长短是由程序设定的,即使当前线程进入停滞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行,sleep方法本身会给其他线程一个运行的机会 但是不考虑优先级,因此会给优先级较低的线程一个机会

yield():使当前线程让出CPU占有权,但让出的时间是不可设定的,只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。实际上,yield()方法对应了如下操作:先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把CPU的占有权交给此线程,否则,继续运行原来的线程。所以yield()方法称为退让,它把运行机会让给了同等优先级的其他线程

waitObject类的方法,用来线程间的通信,对此对象调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,使线程所在对象中的其它synchronized数据可被别的线程使用,只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态
wait()和notify()因为会对对象的锁标志进行操作,所以它们必须在synchronized函数或synchronized block中进行调用。如果在non-synchronized函数或non-synchronized block中进行调用,虽然能编译通过,但在运行时会发生 IllegalMonitorStateException的异常。

3 其他线程方面

3.1 守护线程

3.1.1 守护线程定义

Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程)
用个比较通俗的假设,任何一个守护线程都是整个JVM中所有非守护线程的保姆。只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。Daemon的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是 GC (垃圾回收器),它就是一个很称职的守护者

守护线程普通线程写法上基本么啥区别,调用线程对象的方法setDaemon(true),则可以将其设置为守护线程,但是必须在thread.start()之前设置

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

3.1.2 方法说明

setDaemon方法的详细说明:

public final void setDaemon(boolean on)

将该线程标记为守护线程用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。 且该方法必须在启动线程前调用。

该方法首先调用该线程的checkAccess方法,且不带任何参数。这可能抛出 SecurityException(在当前线程中)。
参数:

  • on:如果为 true,则将该线程标记为守护线程。
  • 抛出:
    IllegalThreadStateException - 如果该线程处于活动状态。
    SecurityException - 如果当前线程无法修改该线程。

3.1.3 实际操作

public class Test { 
        public static void main(String[] args) { 
                Thread t1 = new MyCommon(); 
                Thread t2 = new Thread(new MyDaemon()); 
                t2.setDaemon(true);        //设置为守护线程 
                t2.start(); 
                t1.start(); 
        } 
} 

class MyCommon extends Thread { 
        public void run() { 
                for (int i = 0; i < 5; i++) { 
                        System.out.println("线程1第" + i + "次执行!"); 
                        try { 
                                Thread.sleep(7); 
                        } catch (InterruptedException e) { 
                                e.printStackTrace(); 
                        } 
                } 
        } 
} 

class MyDaemon implements Runnable { 
        public void run() { 
                for (long i = 0; i < 9999999L; i++) { 
                        System.out.println("后台线程第" + i + "次执行!"); 
                        try { 
                                Thread.sleep(7); 
                        } catch (InterruptedException e) { 
                                e.printStackTrace(); 
                        } 
                } 
        } 
}
 
后台线程第0次执行! 
线程1第0次执行! 
线程1第1次执行! 
后台线程第1次执行! 
后台线程第2次执行! 
线程1第2次执行! 
线程1第3次执行! 
后台线程第3次执行! 
线程1第4次执行! 
后台线程第4次执行! 
后台线程第5次执行! 
后台线程第6次执行! 
后台线程第7次执行! 

从上面的执行结果可以看出:前台线程是保证执行完毕的,后台线程还没有执行完毕就退出了。
实际上:JRE判断程序是否执行结束的标准是所有前台执线程行完毕,而不管后台线程的状态,因此,在使用后台线程时候一定要注意这个问题。

3.2 线程优先级

与线程休眠类似,线程的优先级仍然无法保障线程的执行次序。只不过,优先级高的线程获取CPU资源的概率较大,优先级低的并非没机会执行。

线程的优先级用1-10之间的整数表示,数值越大优先级越高,默认的优先级为5

在一个线程中开启另外一个新线程,则新开线程称为该线程的子线程,子线程初始优先级与父线程相同。

public class Test { 
        public static void main(String[] args) { 
                Thread t1 = new MyThread1(); 
                Thread t2 = new Thread(new MyRunnable()); 
                t1.setPriority(10); 
                t2.setPriority(1); 

                t2.start(); 
                t1.start(); 
        } 
} 
class MyThread1 extends Thread { 
        public void run() { 
                for (int i = 0; i < 10; i++) { 
                        System.out.println("线程1第" + i + "次执行!"); 
                        try { 
                                Thread.sleep(100); 
                        } catch (InterruptedException e) { 
                                e.printStackTrace(); 
                        } 
                } 
        } 
} 
class MyRunnable implements Runnable { 
        public void run() { 
                for (int i = 0; i < 10; i++) { 
                        System.out.println("线程2第" + i + "次执行!"); 
                        try { 
                                Thread.sleep(100); 
                        } catch (InterruptedException e) { 
                                e.printStackTrace(); 
                        } 
                } 
        } 
}
 
线程1第0次执行! 
线程2第0次执行! 
线程2第1次执行! 
线程1第1次执行! 
线程2第2次执行! 
线程1第2次执行! 
线程1第3次执行! 
线程2第3次执行! 
线程2第4次执行! 
线程1第4次执行! 
线程1第5次执行! 
线程2第5次执行! 
线程1第6次执行! 
线程2第6次执行! 
线程1第7次执行! 
线程2第7次执行! 
线程1第8次执行! 
线程2第8次执行! 
线程1第9次执行! 
线程2第9次执行! 

3.3 线程栈模型

要理解线程调度的原理,以及线程执行过程,必须理解线程栈模型。
线程栈 是指某时刻 时 内存中线程调度的栈信息,当前调用的方法总是位于 栈顶。线程栈的内容是随着程序的运行动态变化的,因此研究线程栈必须选择一个运行的时刻(实际上指代码运行到什么地方)。

下面通过一个示例性的代码说明线程(调用)栈的变化过程
在这里插入图片描述
这幅图描述在代码执行到两个不同时刻1、2时候,虚拟机线程调用栈示意图。

当程序执行到t.start();时候,程序多出一个分支(增加了一个调用栈B),这样,栈A、栈B并行执行。

从这里就可以看出方法调用和线程启动的区别了

3.4 线程结束

Thread.stop()、Thread.suspend、Thread.resume、Runtime.runFinalizersOnExit这些终止线程运行的方法已经被废弃了,使用它们是极度不安全的!想要安全有效的结束一个线程,可以使用下面的方法。

  1. 正常执行完run方法后结束掉
  2. 控制循环条件和判断条件的标识符来结束掉线程
  3. 使用interrupt结束一个线程

具体操作如下

class MyThread extends Thread {  
    int i=0;  
    @Override  
    public void run() {  
        while (true) {  
            if(i==10)  
                break;  
            i++;  
            System.out.println(i);  
              
        }  
    }  
}  
或者
class MyThread extends Thread {  
    int i=0;  
    boolean next=true;  
    @Override  
    public void run() {  
        while (next) {  
            if(i==10)  
                next=false;  
            i++;  
            System.out.println(i);  
        }  
    }  
}  
或者
class MyThread extends Thread {  
    int i=0;  
    @Override  
    public void run() {  
        while (true) {  
            if(i==10)  
                return;  
            i++;  
            System.out.println(i);  
        }  
    }  
}  

只要保证在一定的情况下,run方法能够执行完毕即可。而不是while(true)的无线循环。

使用第2中方法的标识符来结束一个线程,是一个不错的方法,但是如果,该线程是处于sleep、wait、join的状态的时候,while循环就不会执行,那么标识符就无用武之地了,当然也不能再通过它来结束处于这3种状态的线程了。
可以使用interrupt这个巧妙的方式结束掉这个线程。
我们看看sleep、wait、join方法的声明:

public final void wait() throws InterruptedException  
public static native void sleep(long millis) throws InterruptedException  
public final void join() throws InterruptedException  

可以看到,这三者有一个共同点,都抛出了一个InterruptedException的异常。
那么在什么时候会产生这样一个异常呢?
每个Thread都有一个中断状状态,默认为false。可以通过Thread对象的isInterrupted()方法来判断该线程的中断状态。可以通过Thread对象的interrupt()方法将中断状态设置为true
当一个线程处于sleep、wait、join这三种状态之一的时候,如果此时中断状态为true,那么它就会抛出一个InterruptedException的异常,并将中断状态重新设置为false。
看下面的简单的例子:

public class Test1 {  
    public static void main(String[] args) throws InterruptedException {  
        MyThread thread=new MyThread();  
        thread.start();  
    }  
}  
  
class MyThread extends Thread {  
    int i=1;  
    @Override  
    public void run() {  
        while (true) {  
            System.out.println(i);  
            System.out.println(this.isInterrupted());  
            try {  
                System.out.println("我马上去sleep了");  
                Thread.sleep(2000);  
                this.interrupt();  
            } catch (InterruptedException e) {  
                System.out.println("异常捕获了"+this.isInterrupted());  
                return;  
            }  
            i++;  
        }  
    }  
}  
测试结果:
1  
false  
我马上去sleep了  
2  
true  
我马上去sleep了  

可以看到,首先执行第一次while循环,在第一次循环中,睡眠2秒,然后将中断状态设置为true。当进入到第二次循环的时候,中断状态就是第一次设置的 true,当它再次进入sleep的时候,马上就抛出了InterruptedException异常,然后被我们捕获了。然后中断状态又被重新自动设置为false了(从最后一条输出可以看出来)。

所以,我们可以使用interrupt方法结束一个线程。具体使用如下:

public class Test1 {  
    public static void main(String[] args) throws InterruptedException {  
        MyThread thread=new MyThread();  
        thread.start();  
        Thread.sleep(3000);  
        thread.interrupt();  
    }  
}  
class MyThread extends Thread {  
    int i=0;  
    @Override  
    public void run() {  
        while (true) {  
            System.out.println(i);  
            try {  
                Thread.sleep(1000);  
            } catch (InterruptedException e) {  
                System.out.println("中断异常被捕获了");  
                return;  
            }  
            i++;  
        }  
    }  
}  
多测试几次,会发现一般有两种执行结果:

0  
1  
2  
中断异常被捕获了  
或者
0  
1  
2  
3  
中断异常被捕获了  

这两种结果恰恰说明了 只要一个线程的中断状态一旦为true,只要它进入sleep等状态,或者处于sleep状态,立马回抛出InterruptedException异常。
第一种情况,是当主线程从3秒睡眠状态醒来之后,调用了子线程的interrupt方法,此时子线程正处于sleep状态,立马抛出InterruptedException异常。
第二种情况,是当主线程从3秒睡眠状态醒来之后,调用了子线程的interrupt方法,此时子线程还没有处于sleep状态。然后再第3while循环的时候,在此进入sleep状态,立马抛出InterruptedException异常

posted @ 2021-12-25 17:57  上善若泪  阅读(160)  评论(0编辑  收藏  举报