3、多线程的操作和守护线程
一、多线程的操作
书接上回,讲了多线程的一些基本概念,本节侧重
- 多线程状态之间是如何切换
- 切换操作的原理
- 上节有关多线程的操作涉及到了sleep()、join()、wait()、notify();
1、sleep()
sleep方法 会主动释放CPU的执行权,休眠一段时间。但不会释放锁(就会一直占用着资源不释放)
线程状态:运行状态→限时等待状态 (等到休眠时间结束:限时等待状态→就绪状态→运行状态)
- sleep方法定义在Thread类中,是一组静态方法,有两个重载版本:
//使目前正在执行的线程休眠 millis 毫秒;
public static void sleep(long millis) throws InterruptException; //使目前正在执行的线程休眠 millis 毫秒,nanos 纳秒
public static void sleep(long millis,int nanos) throws InterruptException;
sleep 方法会有 InterruptException 受检异常抛出,如果调用了 sleep 方法,必须进行异常审查,
捕获 InterruptedException 异常,或者再次通过方法声明存在 InterruptedException 异常。
记住这个InterruptedException
2、手动线程退出的方式
之前几篇文章也讲了一些关于线程结束的方式:
- 线程执行完异步逻辑任务,即run()方法里的代码
- 线程执行异步任务的过程出现运行时异常并没有捕获,JVM会中断线程的执行
- synchronized会自动释放锁,lock不会,所以lock要在finally代码块里unlock才能释放锁
接下来我们了解一下关于手动结束线程的方式:
- stop()方法
- 优雅地手动结束线程,interrupt方法
2.1 stop()方法
stop()方法会强制结束线程,但stop()方法的使用是危险的。Object.stop()不推荐使用
原因分析:假如线程持有某把锁(持有共享资源的使用权),通过stop()结束该线程,会导致线程持有的锁不能释放,会产生死锁的问题。
2.2 Interrupt()中断线程
Thread的interrupt()方法并不能中断线程,该方法的本质不是用来中断线程,而是将线程设置为中断状态
究其原因,我们得讲一下interrupt()的两个作用:
- 如果此线程处于阻塞状态(如调用了Object.wait()方法),则会立马退出阻塞,并抛出InterruptException异常。线程就可以通过捕获 InterruptedException 来做一定的处理,然后让线程退出。
更确切的说,如果线程被Object.wait(),Thread.join和Thread.sleep三种方式之一阻塞,此时调用该线程的interrupt()方法,那么该线程将抛出一个InterruptExcption中断异常(该线程必须实现预备好处理此异常),从而提早地终结被阻塞状态
2.如果该线程正处于运行之中,则线程不受任何影响,继续运行,仅仅是线程的中断标记被设置为true。所以,程序可以在适当的位置,通过调用isInerrupted()方法查看自己是否被中断,并作退出操作
public class InterruptUsing {
static class SleepThread extends Thread{
@Override
public void run() {
//线程休眠10s
try {
System.out.println("进入休眠");
Thread.sleep(10000);
//.....执行异步逻辑任务
} catch (InterruptedException e) {
System.out.println("发生异常被中断");
//这里我们可以手动结束线程
return;
}
}
}
public static void main(String[] args) throws InterruptedException {
SleepThread sleepThread=new SleepThread();
sleepThread.start();
System.out.println("等待2s...");
//主线程执行2s后中断 sleepThread线程
Thread.sleep(2000);
sleepThread.interrupt();
}
}
Thread.interrupt( )方法并不像 Thread.stop( )方法那样中止一个正在运行的线程,其作用是设置线程的中断状态位(为 true),至于线程是死亡、还是等待新的任务或是继续运行至下一步,就取决于这个程序本身。
关于为什么线程能够判断自己被interrupt从而抛出异常,是由于线程内部的isInterrupt()会时不时执行判断线程的中断标识位是否位true
3、线程的join操作
线程合并的定义和原理就不谈了,不懂可以看完上一篇文章。
线程的 join 操作的三个版本
Join()方法是 Thread 类的一个实例方法,有三个重载版本:
//重载版本 1:此方法会把当前线程变为 WAITING,直到被合并线程执行结束。
public final void join() throws InterruptedException:
//重载版本 2:此方法会把当前线程变为 TIMED_WAITING,直到被合并线程结束,或者等待被合并
线程执行 millis 的时间。
public final synchronized void join(long millis) throws InterruptedException:
//重载版本 2:此方法会把当前线程变为 TIMED_WAITING,直到被合并线程结束,或者等待被合并
线程执行 millis+nanos 的时间。
public final synchroinzed void join(long millis, int nanos) throws InterruptedException:
4、线程yield操作
线程的 yield(让步)操作的作用:是让目前正在执行的线程放弃当前的执行,让出 CPU 的执行权限,使得 CPU 去执行其他的线程。
处于让步状态的 JVM 层面的线程状态,仍然是RUNNABLE 可执行状态;但是,该线程所对应的操作系统层面的线程,在状态上来说,会从执行状态变成就绪状态。
yield( )方法是 Thread 类提供的一个静态方法,它也可以让当前正在执行的线程暂停,但它不会阻塞该线程,它只是让线程转入就绪状态。
5、关于线程阻塞Blocked
处于阻塞状态的线程不会占用CPU资源(释放了锁),什么情况下会进入阻塞状态
-
线程等待获取锁
等待获取一个锁,而该锁被其他线程持有,则线程阻塞状态。当其他线程释放了该锁,并线程调度器允许该线程持有该锁时,该线程退出阻塞状态
-
IO阻塞
线程发起一个阻塞式IO操作后,如果不具备IO操作的条件,线程会进入阻塞状态。IO包括磁盘IO和网络IO
6、wait方法和notify方法原理
上一篇文章主要讲了线程通信的使用,这篇文章不讲
6.1对象的 wait 方法的主要作用是让当前线程阻塞并等待被唤醒。
wait 方法与对象监视器(Monitor)紧密相关,使用 wait 方法时也一定需要放在同步块中。
Object 类中的 wait 方法,有三个版本:
(1)void wait( )
//这是一个基础版本,当前线程调用了同步对象 locko 的 wait 实例方法后,将导致当前的线程等待,当前线程进入 locko 的监视器 WaitSet,等待被其他线程唤醒。
(2)void wait(long timeout )
//这是一个限时等待版本,导致当前的线程等待,等待被其他线程唤醒,或者指定的时间 timeout 用完,线程不在等待。
(3)void wait(long timeout, int nanos)
//这是一个高精度限时等待版本,其主要作用是更精确控制等待时间。参数 nanos 是一个附加的纳秒级别等待时间,从而实现更加高精度的等待时间控制。
//1 秒 =1000 毫秒 = 1000 000 微秒 = 1000 000 000 纳秒。
6.2 wait方法的原理
- 当线程调用了locko(某个同步锁对象)的wait方法后,JVM会将当前线程加入lock监视器的WaitSet(等待集),等待被其他线程唤醒
- 当前线程会释放locko对象监视器的Ownder权力,让其他线程可以抢夺locko对象的监视器
- 让当前线程等待,进入WAITING状态
6.3对象的notify方法
对象的 notify 方法的主要作用是唤醒在等待的线程。notify 方法与对象监视器紧密相关,使用 notify 方法时也需要放在同步块中。
notify 方法有两个版本:
版本一:void notify( )
//notify 方法的主要作用如下:locko.notify 调用后,唤醒 locko 监视器等待集中的第一条等待线程;被唤醒的线程进入 EntryList,其状态从 WAITING 等待状态变成 BLOCKED 阻塞状态;
版本二:void notifyAll( )
//locko.notifyAll 被调用后,唤醒 locko 监视器等待集中的全部等待线程;所有被唤醒的线程进入 EntryList,线程状态从 WAITING 等待状态变成 BLOCKED 阻塞状态。
6.4 notify方法的原理
对象的 notify(或者 notifyAll)方法的核心原理,大致如下:
- 当线程调用了locko(某个同步锁对象)的notify或notifyAll方法后,JVM会唤醒locko监视器WaitSet中的一条或所以的的等待线程
- 等待线程被唤醒后,会从监视器的WaitSet移动到EntryList,线程具备了排队抢夺监视器的资格,其状态从WAITING转变为Blocked
- EntryList的线程抢夺到监视器Owner权力后,线程的状态变为Runnable,具备重新执行的资格
6.5线程通信“通知-等待”要点
- 使用某个同步对象 locko 的 wait 和 notify 类型方法前,必须要取得这个锁对象的监视锁,所以,wait 和 notify 类型方法必须放在 synchronized(locko)同步块中,如果没有获得监视锁,则 JVM 会报 IllegalMonitorStateException 运行时异常。
- 使用 wait 方法时使用 while 进行条件判断:如果是在某种条件下进行等待,对条件的判断不能使用 if 语句做一次性判断,而是使用 while 循环做反复判断。只有这样,才能在线程被唤醒后继续都检查 wait 的条件,并在条件没有满足的情况下,继续等待。
二、守护线程
线程分为守护线程和用户现场(default)。
1、守护线程和用户线程的区别
两者的本质区别:JVM虚拟机进程终止的方向不同。用户线程和JVM线程是主动关系,即如果全部的用户线程终止,JVM虚拟机进程也随即终止;而守护线程和JVM进程是被动关系,即如果JVM进程终止,所以守护线程也随之终止
只要有一个用户线程没有终止,JVM进程也不会终止。
2、守护线程的要点
- 守护线程必须在启动前,将其状态设置为true;线程启动后,不能再将用户线程设置为守护线程。否则,JVM 会抛出一个 InterruptedException 异常。
- 守护线程有被JVM强制终止的风险,所以,在守护线程中尽量不去访问系统资源,如数据库等,守护线程被强制终止,可能会引发系统资源操作的不负责中断,从而导致资源不可逆的损坏
- 守护线程创建的线程也为守护线程
怎么理解呢,就是在守护线程内创建的新的线程都是守护线程。但在创建后,如果调用setDaemon(false)将新的线程显示的设置为用户线程,新的线程可以调整成为用户线程。