进程与线程
在学习Java多线程之前,我们需要搞清楚进程与线程之间的区别。
进程是程序的一次动态执行过程,它需要经历从代码加载,代码执行到执行完毕的一个完整的过程,这个过程也是进程本身从产生,发展到最终消亡的过程。多进程操作系统能同时达运行多个进程(程序),由于 CPU 具备分时机制,所以每个进程都能循环获得自己的CPU 时间片。由于 CPU 执行速度非常快,使得所有程序好像是在同时运行一样。
多线程是实现并发机制的一种有效手段。进程和线程一样,都是实现并发的一个基本单位。线程是比进程更小的执行单位,线程是进程的基础之上进行进一步的划分。所谓多线程是指一个进程在执行过程中可以产生多个更小的程序单元,这些更小的单元称为线程,这些线程可以同时存在,同时运行,一个进程可能包含多个同时执行的线程。
实现方式
在 Java 中实现多线程有两种手段,一种是继承 Thread 类,另一种就是实现 Runnable 接口。下面我们就分别来介绍这两种方式的使用。由于比较简单直接贴上实现代码。
实现 Runnable 接口
public class RunnableDemo implements Runnable {
public static void main(String[] args) {
//可以直接在构造方法中设置线程名
new Thread(new RunnableDemo(), "线程1").start();
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
// 获取当前线程名
System.out.println(Thread.currentThread().getName() + "=======" + i);
}
}
}
继承Thread类
public class ThreadDemo extends Thread {
public static void main(String[] args) {
ThreadDemo threadDemo = new ThreadDemo();
threadDemo.setName("thread1"); //设置线程名
threadDemo.start(); // 启动线程
}
// 重写线程run方法
@Override
public void run() {
for (int i = 0; i < 10; i++) {
// 获取当前线程名
System.out.println(Thread.currentThread().getName() + "=======" + i);
}
}
}
启动方式
线程的启动方式是通过start()方法而不是run()方法!run方法是线程中的执行方法!
- start()方法来启动线程,真正实现了多线程运行。这时无需等待run方法体代码执行完毕,可以直接继续执行下面的代码;通过调用Thread类的start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运行。 然后通过此Thread类调用方法run()来完成其运行操作的, 这里方法run()称为线程体,它包含了要执行的这个线程的内容, Run方法运行结束, 此线程终止。然后CPU再调度其它线程。
- run()方法当作普通方法的方式调用。。
多线程的基本原理
接下来我们了解一下多线程的基本原理,整体的原理如下。
我们调用线程的start方法之后,实际上底层做了很多事情,具体的实现简图如下,画得不一定工整,但是能够表达大概意思就行。
OS调度算法有很多,比如先来先服务调度算法(FIFO)、最短优先(就是对短作业的优先调度)、时间片轮转调度等。这部分属于操作系统的相关知识。
大致流程:
- 编写ThreadDemo代码
- Java通过start()方法启动线程
- 在JVM中开启系统线程的启动方法
- 根据操作系统调用不同的执行方法
- 线程执行
线程的运行状态
一般来说,在Java中,线程的状态一共是6种状态,分别是:
- NEW:初始状态,线程被构建,但是还没有调用start方法
- RUNNABLED:运行状态,JAVA线程把操作系统中的就绪和运行两种状态统一称为“运行中”
- BLOCKED:阻塞状态,表示线程进入等待状态,也就是线程因为某种原因放弃了CPU使用权,阻塞也分为几种情况
- 等待阻塞:运行的线程执行wait方法,jvm会把当前线程放入到等待队列
- 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被其他线程锁占用了,那么jvm会把当前的线程放入到锁池中
- 其他阻塞:运行的线程执行Thread.sleep或者t.join方法,或者发出了I/O请求时,JVM会把当前线程设置为阻塞状态,当sleep结束、join线程终止、io处理完毕则线程恢复
- WAITING: 等待状态
- TIME_WAITING:超时等待状态,超时以后自动返回
- TERMINATED:终止状态,表示当前线程执行完毕
线程的终止
如何正确停止一个线程呢?这个问题要细聊,还是有很多东西可以说的。
我们知道Thread提供了线程的一些操作方法,比如stop、suspend等,这些方法可以终止一个线程或者挂起一个线程,但是这些方法都不建议大家使用。原因比较简单:
举个例子,假设一个线程中,有多个任务在执行,此时,如果调用stop方法去强行中断,那么这个时候相当于是发送一个指令告诉操作系统把这个线程结束掉,但是操作系统的这个结束动作完成不代表线程中的任务执行完成,很可能出现线程的任务执行了一半被强制中断,最终导致数据产生问题。这种行为类似于在linux系统中执行 kill -9类似,它是一种不安全的操作。
那么除了这种方法之外,还有什么方式可以实现线程的终止呢?要了解这个问题,我们首先需要知道,一个线程什么情况下算是终止了。
一个线程在什么情况下算执行结束
我们分析一下下面这段代码,通过start()启动一个线程之后,本质上就是执行这个线程的run方法。
那么如果这个线程在run方法执行完之前,一直处于运行状态,直到run方法中的指令执行完毕,那么这个线程就会被销毁。
public class MyThread extends Thread {
public void run() {
System.out.println("MyThread.run()");
}
}
MyThread myThread1 = new MyThread();
myThread1.start();
在正常情况下,这个线程是不需要人为干预去结束的。如果要强制结束,只能走stop这个方法。
那在哪些情况下,线程的中断需要外部干预呢?
- 线程中存在无限循环执行,比如while(true)循环
- 线程中存在一些阻塞的操作,比如sleep、wait、join等。
存在循环的线程
假设存在如下场景,在run方法中,存在一个while循环,因为这个循环的存在使得这个run方法一直无法运行结束,这种情况下,如何终止呢?
public class MyThread extends Thread {
public void run() {
while(true){ System.out.println("MyThread.run()"); }
}
}
MyThread myThread1 = new MyThread();
myThread1.start();
按照我们开发的思维来说,首先要解决的就是,while(true)这个循环,必须要有一个结束条件,其次是要在其他地方能够修改这个结束条件让该线程感知到变化。假设我们把while(true)改成while(flag),这个flag可以作为共享变量被外部修改,修改之后使得循环条件无法被满足,从而退出循环并且结束线程。
这段逻辑其实非常简单,其实就是给了线程一个退出的条件,如果没有这个条件,那么线程将会一直运行。
实际上,在Java提供了一个 interrupt 方法,这个方法就是实现线程中断操作的,它的作用和上面讲的这个案例的作用一样。
当其他线程通过调用当前线程的interrupt方法,表示向当前线程打个招呼,告诉他可以中断线程的执行了,至于什么时候中断,取决于当前线程自己。
线程通过检查资源是否被中断来进行相应,可以通过isInterrupted()来判断是否被中断。
public class InterruptDemo {
private static int i;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
//默认情况下 isInterrupted返回false、通过thread.interrupt变成了true
i++;
}
System.out.println("Num:" + i);
}, "interruptDemo");
thread.start();
TimeUnit.SECONDS.sleep(1);
//thread.interrupt(); //加和不加的效果
}
}
这种通过标识位或者中断操作的方式能够使线程在终止时有机会去清理资源,而不是武断地将线程停止,因此这种终止线程的做法显得更加安全和优雅。
处于阻塞状态下的线程中断
另外一种情况,就是当线程处于阻塞状态下时,我想要中断这个线程,那怎么做呢?
public class InterruptDemo {
private static int i;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
//默认情况下 isInterrupted返回false、通过thread.interrupt变成了true
// 循环状态
//i++;
//阻塞状态
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Num:" + i);
}, "interruptDemo");
thread.start();
TimeUnit.SECONDS.sleep(1);
//thread.interrupt(); //加和不加的效果
}
}
从这个例子中反馈出一个问题,我们平时在线程中使用的sleep、wait、join等操作,它都会抛出一个InterruptedException异常,为什么会抛出异常,是因为它在阻塞期间,必须要能够响应被其他线程发起中断请求之后的一个响应,而这个响应是通过InterruptedException来体现的。
但是这里需要注意的是,在这个异常中如果不做任何处理的话,我们是无法去中断线程的,因为当前的异常只是响应了外部对于这个线程的中断命令,同时,线程的中断状态也会复位
,如果需要中断,则还需要在catch中添加下面的代码:
//阻塞状态
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt(); //再次中断
}
所以,InterruptedException异常的抛出并不意味着线程必须终止,而是提醒当前线程有中断的操作发生,至于接下来怎么处理取决于线程本身,比如:
- 直接捕获异常不做任何处理
- 将异常往外抛出
- 停止当前线程,并打印异常信息