01.Thread类

345 阅读35分钟

Thread类

1.介绍

一个线程在Java中使用一个Thread实例来描述。Thread类是Java语言一个重要的基础类,位于java.lang包中。Thread类有不少非常重要的属性和方法,用于存储和操作线程的描述信息,该类的属性和方法,如图所示:

18a41130091147fc8fdbfc5c3d9800ea.png

2.创建并运行线程并使用Jconsole观察线程

介绍

查看JVM中的线程信息,可以使用Jconsole或者Jstack命令来查看,这两个JVM工具都是由JDK自身提供的

创建并运行线程


public class Main { 
	public static void main(String[] args) { 
		MyThread thread = new MyThread(); 
		thread.start(); 
	} 
}
class MyThread extends Thread {
	@Override
	public void run() {
	    System.out.println("线程执行的任务");
	}
}

Jconsol观察线程

关注main线程与Thread-0线程,之前说过在操作系统启动一个JVM的时候,其实是启动了一个进程,而在该进程里面启动了一个以上的线程,其中Thread-0就是咱们创建的,main线程是由JVM启动时创建的,我们都知道J2SE程序的入口就是main函数,虽然我们在上面手动创建了一个线程,事实上还有一个main线程,当然还有一些其他的守护线程,比如垃圾回收线程,RMI线程等

589abc406c924947aefdefede5aff56d.png

3.创建线程的多种方式

继承Thread类

介绍

新线程如果需要并发执行自己的代码,需要做以下两件事情:

  1. 需要继承 Thread 类,创建一个新的线程类
  2. 同时重写run()方法,将需要并发执行的业务代码编写在run方法中
代码
public class Main { 
	public static void main(String[] args) { 
		MyThread thread = new MyThread(); 
		thread.start(); 
	} 
}
class MyThread extends Thread {
	@Override
	public void run() {
	    System.out.println("线程执行的任务");
	}
}

实现Runnable接口

介绍

在Thread类的run方法中,如果 target属性不是空,就执行target属性的run方法。而target属性是Thread类的一个实例属性,并且target属性的类型为Runnable

什么时候target属性非空

Thread类target属性什么情况下非空呢?Thread 类有一系列的构造器,其中有多个构造器可以为target属性赋值,这些构造器包括如下两个:

  1. public Thread(Runnable target)
  2. public Thread(Runnable target,String name)

使用这两个构造器传入target执行目标实例,就可以直接通过Thread类的run方法的默认实现,达到线程的并发执行的目的。在这种场景下,就可以不通过继承Thread类实现线程类的创建了

Runnable接口

Runnable有且仅有一个抽象方法——void run(),代表被执行的用户业务逻辑的抽象,在使用的时候,将用户业务逻辑编写在Runnable实现类的run方法的实现版本中。当Runnable实例传入Thread实例的target属性后,Runnable接口的run的实现版本将被异步调用

87aaaebb134e450fac32cf3cba1da6de.png

代码
class RunnableImplements01 implements Runnable {
    @Override
    public void run() {
        System.out.println("线程的任务");
    }
}
public class Main { 
	public static void main(String[] args) { 
		new Thread(new RunnableImplements01()).start();
	} 
}

优缺点

缺点
  1. 所创建的类并不是线程类,而是线程的target执行目标类,需要将其实例作为参数传入线程类的构造器,才能创建真正的线程
  2. 如果访问当前线程的属性(甚至控制当前线程),不能直接访问Thread的实例方法,必须通过Thread.currentThread()获取当前线程实例,才能去访问和控制当前线程
优点
  1. 可以避免由于Java单继承带来的局限性。如果异步逻辑所在类,已经继承了一个基类,就没有办法在继承 Thread类。比如,当一个Dog类继承了Pet类,再需继承Thread类时就不行了。所以在已经存在继承关系情况下,只能使用实现Runnable接口的方式
  2. 逻辑和数据的更好分离:通过实现Runnable接口的方法创建多线程,更加适合同一个资源被多段业务逻辑并行处理的场景。在同一个资源的情况被多个线程逻辑去异步、并行处理的场景中,通过实现Runnable接口的方式设计多个target执行目标类,可以更加方便的、清晰的将执行逻辑和数据存储分离,更好的体现了面向对象的设计思想

其他方式

介绍

Java官方只承认上面两种创建线程的方式,其他方式不过是对上面两种方式的使用

匿名内部类方式
 Thread thread = new Thread() {
            @Override
            public void run() {
                System.out.println("线程的任务");
            }
        };
thread.start();
Lambda表达式方式
 Thread thread = new Thread(()->{
            System.out.println("线程的任务");
        });
thread.start();
线程池

省略在线程池那一章介绍

Future

省略专门在Future的章节介绍

