1、什么是进程与线程
1.1 含义
1.1.1 进程
进程是指正在运行的程序的实例。在操作系统中,一个进程代表了一个正在执行的程序,它包括了程序的代码、数据以及程序执行时所需要的系统资源。
最直观的就是我们任务管理器:
任务管理器中的每一个应用程序都是一个进程。
1.1.2 线程
线程是进程中的一个执行单元。一个进程可以包含多个线程,每个线程都拥有独立的执行路径,可以独立执行特定的任务,一个进程中的所有线程都共享这个进程的资源。
比如下面的这个例子。 进程:餐厅的整个经营过程可以看作一个进程。它包括了所有的资源和活动,例如厨房、餐厅大厅、服务员、厨师、顾客等。 线程:在餐厅中,服务员可以看作是一个线程。有很多个服务员,他们分别负责接待顾客、记录订单、传递菜单、上菜等任务。服务员是餐厅进程中的一个执行单元,他与其他服务员共享同一份资源,如餐厅的桌子、厨房、食材等。多个服务员可以并行执行任务,提高效率。
1.1.3 线程与进程的区别
进程的诞生就是为了实现程序的并发执行。所谓并发,就是在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行。
线程的诞生目的是为了减少程序在并发执行时所付出的时空开销,使操作系统具有更好的并发性。
- 地址空间:线程共享本进程的地址空间,而进程之间是独立的地址空间。
- 资源:线程共享本进程的资源如内存、I/O、CPU等,而进程之间的资源是独立的。
- 健壮性:多进程要比多线程健壮,一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。
- 执行过程:每个独立的进程有一个程序运行的入口、顺序执行序列和程序出口,执行开销大。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,执行开销小。
- 可并发性:两者均可并发执行。
- 切换时:进程切换时,消耗的资源大,效率低。所以涉及到频繁的切换时,使用线程要好于进程。
- 其他:进程是操作系统资源分配的基本单位,线程是操作系统调度的基本单位。
1.2 PCB与TCB
进程控制块 PCB(Process Control Block),它是用来描述和控制进程的运行的一个数据结构。它是进程存在的唯一标志,也是操作系统管理进程的重要依据。
TCB(Thread Control Block)是线程控制块的缩写,是操作系统中用来描述和控制线程运行的一种数据结构。
它们的关系:
PCB中存储了进程的标识符、状态、优先级、寄存器、程序计数器、堆栈指针、资源清单、同步和通信机制等信息。PCB是进程存在的唯一标志,也是实现进程切换和调度的基础。
TCB与PCB是差不多的,TCB中也存储了线程的标识符、状态、优先级、寄存器、程序计数器、堆栈指针、信号屏蔽等信息。TCB是线程存在的唯一标志,也是实现线程切换和调度的基础。
2.创建线程的几种方式
线程是操作系统中的概念。操作系统内核实现了线程这样的机制,并且对用户层提供了一些 API 供用户使用。Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装。
2.1 继承 Thread 类
(1)继承 Thread 来创建一个线程类。
class MyThread extends Thread{
//重写run方法,run 表示这个线程需要执行的任务
@Override
public void run() {
System.out.println("这个线程需要执行的任务");
}
}
(2)创建 MyThread 实例。
//创建一个 MyThread 实例
MyThread myThread = new MyThread();
(3)调用 start() 方法,线程开始执行。
//start 方法表示这个线程开始执行,注意,这里不是调用 run()方法
myThread.start();
2.2 实现 Runnable 接口
(1)实现 Runnable 接口。
//通过 实现 Runnable 接口来创建一个线程
class MyRunnable implements Runnable{
//需要重写run方法
@Override
public void run() {
System.out.println("这个线程需要执行的任务");
}
}
(2)创建一个 Thread 实例。
//创建 Thread 类实例, 调用 Thread 的构造方法时将 Runnable 对象作为参数。
Thread thread = new Thread(new MyRunnable());
(3)调用 start() 方法,线程开始执行。
//线程开始运行
thread.start();
2.3 其它变形
- 匿名内部类创建 Thread 子类对象。
Thread thread1 = new Thread(){
@Override
public void run() {
System.out.println("使用匿名类创建 Thread 子类对象");
}
};
- 匿名内部类创建 Runnable 子类对象。
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("使用匿名类创建 Runnable 子类对象");
}
});
- lambda 表达式创建 Runnable 子类对象。
Thread thread3 = new Thread(()-> {
System.out.println("使用匿名类创建 Thread 子类对象");
});
3.Thread 类的常见方法
3.1 Thread 的常见构造方法
方法 | 说明 |
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用 Runnable 对象创建线程对象 |
Thread(String name) | 创建线程对象,并命名 |
Thread(Runnable target, String name) | 使用 Runnable 对象创建线程对象,并命名 |
Thread thread = new Thread("小明"){
@Override
public void run(){
while(true){
System.out.println("我是小明");
}
}
};
这个线程的名字为“小明”,如何查看这个线程的名字呢?在Java开发工具包(JDK) 中,可以使用jconsole来监视线程。
- 打开你的 jdk 路径,我这里是C:\Program Files\Java\jdk1.8.0_31\bin,打开该路径下的jconsole工具。
- 运行上面的代码,然后根据如下操作:
可以看到有一个叫“小明”的线程,这个就是我们创建的线程。
3.2 获取 Thread 属性的方法
属性 | 获取方法 |
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否后台线程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
- ID 是线程的唯一标识,不同线程不会重复。
- 名称是线程的名称。
- 状态表示线程当前所处的一个情况,比如阻塞、运行等。
- 优先级高的线程更容易被调度到。
- 关于后台线程:后台线程(Daemon Thread)是在程序运行过程中在后台提供服务的线程。与前台线程(也称为用户线程)相对,后台线程不会阻止程序的终止。当所有的前台线程(用户线程)结束时,后台线程会自动被终止,即使它们尚未执行完毕。
- 是否存活,即简单的理解,为 run 方法是否运行结束了。
3.3 启动线程的方法 start()
前文已经演示过了,之前我们已经看到了如何通过重写 run 方法创建一个线程对象,但线程对象被创建出来并不意味着线程就开始运行了。重写 run 方法是提供给线程要做的事情的指令清单。而调用 start() 方法,线程才能真正独立去执行,调用 start 方法, 才真的在操作系统的底层创建出一个线程。
3.4 线程睡眠 sleep()
方法 | 说明 |
public static void sleep(long millis) throws InterruptedException | 休眠当前线程 millis 毫秒 |
public static void sleep(long millis, int nanos) throws InterruptedException | 可以更高精度的休眠 |
如下代码:
public static void main(String[] args) throws InterruptedException {
long begin = System.currentTimeMillis();
Thread.sleep(3000);//睡眠当前线程
long end = System.currentTimeMillis();
System.out.println(end - begin);
}
结果:
它的一些更详细内容后文介绍。
3.5 中断线程
这里的中断不是马上就中断的意思,具体的我们看下面的案例。
这里有两个线程,通过一个线程来中断另一个线程,这里先不使用Thread的方法。
public class Main {
public static volatile boolean flag = true;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(){
@Override
public void run(){
// flag 是中断标记
while(flag){
try {
Thread.sleep(500);//睡眠
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Hi");
}
}
};
//开启线程
thread.start();
//中断线程
Thread.sleep(3000); //睡眠 3 秒
flag = false;//更改标记
System.out.println("开始中断线程");
}
}
(volatile的作用这里不作介绍,后面会持续更新。)
结果:
你会发现,这里的中断就是改变标记位的值,并且这里是有延迟的,只有sleep睡眠结束的时候才能结束线程。
所以就可以使用Thread提供的方法,即: isInterrupted()或者interrupted(),它们的优点就是能立刻唤醒睡眠的线程,看下面的案例。
方法 | 说明 |
public void interrupt() | 中断对象关联的线程,如果线程正在阻塞,则以异常方式通知, 否则设置标志位 |
public static boolean interrupted() | 判断当前线程的中断标志位是否设置,调用后清除标志位 |
public boolean isInterrupted() | 判断对象关联的线程的标志位是否设置,调用后不清除标志位 |
这里的标志位的详细介绍在后面。
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(){
@Override
public void run(){
// flag 是中断标记
while(!Thread.currentThread().isInterrupted()){
try {
Thread.sleep(500);//随眠
} catch (InterruptedException e) {
e.printStackTrace();//打印报错信息
}
System.out.println("Hi");
}
}
};
//开启线程
thread.start();
Thread.sleep(3000); //睡眠 3 秒
thread.interrupt();//中断 thread 线程,把标记位置为true。
System.out.println("开始中断线程");
}
这里解释一下!Thread.currentThread().isInterrupted(),Thread.currentThread()的作用是返回当前线程对象的引用,跟this差不多:
方法 | 说明 |
public static Thread currentThread(); | 返回当前线程对象的引用 |
isInterrupted():如果当前线程被中断就返回true,否则返回false。
这里的中断要根据中断标志位来看,中断标志位(interrupt flag)是线程的内部状态之一,实际上是存在于线程的内部数据结构中。**具体来说,中断标志位是线程对象的一个成员变量,用于表示线程的中断状态。**这个标志位在线程创建时被初始化为false,当调用线程的interrupt()方法时,中断标志位会被设置为true,表示线程被中断。
Thread.currentThread().isInterrupted(),这里就表示当前线程是否中断,如果中断就返回true,否则false。最后在前面加上一个!,来作为while的条件,那就是中断就返回false,否则true。
最后我们来看看结果,注意,上面我们已经调用了thread.interrupt(),所以预期是sleep抛异常,然后结束循环(结束线程),
结果:
为什么这里程序没有结束呢?我们来慢慢分析,没结束显然是while循环的条件还是ture,那么Thread.currentThread().isInterrupted()的值就是false,就表示线程不是处于中断状态,What?我们不是已经调用了interrupt()方法了吗?
原因就是:当线程被阻塞时,比如调用了sleep()、join() 或 wait()方法,如果收到了中断请求,这些方法会抛出 InterruptedException 异常,并清除中断状态!,这里的清除中断状态就是把标记位置为false,表示该线程没有被中断。
如果解决这个问题呢?直接在catch中加上break就行了。
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(){
@Override
public void run(){
// flag 是中断标记
while(!Thread.currentThread().isInterrupted()){
try {
Thread.sleep(500);//随眠
} catch (InterruptedException e) {
//这里进行善后处理
break;
}
System.out.println("Hi");
}
}
};
//开启线程
thread.start();
//中断线程
Thread.sleep(3000); //睡眠 3 秒
thread.interrupt();//中断 thread 线程
System.out.println("开始中断线程");
}
像这样做的优点有哪些呢?线程可以根据自己的逻辑来处理中断请求,比如结束循环、关闭资源或者忽略。
总结:先觉条件,调用interrupt();
- 如果线程因为调用 wait/join/sleep 等方法而阻塞挂起,则以InterruptedException异常的形式通知,重置中断标记。这时要不要结束线程取决于 catch 中代码的写法。可以选择忽略这个异常,也可以跳出循环结束线程。
- 如果没有 wait/join/sleep 等方法,
- Thread.interrupted() 判断当前线程的中断标志是否被设置,判断后,重置中断标志。
- Thread.currentThread().isInterrupted() 判断当前线程的中断标志是否被设置,不重置除中断标志。
3.6 等待线程 join()
有时,我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作。
方法 | 描述 |
join() | 等待该线程执行完成 |
join(long millis) | 最多等待指定的毫秒数,如果线程在该时间内没有执行完成,当前线程将继续执行 |
join(long millis, int nanos) | 最多等待指定的毫秒数和纳秒数,如果线程在该时间内没有执行完成,当前线程将继续执行 |
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(){
@Override
public void run(){
for (int i = 0; i < 9; i++) {
System.out.println("thread");
}
}
};
thread.start();//开启线程
System.out.println("join()前");
try {
thread.join();//先等 thread 线程执行完
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("join()后");
}
}
如果在join()之前thread线程已经执行完了的话,join()就不会阻塞了。
这些join()方法提供了线程之间的协同执行机制,允许一个线程等待另一个线程执行完成后再继续执行。这对于需要线程执行顺序或线程之间的依赖关系的场景非常有用。