文章目录
- 前言
- 我们为什么要使用线程而不是进程来实现并发编程
- 什么是线程
- 进程和线程的区别
- 如何使用Java实现多线程
- 创建线程
- 1.创建一个继承 Thread 类的线程类
- 2.实现 Runnable 接口
- 匿名内部类方式实现 Runnable 接口
- lambda 表达式实现 Runnable 接口
- Thread 类的常见构造方法
- Thread 的几个常见属性
- 启动线程
- 终止线程
- 1.自定义标志位终止线程
- 2.使用 Thread 自带的标志位终止线程
- 线程等待
前言
前面我们了解了什么是进程以及如何实现进程调度,那么今天我将为大家分享关于线程相关的知识。在学习线程之前,我们认为进程是操作系统执行独立执行的单位,但其实并不然。线程是操作系统中能够独立执行的最小单元。只有掌握了什么是线程,我们才能实现后面的并发编程。
我们为什么要使用线程而不是进程来实现并发编程
实现并发编程为什么不使用多进程,而是使用多线程呢?主要体现在几个方面:
- 创建一个进程的开销很大
- 调度一个进程的开销很大
- 销毁一个进程的开销很大
开销不仅体现在时间上,还体现在内存和 CPU 上。现在以”快“著称的互联网时代,这种大开销是不受人们欢迎的。那么为什么多线程就可以实现快捷的并发编程呢?
- 共享资源:多个线程之间共用同一部分资源,大大减少了资源的浪费
- 创建、调度、销毁的开销小:相较于进程的创建、调度和销毁,线程的创建、调度和销毁就显得很轻量,这样也大大节省了时间和资源的浪费
- 现在的计算机 CPU 大多都是多核心模式,我们的多线程模式也更能利用这些优势
什么是线程
线程是操作系统能够独立调度和执行的最小执行单元。线程是进程内的一个执行流程,也可以看作是进程的子任务。与进程不同,线程在进程内部创建和管理,并且与同一进程中的其他线程共享相同的地址空间和系统资源。
只有当第一个线程创建的时候会有较大的开销,后面线程的创建开销就会小一点。并发编程会尽量保证每一个线程在不同的核心上单独执行,互不干扰,但也不可避免的出现在单核处理器系统中,线程在一个 CPU 核心上运行,它们通过时间片轮转调度算法使得多个线程轮流执行,给我们一种同时执行感觉。
线程是操作系统调度执行的基本单位
进程和线程的区别
一个进程中可以包含一个线程,也可以包含多个线程。
-
资源和隔离:进程是操作系统中的一个独立执行单位,具有独立的内存空间、文件描述符、打开的文件、网络连接等系统资源。每个进程都拥有自己的地址空间,进程间的数据不共享。而线程是进程内的执行流程,共享同一进程的地址空间和系统资源,可以直接访问和修改相同的数据。
-
创建和销毁开销:相对于进程,线程的创建和销毁开销较小。线程的创建通常只涉及创建一个新的执行上下文和一些少量的内存。而进程的创建需要分配独立的内存空间、加载可执行文件、建立进程控制块等操作,开销较大。
-
并发性和响应性:由于线程共享进程的地址空间,多个线程可以在同一进程内并发执行任务,共享数据和通信更加方便。因此,线程的切换成本较低,可以实现更高的并发性和响应性。而进程之间通常需要进程间通信(IPC)的机制来进行数据交换和共享,开销较大,响应性较低。
-
管理和调度:进程由操作系统负责管理和调度,每个进程之间是相互独立的。而线程是在进程内部创建和管理的,线程调度和切换由操作系统的线程调度器负责。线程的调度通常比进程的调度开销小,线程切换更快。
-
安全性和稳定性:由于进程之间相互独立,一个进程的崩溃不会影响其他进程的正常运行,因此进程具有更好的安全性和稳定性。而一个线程的错误或异常可能会导致整个进程崩溃。
前面我们所说的 PCB 其实也是针对线程来说的,一个线程具有一个 PCB 属性,一个进程可以含有一个或多个 PCB。
PCB 里的状态:上下文,优先级,记账信息,都是每个线程有自己的,各自记录自己的,但是同一个进程里的PCB之间,pid是一样的,内存指针和文件描述符表也是一样的。
如何使用Java实现多线程
在Java中使用一个线程大致分为以下几个步骤:
- 创建线程
- 启动线程
- 终止线程
- 线程等待
创建线程
在Java中执行线程操作依赖于 Thread
类。并且创建一个线程具有多种方法。
- 创建一个线程类继承自 Thread 类
- 实现 Runnable 接口
1.创建一个继承 Thread 类的线程类
class MyThread extends Thread {
@Override
public void run() {
System.out.println("这是一个MyThread线程");
}
}
我们需要重写 run 方法,而 run 方法是指该线程要干什么。
创建实例对象
public class TreadDemo1 {
public static void main(String[] args) {
Thread t = new MyThread();
}
}
2.实现 Runnable 接口
创建一个线程我们不仅可以直接创建一个继承自 Thread 的线程类,我们也可以直接实现 Runnable 接口,因为通过源码我们可以知道 Thread 类也实现了 Runnable 接口。
我们可以将 Runnable 作为一个构造方法的参数传进去。
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("这是一个线程");
}
}
public class ThreadDemo2 {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
}
}
但是这种实现 Runnable 接口的方式会显得很麻烦,因为每个线程执行的内容大多是不同的,所以我们可以采用下面两种方式来实现 Runnable 接口。
- 匿名内部类
- lambda 表达式
匿名内部类方式实现 Runnable 接口
public class ThreadDemo3 {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("这是一个线程");
}
});
}
}
lambda 表达式实现 Runnable 接口
public class ThreadDemo4 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("这是一个线程");
});
}
}
Thread 类的常见构造方法
方法 | 说明 |
---|---|
Thread() | 创建对象 |
Thread(Runnable target) | 使用 Runnable 对象创建线程对象 |
Thread(String name) | 创建线程对象,并命名 |
Thread(Runnable target,String name) | 使用 Runnable 对象创建线程对象,并命名 |
Thread(ThreadGroup group,Runnable target | 线程可以被用来分组管理,分好的组即为线程组,这个我们目前了解即可 |
Thread 类有很多构造方法,大家有兴趣可以自己去看看。
Thread 的几个常见属性
属性 | 获取方法 |
---|---|
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否后台进程 | isDaemon() |
是否存活 | isAlive |
是否被中断 | isInterrupted() |
- ID 是线程的唯一标识,不同线程不会重复
- 名称是各种调试工具用到
- 状态表示线程当前所处的一个情况
- 优先级高的线程理论上来说更容易被调度到
- 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
- 是否存活,即简单的理解,为 run 方法是否运行结束了
- 线程的中断问题,下面我们进一步说明
我们前面创建的都是前台进程,我们可以感知到的,那么什么叫做后台进程呢?
后台进程是指在计算机系统中以低优先级运行且不与用户交互的进程。与前台进程相比,后台进程在运行时不会占据用户界面或终端窗口,并且通常在后台默默地执行任务。
后台进程通常用于执行系统服务、长时间运行的任务、系统维护或监控等。它们在后台运行,不需要用户的直接参与或操作,而且可以持续运行,即使用户退出或注销系统。
启动线程
我们上面只是创建了线程,要想让线程真正的起作用,我们需要手动启动线程。线程对象.start()
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("这是一个线程");
}
}
public class ThreadDemo2 {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start();
}
}
这里有人看到输出结果可能会问了,这跟我直接调用 run 方法好像没什么区别吧?我们这个代码肯定看不出来区别,所以我们稍稍修改一下代码。
class MyRunnable implements Runnable {
@Override
public void run() {
while(true) {
System.out.println("hello MyThread!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class ThreadDemo2 {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start();
while(true) {
System.out.println("hello main!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
这里 Thread.sleep()
的作用是使线程停止一会,防止进行的太快,我们不容易看到结果,并且这里的 Thread.sleep()
方法还需要我们抛出异常
我们可以看到这里的执行结果是 main 线程和 t 线程都执行了,而不是只执行其中的一个线程。不仅如此,这两个线程之间没有什么规定的顺序执行,而是随机的,这种叫做抢占式执行,每个线程都会争抢资源,所以会导致执行顺序的不确定,也正是因为多线程的抢占式执行,会导致后面的线程安全问题。
那么我们再来看看,如果直接调用 run 方法,而不是 start 方法会有什么结果。
当直接调用 run 方法的话,也就只会执行 t 对象的 run 方法,而没有执行 main 方法后面的代码,也就是说:当直接调用 run 方法的时候,线程并没有真正的启动,只有调用 start 方法,线程才会启动。
我们也可以通过 Java 自带的 jconsle
来查看当前有哪些Java进程。
我们需要找到 jconsole.exe
可执行程序。通常在这个目录下C:\Program Files\Java\jdk1.8.0_192\bin
我们也可以点进来看看。
终止线程
通常当主线程 main 执行完 mian 方法之后或者其他线程执行完 run 方法之后,线程就会终止,但是我们也可以在这之前手动终止线程。但是我们这里终止线程并不是立刻终止,也就相当于这里只是建议他这个线程停止,具体要不要停止得看线程的判断。
- 自定义标志位来终止线程
- 使用 Thread 自带的标志位来终止线程
1.自定义标志位终止线程
public class ThreadDemo4 {
private static boolean flg = false; //定义一个标志位
public static void main(String[] args) {
Thread t = new Thread(() -> {
while(!flg) {
System.out.println("hello mythread!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
System.out.println("线程开始");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
flg = true; /修改标志位使线程终止
System.out.println("线程结束");
}
}
2.使用 Thread 自带的标志位终止线程
可以使用 线程对象.interrupt()
来申请终止线程。并且使用 Thread.currentThread,isInterrupted()
来判断是否终止线程。
Thread.currentThread()
获取到当前线程对象
public class ThreadDemo4 {
private static boolean flg = false;
public static void main(String[] args) {
Thread t = new Thread(() -> {
while(!Thread.currentThread().isInterrupted()) {
System.out.println("hello mythread!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
System.out.println("线程开始");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
t.interrupt();
System.out.println("线程结束");
}
}
发现了没,这里抛出了异常,但是线程并没有终止,为什么呢?问题出在哪里呢?
其实这里问题出在 Thread.sleep
上,如果线程在 sleep 中休眠,此时调用 interrupt() 会终止休眠,并且唤醒该线程,这里会触发 sleep 内部的异常,所以我们上面的运行结果就抛出了异常。那么为什么线程又被唤醒了呢?
interrupt 会做两件事:
- 把线程内部的标志位给设置成true,也就是
!Thread.current.isInterrupt()
的结果为true - 如果线程在进行 sleep ,就会触发吟唱,把 sleep 唤醒
但是 sleep 在唤醒的时候,还会做一件事,把刚才设置的这个标志位,再设置回false(清空标志位),所以就导致了线程继续执行。那么如何解决呢?
很简单,因为 sleep 内部发生了异常,并且我们捕获到了异常,所以我们只需要在 catch
中添加 break
就行了。
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
这也就相当于,我 t 线程拒绝了你的终止请求。
线程等待
在多线程中,可以使用 线程对象.join()
来使一个线程等待另一个线程执行完或者等待多长时间后再开始自己的线程。
方法 | 说明 |
---|---|
public void join() | 等待线程结束 |
public void join(long millis) | 等待线程结束,最多等 millis 毫秒 |
public void join(long millis,int nanos) | 同理,但可以更高精度 |
public class ThreadDemo5 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
for(int i = 0; i < 5; i++) {
System.out.println("hello mythread!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
try {
t.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
for(int i = 0; i < 5; i++) {
System.out.println("hello main!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
在那个线程中调用的 线程对象.join()
就是哪个线程等待,而哪个线程调用 join()
方法,那么这个线程就是被等待的。而这个等待的过程也被称为阻塞。如果在执行 join 的时候,调用 join 方法的线程如果已经结束了,那么就不会发生阻塞。