总结

  1. 通过继承Thread类实现多线程,能更好地做到多个线程并发完成各自的任务,访问各自的数据资源
  2. 通过实现Runnable接口实现多线程,能更好地做到多个线程并发完成同一个的任务,访问同一份的数据资源。多个线程的代码逻辑,可以方便地访问和处理同一个共享数据资源,这样就将有效的将线程逻辑和业务数据进行有效的分离,更好的体现了面向对象的设计思想
  3. 通过实现Runnable接口实现多线程时,如果数据资源存在多线程共享的情况,则数据共享资源需要使用原子类型(而不是普通数据类型),或者需要进行线程的同步控制,以保证对共享数据操作时不会出现线程安全问题
  4. 在大多数情况下,偏向于用实现Runnable接口来实现线程执行目标类,这样能使得代码更加的简洁明了。后面介绍线程池的时候会讲到,异步执行任务在大多数情况下是通过线程池去提交的,而很少通过创建一个新的线程去提交,所以,更多的做法是,通过实现Runnable接口创建异步执行任务,而不是继承Thread去创建异步执行任务

4.线程的调度与时间片

由于CPU的计算频率非常高,每秒计算数十亿次,于是,可以将CPU的时间从毫秒的维度进行分段,每一小段叫做一个CPU时间片。不同的操作系统、不同的处理器,线程的CPU时间

片长度都不同。假定操作系统的线程一个时间片的时间长度为20毫秒(比如 Windows XP),在一个2GHz的CPU上,那么一个时间片可以进行计算的次数是:20亿/(1000/20)=4千万次,也就是说,一个时间片内的计算量是非常巨大的

目前操作系统中主流的线程调度方式大都是;基于CPU时间片方式进行线程调度。线程只有得到CPU时间片,才能执行指令,处于执行状态;没有得到时间片的线程,处于就绪状态,等待系统分配下一个CPU时间片。由于时间片非常短,在各个线程之间快速地切换,表现出来特征是很多个线程在“同时执行”或者“并发执行”

线程的调度模型,目前主要分为两种调度模型:分时调度模型、抢占式调度模型

分时调度模型——系统平均分配CPU的时间片,所有线程轮流占用CPU。分时调度模型在时间片调度的分配上,所有线程人人平等,如图所示:

ea4d832e39384a58a6e671737c215c6c.png

抢占式调度模型——系统按照线程优先级分配CPU时间片。优先级高的线程,优先分配CPU时间片;如果所有的就绪线程的优先级相同,那么会随机选择一个;优先级高的线程获取的CPU时间片相对多一些

5.线程的优先级

介绍

由于目前大部分操作系统都是使用抢占式调度模型进行线程调度。Java的线程管理和调度是委托给了操作系统完成的,与之相对应,Java的线程调度也是使用抢占式调度模型。所以,Java的线程都有优先级

优先级是1-10,Java中线程默认优先级是5,子线程继承父线程的优先级

注意

  • 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示(hint),调度器可以忽略它
  • 如果CPU比较忙,那么优先级高的线程会获得更多的时间片,但CPU空闲时,优先级几乎没有作用,所以不要在程序设计中企图使用线程优先级绑定某些特定的业务,或者让业务严重依赖于线程优先级,这可能会让你大失所望

代码

Runnable task1 = () -> {
 int count = 0;
 for (;;) {
 System.out.println("---->1 " + count++);
 }
};
Runnable task2 = () -> {
 int count = 0;
 for (;;) {
 // Thread.yield();
 System.out.println(" ---->2 " + count++);
 }
};
Thread t1 = new Thread(task1, "t1");
Thread t2 = new Thread(task2, "t2");
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();

6.守护线程

介绍

默认情况下,Java进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护进程运行结束了,即使守护进程代码没有执行完,也会强制结束

Java中的线程为分为两类:守护线程与用户线程。守护线程也称为后台线程,专门指在程序进程运行过程中,在后台提供某种通用服务的线程。比如,每启动一个JVM进程,都会在后台运行着一系列的GC(垃圾回收)线程,这些GC线程就是守护线程,提供幕后的垃圾回收的服务

代码

log.debug("开始运行...");
Thread t1 = new Thread(() -> {
 log.debug("开始运行...");
 sleep(2);
 log.debug("运行结束...");
}, "daemon");
// 设置该线程为守护线程
t1.setDaemon(true);
t1.start();
sleep(1);
log.debug("运行结束...");

注意

  • 垃圾回收器线程就是一种守护线程
  • Tomcat中的Acceptor和Poller线程都是守护线程,所以Tomcat接收到shutdown命令后,不会等待它们处理完当前请求
  • 守护线程必须在启动前,将其守护状态设置为true;启动之后,不能再将用户线程设置为守护线程。否则,JVM 会抛出一个InterruptedException异常
  • 具体来说,如果线程为守护线程,必须在线程实例的start()方法调用之前,调用线程实例的setDaemon(true),设置其daemon实例属性值为 true
  • 守护线程存在被 JVM 强行终止的风险,所以,在守护线程中尽量不去访问系统资源,如文件句柄、数据库连接等等。守护线程被强行终止时,可能会引发系统资源操作的不负责任的中断,从而导致资源不可逆的损坏
  • 守护线程创建的线程,也是守护线程。在守护线程中创建的线程,新的线程都是守护线程。在创建之后,如果通过调用setDaemon(false)将新的线程显示的设置为用户线程,新的线程可以调整成为用户线程

