Thread类

概述

​ 一个线程就是一个“执行流”。每个线程都可以按照顺序执行自己的代码,而多个线程可以“同时”执行多份代码。线程本身是操作系统中的概念。操作系统内核实现了线程这样的机制,并且对用户层提供了一些API供用户使用(例如Linuxpthread库)。而Java标准库中的Thread类可以视为是对操作系统提供的API进行了进一步的抽象和封装

​ Thread类是JVM用于管理线程的一个类。在Java中每个线程执行流都是通过Thread类的对象来描述。JVM会将这些Thread对象组织起来,用于线程调度、线程管理

​ 使用Thread类,我们可以创建和管理多个线程,并且控制它们的执行顺序、优先级、暂停、恢复等

Thread的几个常见属性

属性 获取方法
ID getId()
名称 getName()
状态 getState()
优先级 getPriority()
是否位后台线程 isDaemon()
是否存活 isAlive()
是否打断 isInterrupted()
  • ID是线程的唯一标识,不会重复
  • 名称就是线程的命名
  • 状态标识线程当前所处的一个情况
  • 是否存货,可以简单地理解为:run方法是否运行结束了
  • 优先级高的线程理论上来所更容易被调度到,但并不绝对。因为优先级只是”建议“性质的,不是强制性的。优先级意味着”建议“操作系统优先调度某线程,但实际上到底要不要优先调度,还是取决于操作系统
  • 线程分后台线程和前台线程。关于后台线程:JVM会在一个进程的所有非后台线程结束后,才会结束运行。创建的线程默认是前台线程,main线程也是一个前台线程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    @Test
public void daemonTest(){
Thread t1 = new Thread(() -> {
while (true) {
System.out.println("我是守护线程。。。");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t1.setDaemon(true);
t1.start();
System.out.println("我是主线程");
}
//执行结果
/*
我是主线程
我是守护线程。。。
*/

常用方法

启动线程

start()方法

start()方法是启动线程的方法,调用该方法会使线程进入就绪状态,等待CPU分配时间片后开始执行。run()方法是线程的执行体,它包含了该线程要执行的代码

当调用start()方法启动线程后,线程会在独立的执行路径下自动执行run()方法中的代码

主要注意的是:run()方法并不标识新的线程的创建;调用start方法,才真的在操作系统的底层创建出一个线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void test(){
Thread t1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName());
},"t1");
t1.start();
new Thread(() -> {
System.out.println(Thread.currentThread().getName());
},"t2").run();
System.out.println(Thread.currentThread().getName());
}
//执行结果
/*
main
main
t1
*/

线程中断

方法1-成员变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static boolean quit=false;

@Test
public void interruptTest(){
new Thread(()->{
while (!quit){
try {
System.out.println(Thread.currentThread().getName());
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"t1").start();

try {
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName());
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
quit=true;
}

为什么quit要声明为成员变量?

  • 在Java中,Lambda表达式内部访问的局部变量必须是final或者事实上的final(不对其进行修改),这是因为Lambda表达式实际上是一个闭包,它包含了对外部的变量的应用。这些变量在Lambda表达式执行期间不能被修改,避免线程安全问题确保数据的一致性

  • 闭包是一个函数及其相关的引用环境,它可以自由地访问其所在的词法作用域中的变量。简单来说,闭包是一种函数,它可以捕获外部作用域中的变量,并在其生命周期内继续使用这些变量,即使变量已经离开了作用域。

方法2-Interrupt

isInterrupted()就可以理解为,是t对象自带的一个结束标志位。通过t.interrupt()方法将t内部的标志位给设定成 true

打断线程,但线程停不停下来由自己决定

interrupt() 方法不仅会改变线程内部的标志位,还会将sleep()唤醒。这是上述异常来源的原因。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public static void main(String[] agrs){

Thread t1=Thread(()->{

while(true){

boolean interrupted=Thread.currentThread().isInterrupted();

if(interrupted){

break;

}

}

},"t1");

t1.start();

Thread.sleep(1000);

t1.interrupt();

}

interrupt()方法的作用

  1. 设置标志位为true
  2. 如果该线程正在阻塞状态中(如正在执行sleep,join,wait等),此时就会把该线程从阻塞状态唤醒,并通过抛出异常的方式让sleep立即结束。

换句话说,如果interrupt()执行时,t线程正在sleep,那么interrupt()在将标志位设置为true后,又会直接将sleep强行唤醒。sleep在该程序中,占据了绝绝绝大部分的时间,因此当interrupt()执行时,几乎一定会遇到正在sleep的情况。注意:当sleep被提前唤醒的时候,sleep会自动把isInterrupted标志位再次清空(即把true又变为false)。这就导致了下次再判断循环条件,循环条件还成立,因此循环还在继续执行。

线程等待

线程之间是并发执行的,操作系统对于线程的调度是无序的,无法判断两个线程的执行先后。

join()方法

  • 在一个线程中调用另一个线程的join()方法就是让当前线程阻塞,等待另一个线程的执行结束
  • 带参数,就是最多等待的时间