🏀🏀🏀来都来了,不妨点个关注!
🎧🎧🎧博客主页:欢迎各位大佬!
文章目录
前言
在上一篇进程和线程中,我们介绍了进程和线程的概念,今天我们来介绍一下在Java中如何进行多线程编程,此时就有人问了,哪为啥不是进行多进程编程呢,这是因为操作系统其实是提供了一组进行多进程编程的API,但JDK中并没有给我们Java程序员封装这些多进程的API,同时,在上一篇我们介绍进程和线程中介绍过,进程切换是开销比较大的操作,而线程切换的成本比较低。
1.Thread类
在Java标准库中提供了一个类Thread来表示一个线程。下面我们来简单的介绍一下如何通过Thread来进行多线程编程。
1.1 通过Thread进行多线程编程
class Mythread extends Thread {
public void run() {
System.out.println("hello world");
}
}
public class ThreadDemo {
public static void main(String[] args) {
Thread t = new Mythread();
t.start();
}
}
运行结果如下:
下面我们来理解一下上述代码都干了些什么。
我们是先创建一个MyThread类的实例,然后再通过t.start()启动这个线程,这相当于在进程中又搞了一套流水线,开始和main并发的执行另外一个任务。
上述代码主要是涉及到两个线程:
- 第一个就是main方法对应的线程(每一个进程中至少有一个线程,也可以叫作主线程)
- 第二个就是通过t.start()创建的新线程
1.2 通过线程的生命周期理解Thread类
线程的生命周期是一个从创建到终止的完整过程,它包含了多个状态之间的转换。
下面是线程生命周期的五个主要状态:
- 新建(New):当使用new关键字创建一个线程对象时,该线程就处于新建状态。此时,线程对象已经分配了内存空间,但还没有被执行。
- 就绪(Runnable):调用线程的start()方法后,线程会进入就绪状态。这时,线程已经获取了执行所需的资源(如JVM为其创建的方法调用栈和程序计数器),并等待CPU的调度。
- 运行(Running:当就绪状态的线程被CPU调度并获得执行权时,它进入运行状态。此时,线程正在执行其run()方法中的代码,并占用CPU资源。
- 阻塞(Blocked):在运行过程中,线程可能因为多种原因(如等待I/O操作、调用sleep()或wait()方法等)而进入阻塞状态。此时,线程暂时无法获取CPU资源,并等待某个条件满足后被唤醒。
- 销毁(Terminated/Dead):当线程执行完毕(run()方法执行完成)、被强制终止(尽管不推荐使用stop()方法,因为它容易导致死锁)或因为异常而结束时,线程进入销毁状态。此时,线程所占用的资源被释放,线程的生命周期结束。
由此可知,当我们通过创建一个MyThread实例的时候,该线程就处于新建状态,当我们调用该线程的start()方法后,线程就会进入就绪状态等待CPU的调度,当处于就绪态的线程被CPU调度获得执行权的时候,此时该线程执行自身的run()方法,并占用CPU的资源。
1.3 创建Thread的几种方法
- 继承Thread类重写run方法
这个就是我们最开始介绍Thread类使用的方法
class Mythread extends Thread {
public void run() {
System.out.println("hello world");
}
}
public class ThreadDemo1 {
public static void main(String[] args) {
Thread t = new Mythread();
t.start();
}
}
- 继承Runnable接口重写run方法
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("hello t");
}
}
public class ThreadDemo2 {
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
Thread t = new Thread(runnable);
t.start();
}
}
- 继承Thread类,使用匿名内部类的方式
public class ThreadDemo3 {
public static void main(String[] args) {
Thread t = new Thread() {
@Override
public void run() {
while (true) {
System.out.println("hello t");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t.start();
while (true) {
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 继承Runnable接口,使用匿名内部类的方式
public class ThreadDemo4 {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
while(true) {
System.out.println("hello t");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
t.start();
while(true) {
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- lambda表达式(最推荐在这里插入代码片的写法,最简单最直观的写法
public class ThreadDemo5 {
public static void main(String[] args) {
Thread t = new Thread( () -> {
while (true) {
System.out.println("hello t");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
while (true) {
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
2.Thread类及常见方法
2.1 Thread常见方法
(1)Thread()
创建线程对象
Thread t = new Thread();
(2)Thread()
使用Runnable对象创建线程对象
Thread t2 = new Thread(new MyRunnable());
(3)Thread(String name)
创建线程并命名
Thread t3 = new Thread("这是我的名字");
(4)Thread(Runnable target,String name)
使用Runnable对象创建线程对象并命名
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");
2.2 Thread类的常见属性
- ID
getId()
ID 是线程的唯一标识,不同线程不会重复 - 名称
getName()
名称是各种调试工具用到 - 状态
getState()
状态表示线程当前所处的一个情况,对应着上面我们介绍的线程的生命周期,下面我们看例子:
class Mythread extends Thread {
public void run() {
System.out.println("hello world");
}
}
public class ThreadDemo1 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Mythread();
System.out.println("实例化Thread之后的状态: " + t.getState());
t.start();
System.out.println("调用start()方法之后的状态" + t.getState());
t.sleep(1000);
System.out.println("run()方法执行之后的状态" + t.getState());
}
}
运行结果如下:
- 优先级
getPriority()
优先级对于系统来说只是给出"建议",理论上优先级越高,更容易被调度到 - 是否后台进程
isDaemon()
如果是true表示为后台线程(守护线程),false表示是前台线程(用户线程)
【后台线程】:后台线程不阻止java进程结束,哪怕后台线程还没执行完,java进程该结束就结束
【前台线程】:我们创建的线程默认是前台线程,可以通过setDaemon()设置成后台线程,JVM会在一个进程的所有非后台线程结束后,才会结束运行
public class ThreadDemo5 {
public static void main(String[] args) {
Thread t = new Thread( () -> {
System.out.println("hello t");
});
t.start();
//判断线程是否是后台线程
System.out.println(t.isDaemon());
}
}
运行结果如下:
6. 是否存活
isAlive()
判断当前的线程是否处于活动状态,描述的是操作系统里的那个线程是否存活,线程处于正在运行或准备开始运行的状态,就认为线程是"存活"的状态
1.在调用start()方法前只是给该线程分配了内存,所以isAlive()为false,而调用run()方法时,该线程被CPU调用,此时为存活状态,当执行完run()方法后过一会线程就没了,此时isAlive()为false,如下面代码运行结果:
class MyThread extends Thread {
@Override
public void run() {
System.out.println("调用run(): " + this.isAlive());
}
}
public class ThreadDemo1{
public static void main(String[] args) throws InterruptedException {
Thread t = new MyThread();
System.out.println("调用start(): " + t.isAlive());
t.start();
t.sleep(1000);
System.out.println("run()方法结束后: "+t.isAlive());
}
}
运行结果如下:
- 是否被中断
isInterrupted()
判断该线程是否被中断,被中断返回true,未被中断返回false,这个地方我们在下面会详情介绍
2.3 中断一个线程
线程的中断,就是让一个线程停止下来,需要注意的是,这里的中断不是让线程立刻停下来,它仅仅是一种协作机制,意味着线程在接收到中断请求后,可以选择在合适的时机响应这个请求,优雅地终止其运行。
这个其实也很好理解,比如现在我正在刷题中,此时我的女朋友小万过来说,你去给我买杯咖啡,此时就相当于小万告诉我要停止我的刷题了,但至于我什么时候停止,取决于我自己,我可以选择将题目写完再去给她买咖啡,我也可以选择立刻停下来去给她买咖啡。
那么如何通知一个线程需要中断了呢,目前常见的有两种方法:
- 自定义一个共享的标志位通知
- 使用Thread类自带的interrupt()方法
2.3.1 自定义标志位中断线程
public class ThreadDemo {
public static boolean isQuit = false;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while(!isQuit) {
System.out.println("hello t");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t 线程中止");
});
t.start();
Thread.sleep(3000);
isQuit = true;
}
}
上述代码,我们创建了一个线程循环打印“hello t”,当我们的标志位一直为false的情况下会一直循环打印,就会陷入死循环,此时我们在main()线程中休眠三秒后将标志位设置为true,此时就相当于通知t线程需要结束了,当t线程下一次进入循环发现循环判断条件为false就会退出循环执行下面的逻辑了。
2.3.2 调用interrupt()方法中断线程
在我们Thread类内置了一个标志位,用于表示线程的中断状态。这个标志位主要用于线程间的通信,允许一个线程请求另一个线程停止其正在执行的活动。
下面我们通过具体的代码进行举例:
public class ThreadDemo {
public static void main(String[] args) {
Thread t = new Thread( () -> {
//currentThread()用于获取当前线程的实例,此处就是获取线程t,
//isInterrupted()用于判断该线程是否被中断,上面Thread类的属性里提到
while (!Thread.currentThread().isInterrupted()) {
System.out.println("hello t");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//将标志位设为true
t.interrupt();
}
}
运行结果如下:
这里我们发现,结果并没有按照我们的预想那样,我们发现3s后调用t.interrupt()方法之后线程并没有结束,而是打印了一个异常信息后继续执行。这里我们就需要从interrupt()方法的两个作用来解释了:
- 将标志位设置为true
- 如果当前线程处在阻塞中(比如在执行sleep),此时就会把阻塞状态唤醒,并通过抛出异常的方式让sleep()结束。
这里需要注意一个非常的严重的问题就是,当sleep()被唤醒的时候,它会将isInterrupted()标志位清除(true—> false)。这就导致下次循环时,循环条件依然为true,继续打印“hello t”。
这里的解决方案就是在catch{}中加个break,当抛出异常后我们就直接跳出循环。如下图:
为什么sleep要清空标志位
目的就是为了让线程对于自身何时结束有一个明确的控制,这和我们最开始介绍中断的概念时是一样的,中断只是告诉线程需要结束了,而什么时候结束,都是由线程自身灵活控制的。
注意事项:
- 当线程在阻塞状态(如sleep()、wait()、join()等)中被中断时,会抛出InterruptedException异常,并且中断状态会被清除。因此,在捕获到这个异常后,如果需要保持中断状态,可能需要手动重新设置。
- 线程的中断是一种协作机制,而不是强制停止线程执行的方式。线程必须在其执行逻辑中适时地检查中断状态,并根据需要做出响应。