相关方法

  1. setDaemon:此方法将线程标记为守护线程或者用户线程,true是守护线程,false是用户线程,默认用户线程
  2. isDaemon:获取线程的守护状态,用于判断该线程是否是守护线程

7.线程状态(线程的生命周期)

介绍

线程的状态有两种说法一种是五种状态另一种是六种状态,五种状态是从操作系统层面来说的,六种状态是从Java的角度来说的(根据 Thread.State 枚举,分为六种状态)

五种状态

图示

20210423194400685.png

3d87f4e0578d4b7b93a5b908a3573c6b.png

初始状态

仅是在语言层面创建了线程对象,还未与操作系统线程关联

可运行状态(就绪状态)

指该线程已经被创建(与操作系统线程关联),可以由CPU调度执行,当前线程进入就绪状态的条件,大致包括以下几种:

  1. 调用线程的start()方法,此线程进入就绪状态
  2. 当前线程的执行时间片用完
  3. 线程sleep操作结束
  4. 对其他线程合入(join)操作结束
  5. 等待用户输入结束
  6. 线程争抢到对象锁(Object Monitor)
  7. 当前线程调用了yield方法出让CPU执行权限
运行状态

指获取了CPU时间片运行中的状态,当CPU时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换

阻塞状态
  1. 如果调用了阻塞API,如读写文件,这时该线程实际不会用到CPU,会导致线程上下文切换,进入【阻塞状态】
  2. 等文件操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
  3. 与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们

以下情况会让线程进入阻塞状态:

  1. 线程等待获取锁:等待获取一个锁,而该锁被其他线程持有,则该线程进入阻塞状态。当其他线程释放了该锁,并且线程调度器允许该线程持有该锁时,该线程退出阻塞状态
  2. IO阻塞:线程发起了一个阻塞式IO操作后,如果不具备IO操作的条件,线程会进入阻塞状态。IO包括磁盘 IO、网络IO等。IO阻塞的一个简单例子就是线程等待用户输入内容后继续执行
终止状态

表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态

六种状态

图示

20210420194233150.png

20210425111152768.png

20210531162151392.png

NEW

Java源码对NEW状态的注释说明是:创建成功但是没有调用start()方法启动的Thread线程实例,都处于NEW状态

当然,并不是Thread线程实例的start()方法一经调用,其状态从NEW状态到RUNNABLE状态,此时并不意味着线程就立即获取CPU时间片并且立即执行,中间需要一系列的操作系统内部操作

RUNNABLE

当调用了Thread实例start()方法后,下一步如果线程获CPU时间片开始执行,JVM将异步调用线程的run()方法执行其业务代码。那么在run()方法被异步调用之前,JVM 做了哪些事情呢?

JVM的幕后工作和操作系统的线程调度有关。Java中的线程管理,是通过JNI本地调用的方式,委托操作系统的线程管理API完成的。当Java线程的Thread实例的start()方法被调用了后,操作系统中的对应线程,进入的并不是运行状态,而是绪状态,而Java线程并没有这个就绪状态。操作系统中线程的就绪状态是一个什么状态呢?

JVM的线程状态,以及其幕后的操作系统线程状态之间的转换关系,简化后大致如图:

a73b1c65e1d949ddb14cc8f0ffaa5dfb.png

一个操作系统线程如果处于就绪状态,表示万事俱备只欠东风,表示该线程已经满足了执行条件的,但是还不能执行。处于就绪状态的线程,需要等待系统的调度,获取CPU时间片,然后上CPU执行;一旦就绪状态被系统选中,获得CPU时间片,线程就开始占用CPU,开始执行线程的代码,这时候线程的操作系统状态发生了改变——进入了运行状态

操作系统中,处于运行状态的线程在CPU时间片用完之后,又回到就绪状态,等待CPU的下一次调度。就这样,操作系统线程在就绪状态和执行状态之间,被系统反复的调度,这种情况会一直持续,直到线程的代码逻辑执行完成、或者异常终止。这时线程的操作系统状态又发生了改变,进入了线程的最后状态——TERMINATED 终止状态

就绪状态和运行状态,都是操作系统中的线程状态。在Java语言中,并没有细分这两种状态,而是将这两种状态合并成同一种状态——RUNNABLE(可执行)状态。因此,在Thread.State枚举类中,没有定义线程的就绪状态和运行状态,只是定义了RUNNABLE可执行态。这就是Java线程状态和操作系统中的线程状态有所不同的地方

总之,NEW状态的Thread实例,调用了start()方法后,线程的状态将变成RUNNABLE状态。尽管如此,线程的run方法不一定会马上被并发执行,需要在线程获取了CPU时间片之后,才会真正启动并发执行

TERMINATED

处于RUNNABLE状态的线程,在run()方法执行完成之后,就变成了终止状态TERMINATED了。当然,如果在 run()方法执行过程中发生了运行时异常而没有被捕获,run()方法将被异常终止线程也会变成TERMINATED状态

TIMED_WAITING

限时等待状态,线程处于一种特殊的等待状态,准确的说,线程处于限时等待状态。能让线程处于限时等待 状态的操作,大致有以下几种:

  1. Thread.sleep(int n):使得当前线程进入限时等待状态,等待时间为n毫秒
  2. Object.wait():带时限的抢占对象的monitor锁
  3. Thread.join():带时限的线程合并
  4. LockSupport.parkNanos():让线程等待,时间以纳秒为单位
  5. LockSupport.parkUntil():让线程等待,时间可以灵活设置

进入BLOCKED状态、WAITING状态、TIMED_WAITING状态的线程都会让出CPU的使用权;另外,等待或者阻塞状态的线程被唤醒后,进入Ready状态,需要重新获取时间片才能接着运行

BLOCKED

都是Java API层面对【阻塞状态】的细分

WAITING

处于 WAITING(无限期等待)状态的线程不会被分配CPU时间片,需要被其他线程显式地唤醒,才会进入就绪状态。线程调用以下3种方法,会让自己进入无限等待状态:

  1. Object.wait()方法,对应的唤醒方式为:Object.notify() / Object.notifyAll()
  2. Thread.join()方法,对应的唤醒方式为:被合入的线程执行完毕
  3. LockSupport.park()方法,对应的唤醒方式为:LockSupport.unpark(Thread)

8.深入理解Thread构造函数

线程的命名

  1. 在构造线程的时候可以为线程起一个有特殊意义的名字,这也是比较好的一种做法,尤其是在一个线程比较多的程序中,为线程赋予一个包含特殊意义的名字有助于问题的排查和线程的跟踪
  2. 不给的话线程的默认命名:Thread-0(从0递增)
  3. 在new Thread的时候给予线程名称即可

线程的父子关系

Thread的所有构造函数,最终都会调用一个静态方法init,我们截取片段代码对其进行分析,不难发现新创建的任何一个线程都会有一个父线程

    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name;
        // 获取当前线程为父线程
        Thread parent = currentThread();

一个线程的创建肯定是由另一个线程完成的,被创建线程的父线程是创建它的线程

ThreadGroup

在构造函数中,可以显示指定线程的Group,也就是ThreadGroup,在Thread的init方法中可以看到,如果在构造Thread的时候没有显示的指定一个ThreadGroup,那么子线程将会被加入父线程所在的线程组

main线程所在的ThreadGroup称为main

在默认设置中,当然除了子线程会和父线程同属于一个Group之外,它还会和父线程拥有同样的优先级,同样的Daemon

Runnable

逻辑执行单元,前面说的很详细了,这里省略

stackSize

一般情况下,创建线程的时候不会手动指定栈内存的地址空间字节数组,统一通过xss参数进行设置即可,stacksize越大则代表着正在线程内方法调用递归的深度就越深,stacksize越小则代表着创建的线程数量越多,当然了这个参数对平台的依赖性比较高,比如不同的操作系统、不同的硬件

在有些平台下,越高的stack设定,可以允许的递归深度越多;反之,越少的stack设定,则递归深度越浅。当然在某些平台下,该参数压根不会起到任何作用,如果将该参数设置为0,也不会起到任何的作用。

默认为0

守护线程

如果一个JVM进程中没有一个非守护线程,那么JVM会退出,也就是说守护线程具备自动结束生命周期的特性,而非守护线程则不具备这个特点,试想一下如果JVM进程的垃圾回收线程是非守护线程,如果 main线程完成了工作,则JVM无法退出,因为垃圾回收线程还在正常的工作。再比如有一个简单的游戏程序,其中有一个线程正在与服务器不断地交互以获取玩家最新的金币、武器信息,若希望在退出游戏客户端的时候,这些数据同步的工作也能够立即结束,等等

守护线程经常用作与执行一些后台任务,因此有时它也被称为后台线程,当你希望关闭某些线程的时候,或者退出JVM进程的时候,一些线程能够自动关闭,此时就可以考虑用守护线程为你完成这样的工作

9.ThreadGroup详解

介绍

在Java程序中,默认情况下,新的线程都会被加入到main线程所在的group中,main线程的group名字同线程名。如同线程存在父子关系一样,ThreadGroup同样也存在父子关系

无论如何,线程都会被加入某个Thread Group中

b559bf0fbb52439086523c01a633660b.png

创建Thread Group

  1. public ThreadGroup(String name);
  2. public ThreadGroup(ThreadGroup parent,String name);

创建ThreadGroup的语法非常简单,可通过上面某个构造函数来创建,第一个构造函数为ThreadGroup赋予了名字,但是该ThreadGroup的父ThreadGroup时创建它的线程所在的ThreadGroup;第二个ThreadGroup的构造函数赋予group名字的同时又显示指定了父Group

复制Thread数组和ThreadGroup数组

介绍

在一个ThreadGroup中会加入若干个线程以及子ThreadGroup,ThreadGroup为我们提供了若干个方法,可以复制出线程和线程组

复制Thread数组
  1. public int enumerate(Thread[] list);
  2. public int enumerate(Thread[] list,boolean recurse);

上述两个方法,会将ThreadGroup中的active线程全部复制到Thread数组中,其中recurse参数如果为true,则该方法会将所有子group中的active线程都递归到Thread数组中

注意:

  1. enumerate方法获取的线程仅仅是个预估值,并不能百分之百地保证当前group的活跃线程,比如在调用复制之后,某个线程结束了生命周期或者新的线程加入了进来,都会导致数据的不准确
  2. enumerate方法的返回值int相较Thread[]的长度更为真实,比如定义了数组长度的Thread数组,那么enumerate方法仅仅会将当前活跃的Thread分别放进数组中,而返回值init代表真实的数量,并非Thread数组的长度
复制ThreadGroup数组
  1. public init enumerate(ThreadGroup[] list);
  2. public init enumerate(ThreadGroup[] list,boolean recurse);

和复制Thread数组类似,上述两个方法,主要用于复制当前ThreadGroup的子Group,同样recurse参数会决定是否递归的方式向下复制

ThreadGroup基本操作

  1. activeCount():用于获取group中活跃的线程,这只是个估计值,并不能百分之百地保证数字一定正确,原因已经分析过了,该方法会递归获取其他子group中的活跃线程
  2. activeGroupCount():用于获取group中活跃的子group,这也是一个近似估值,该方法也会递归获取所有的子group
  3. getMaxPriority():用于获取group的优先级,默认情况下,Group的优先级为10,在该group中,所有线程的优先级都不能大于group的优先级
  4. getName():用于获取group的名字
  5. getParent():用于获取group的父group,如果父group不存在,则会返回null,比如system parent的父group就为null
  6. list():该方法没有返回值,执行该方法会将group中所有的活跃线程信息全部输出到控制
  7. parentOf(ThreadGroup g):会判断当前group是不是给定group的父group,另外如果给定的group就是自己本身,也会返回true
  8. setMaxPriority(int pri):会指定group的最大优先级,最大优先级不能超过父group的最大优先级,执行该方法不仅会改变当前group的最大优先级,还会改变所有子group的最大优先级

ThreadGroup的interrupt

interrupt一个thread group会导致该group中(会递归子group)所有的active线程都被interrupt,也就是说该group中每一个线程的interrupt标识都被设置了

ThreadGroup的destroy

destroy用于销毁ThreadGroup,该方法只是针对一个没有任何active线程的group进行一次destroy标记,调用该方法的直接结果是在父group中将自己移除

销毁ThreadGroup及其子ThreadGroup,在该ThreadGroup中所有的线程必须是空的,也就是说ThreadGroup或者子ThreadGroup所有的线程都已经停止运行,如果active线程存在,调用destroy方法则会抛出异常

守护ThreadGroup

线程可以设置为守护线程,ThreadGroup也可以设置守护ThreadGroup,但是若将一个ThreadGroup设置为daemon,也并不会影响线程的daemon属性,如果一个ThreadGroup的deamon被设置为true,那么在group中没有任何active线程的时候该group将自动destroy

10.Thread常用方法

sleep方法

介绍

sleep是一个静态方法,其有两个重载方法,其中一个需要传入毫秒数,另一个既需要毫秒数也需要纳秒数

sleep方法会让当前线程进入指定毫秒数的休眠,暂停执行,虽然给定了一个休眠的时间,但是最终要以系统的定时器和调度器的精度为准,休眠有一个非常重要的特性,那就是其不会放弃monitor锁的所有权

方法定义
  1. public static void sleep(long millis) throws InterruptedException
  2. public static void sleep(long millis,long nanos) throws InterruptedException
代码
public class Test {
    public static void main(String[] args) {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}
使用TimeUnit替代Thread.sleep

在JDK1.5以后,JDK引入了一个枚举类TimeUnit,其对sleep方法提供了很好的封装,使用它可以省去时间单位的换算步骤,案例代码如下:

public class Test {
    public static void main(String[] args) {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

yield方法

说明

yield方法属于一种启发式的方法,其会提醒调度器我愿意放弃当前的CPU资源,如果CPU的资源不紧张,则会忽略这种提醒

调用yield方法会使当前线程从RUNNING状态切换到RUNNABLE状态

yield是让目前正在执行的线程放弃当前的执行,让出CPU的执行权限,使得CPU去执行其他的线程。处于让步状态的JVM层面的线程状态,仍然是RUNNABLE可执行状态;但是,该线程所对应的操作系统层面的线程,在状态上来说,会从执行状态变成就绪状态。线程在yield时,线程放弃和重占CPU的时间是不确定的,可能是刚刚放弃CPU,马上又获得CPU执行权限,重新开始执行

代码
public class Test {
    public static void main(String[] args) {
        Thread.yield();
    }
}
yield与sleep

在JDK1.5以前的版本中yield的方法事实上是调用了sleep(0),但是它们之间有着本质的区别,具体如下:

  1. sleep会导致当前线程暂停指定的时间,没有CPU时间片的消耗
  2. yield只是对CPU调度器的一个提示,如果CPU调度器没有忽略这个提示,它会导致上下文切换
  3. sleep会使线程短暂的block,会在给定的时间内释放CPU资源
  4. yield会使RUNNING状态的Thread进入RUNNABLE状态(如果CPU调度器没有忽略这个提示的话)
  5. sleep几乎百分之百的完成了给定时间的休眠,而yield的提示并不能一定担保
  6. 一个线程sleep另一个线程调用interrupt会捕获到中断信号,而yield则不会

优先级相关方法

说明

可以设置线程的优先级或者获取线程的优先级

方法定义
  1. public final void setPriority(int newPriority):为线程设置优先级
  2. public final int getPriority():获取线程的优先级
优先级范围
  1. 默认为5
  2. 最小为1
  3. 最大为10
忠告

不要在程序设计中企图使用线程优先级绑定某些特定的业务,或者让业务严重依赖于线程优先级,这可能会让你大失所望

getId方法

说明

获取线程唯一ID,线程的ID在整个JVM进程中都会是唯一的,并且是从О开始逐次递增。如果你在main线程(main 函数)中创建了一个唯一的线程,并且调用getId()后发现其并不等于0,也许你会纳闷,不应该是从0开始的吗?之前已经说过了在一个JVM进程启动的时候,实际上是开辟了很多个线程,自增序列已经有了一定的消耗,因此我们自己创建的线程绝非第0号线程

方法定义
  1. public long getId();
代码
public class Test {
    public static void main(String[] args) {
        Thread t = new Thread(()->{System.out.println("1")});
        long id = t.getId();
    }
}

currentThread方法

说明

用于返回当前执行线程的引用

代码
public class Test {
    public static void main(String[] args) {
        Thread currentThread = Thread.currentThrad();
    }
}

线程上下文类加载器相关方法

说明

可以获取或设置线程上下文类加载器

方法定义
  1. public ClassLoader getContextClassLoader():获取线程上下文的类加载器,简单来说就是这个线程是由那个类加载器加载的,如果没有修改线程上下文类加载器的情况下,则保持与父线程同样的类加载器
  2. public void setContextClassLoader():设置该线程的类加载器,这个方法可以打破Java类加载的父委托机制,有时候该方法也被称为Java类加载器的后门

join方法

说明

Thread的join方法同样是一个非常重要的方法,使用它的特性可以实现很多比较强大的功能,与sleep一样它也是一个可中断的方法,也就是说,如果有其他线程执行了对当前线程的interrupt操作,它也会捕获到中断信号,并且擦除线程的interrupt标识,Thread的API为我们提供了三个不同的join方法,join某个线程A,会使得当前线程B进入等待,直到线程A结束生命周期,或者到达给定的时间,那么在此期间B线程是处于BLOCKED的,而不是A线程

6079c896420f486bba3316047e56e82f.png

方法定义
  1. public final void join() throws InterruptedException
  2. public final synchronized void join(long millis,int nanos) throws InterruptedException
  3. public final synchronized void join(long millis) throws InterruptedException
代码案例
public class Test {
    public static void main(String[] args) {
        Thread t = new Thread(()->{
            for(int i=0;i<10;i++){
                System.out.println(i);
            }
        });
        // 使用join让main线程等待t线程执行完毕
        t.join();
        System.out.println("t线程执行完毕,main线程继续运行");
    }
}

11.线程打断

stop方法的缺点(为什么需要线程打断)

Java语言提供了stop方法终止正在运行的线程,但是,Java将Thread的stop方法设置为过时,不建议大家使用。为什么呢?因为使用stop方法是很危险的,就象突然关闭计算机电源,而不是按正常程序关机。在程序中,我们是不能随便stop一个线程的,我们无法知道这个线程正运行在什么状态,它可能持有某把锁,强行中断线程可能导致锁不能释放的问题;或者线程可能在操作数据库,强行中断线程可能导致数据不一致的问题。正由于使用stop方法来终止线程可能会产生不可预料的结果,因此并不推荐使用

一个线程什么时候可以退出呢?当然只有线程自己才能知道

介绍

Thread的 interrrupt(中断)方法,此方法本质不是用来中断一个线程,而是将线程设置为中断状态

如果此线程处于阻塞状态(如调用了 Object.wait方法),则会立马退出阻塞,并抛出InterruptedException异常,线程就可以通过捕获InterruptedException来做一定的处理,然后让线程退出。更确切的说,如果线程被Object.wait, Thread.join和Thread.sleep三种方法之一阻塞,此时调用该线程的interrupt()方法,那么该线程将抛出一个InterruptedException中断异常(该线程必须事先预备好处理此异常),从而提早地终结被阻塞状态如果此线程正处于运行之中,则线程不受任何影响,继续运行,仅仅是线程的中断标记被设置为true。所以,程序可以在适当的位置,通过调用isInterrupted()方法来查看自己是否被中断,并做出退出操作

线程会有一个打断标记,默认是false表示该线程未接收到打断请求。使用线程对象的interrupt()方法来打断线程

注意

如果线程的interrupt方法先被调用,然后线程开始调用阻塞方法进入阻塞状态,InterruptedException异常依旧会被抛出。如果线程捕获InterruptedException异常后,继续调用阻塞方法,将不再触发InterruptedException异常

代码案例

public class Test {
    public static void main(String[] args) {
        Thread t = new Thread(()->{
            try{
                Thread.sleep(1000000);
            }catch(InterruptedException e){
                e.printStackTrace();
                System.out.println("线程被打断,即将执行退出");
                return;
            }
        });
        t.start();
        t.interrupt();
    }
}

打断标记什么时候会被清空

  1. 首先调用线程对象的interrupt()方法不会清空打断标记
  2. 如果线程正在调用:sleep,wait,join的其中一个方法会让阻塞的线程唤醒但是打断标记会被清除也就是从ture变为了false

如果打断的是正在park的线程

  1. 如果线程正在执行LockSupport.park()进入阻塞然后被打断
  2. 会导致线程被唤醒但是打断标记还是true
  3. 虽然打断标记为true但是这个线程再一次执行的时候就不会陷入阻塞了也就是park()方法失效
  4. 可以使用Thread.interrupted()清除打断状态
  5. 也就是说调用LockSupport.park()会陷入阻塞如果使用interrupt()来打断会导致唤醒这个线程并让打断标记为true,虽然打断标记为true但是当这个线程再一次执行park方法的时候就不会陷入阻塞了而是直接向下执行可以使用Thread.interrupted()清除打断状态来解决这个问题

常见方法与含义

  1. interupt:线程的实例方法,打断线程方法
  2. Thread.interrupted():重置打断标记为false
  3. isInterrupt:线程的实例方法,获取当前打断标记的值,不会重置打断标记

12.关闭线程

介绍

JDK有个过时的方法stop,但是该方法存在一个问题,JDK官方早已经不推荐使用,其在后面的版本中有可能会被移除,根据官网的描述,该方法在关闭线程时可能不会释放掉monitor的锁,所以不要使用这种方式结束线程

其他关闭线程的方式

  1. 线程结束生命周期正常结束
  2. 捕获中断信号关闭线程
  3. 使用volatile开关控制
  4. 异常退出

线程结束生命周期正常结束

线程运行结束,完成了自己的使命之后,就会正常退出,如果线程中的任务耗时比较短,或者时间可控,那么就放任它正常结束就好了

捕获中断信号关闭线程

在线程打断中已经写了;可以通过捕获中断异常或者通过isInterrupted方法来判断是否中断,接收到中断后退出即可

使用volatile开关控制

由于线程的打断标记很有可能被重置,或者线程的任务代码中没有调用任何可以捕获到interruptedException的方法,所以volatile修饰的开关flag关闭线程也是一种常用的做法

public class Test {
    public static void main(String[] args) {
        MyRunnable r = new MyRunnable();
        new Thread(r).start();
        r.close();
    }
}
class MyRunnable implements Runnable {
    // 关闭标记,使用volatile的原因是需要保证多线程之间的可见性,默认false表示不关闭
    private volatile boolean flag = false;
    
    @Override
    public void run(){
        System.out.println("开始运行");
        while (!flag){
            // 工作
            System.out.println("正在工作");
        }
        System.out.println("flag状态改变,结束当前线程了");
    }
    
    public void close(){
        this.flag = true;
    }
}

异常退出

在一个线程的执行单元中,是不允许抛出checked异常的,不论Thread中的run方法,还是Runnable中的run方法,如果线程在运行过程中需要捕获checked异常并且判断是否还有运行下去的必要,那么此时可以将checked封装为unchecked异常(RuntimeException)抛出进而结束线程的生命周期

13.线程中的异常处理

介绍

如何获取线程在运行时期的异常信息

Thread类中处理运行时异常的方法

  1. public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh):为某个特定线程指定UncaughtExceptionHandler
  2. public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh):设置全局的UncaughtExceptionHandler
  3. public UncaughtExceptionHandler getUncaughtExceptionHandler():获取特定线程的UncaughtExceptionHandler
  4. public static UncaughtExceptionHandler getDefaultUncaughtExceptionHandler():获取全局的UncaughtExceptionHandler

UncaughtExceptionHandler介绍

线程在执行单元中是不允许抛出checked异常的,而且线程运行在自己的上下文中,派生它的线程将无法直接获得它运行中出现的异常信息。对此,Java为我们提供了一个UncaughtExceptionHandler接口,当线程在运行过程中出现异常时,会回调UncaughtExceptionHandler接口,从而得知是哪个线程在运行时出错,以及出现了什么样的错误,UncaughtExceptionHandler接口是一个函数时接口,该回调接口会被Thread中的dispatchUncaughtException方法调用,接口定义如下:

b54333ab5a05488db027cabcc35e22eb.png

当线程在运行过程中出现异常时,JVM会调用dispatchUncaughtException方法,该方法会将对应的线程实例以及异常信息传递给回调接口

示例代码

public class Test {
    public static void main(String[] args){
        Thread.setDefaultUncaughtExceptionHandler((t,e)->{
            System.out.println(t.getName);
            e.printStackTrace();
        })
        new Thread(()->{
            System.out.println(1/0);
        }).start();
    }
}

没有设置UncaughtExceptionHandler的情况

  1. 当前线程是否设置了handler,如果有则执行,没有就到所在的ThreadGroup中获取
  2. ThreadGroup实现了UncaughtExceptionHandler接口它的逻辑是:
    • 该ThreadGroup如果有父ThreadGroup,则直接调用父Group的uncaughtException方法
    • 如果设置了全局默认的UncaughtExceptionHandler接口,则调用全局的
    • 若即没有父ThreadGroup,也没有设置全局UncaughtExceptionHandler接口,则会直接将异常的堆栈信息定向到System.err中

在这里插入图片描述

14.Hook线程(钩子线程)

介绍

JVM进程的退出是由于JVM进程中没有活跃的非守护线程,或者收到了系统中断信号,向JVM程序注入一个Hook线程,在JVM进程退出的时候,Hook线程会启动执行,通过Runtime可以为JVM注入多个Hook线程

注入Hook线程代码

public class Test {
    public static void main(String[] args){
        // 该方法可以执行多次注册多个钩子线程
        Runtime.getRuntime().addShutdownHook(()->{
           System.our.println("钩子执行"); 
        });
    }
}

实战

需求场景

在我们开发中经常会遇到Hook线程,比如为了防止某个程序被重复启动,在进程启动时会创建一个lock文件,进程收到中断信号的时候会删除这个lock文件,我们在MySQL服务器,Zookeeper,Kafka等系统中个都可以看到lock文件的存在,这里的需求就是模拟一个防止重复启动的冲虚

代码
public class Test {
    private final static String LOCK_PATH = "/home/wangwenjun/locks/";
    private final static String LOCK_FILE = ".lock";
    private final static String PERMISSIONS = "rw--------";
    
    public static void main(String[] args) throws Exception{
        // 1.注入Hook线程,在程序退出的时候删除lock文件
        Runtime.getRuntime().addShutdownHook(()->{
            System.out.println("钩子执行");
            getLockFile().toFile().delete();
        })
        // 2.检查是否存在.lock文件
        checkRunning();
        // 3.模拟业务执行
        Thread.sleep(999999);
    }
    
    private static void checkRunning() throws Exception{
        Path path = getLockFile();
        if (path.toFile().exists()){
            throws new RuntimeException("程序已经运行");
        }
        Set<PosixFilePermission> perms = PosixFilePermissions.fromString(PERMISS-IONS);
        Files.createFile(path,PosixFilePermissions.asFileAttribute(perms));
    }
    private static Path getLockFile(){
        return Paths.get(LOCK_PATH,LOCK_FILE);
    }
}

启动程序后,执行kill pid或者kill -l pid命令之后,JVM进程会收到中断信号,并且启动Hook线程删除.lock文件

Hook线程应用场景以及注意事项

  1. Hook线程只有在收到退出信号的时候会被执行,如果在kill的时候使用了参数-9,那么Hook线程不会得到执行,进程将会立即退出,因此.lock文件将得不到清理
  2. Hook线程中也可以执行一些资源释放的工作,比如关闭文件句柄,socket链接,数据库connection等
  3. 尽量不要在Hook线程中执行一些耗时非常长的操作,因为其会导致程序迟迟不能退出