认识多线程
一、认识线程
1. 线程是什么?
一个线程就是一个“执行流”,每个线程之间都可以按照顺序执行自己的代码。多个线程之间 “同时”执行着多份代码。
一个可执行的.exe文件运行起来就是一个进程。一个进程可以有一个线程,或者多个线程。每个线程都是一个独立的执行流。多个线程之间,是并发执行的。多个线程可能是在一个 CPU 核心上同时运行,也可能是在多个 CPU 核心上,通过快速调度的方式,并发执行。操作系统真正调度的是线程,而不是进程。
举个例子:
2. 为什么要有多线程?
1. 首先,“并发变成” 成为了 “刚需”。
- 单核 CPU 的发展遇到了瓶颈。想要提高算力,就需要多核 CPU 。而并发编程能更充分利用多核 CPU 资源。
- 有些任务场景需要 “等待IO”,为了让等待 IO 的时间能够去做一些其他的工作,也需要用到并发编程。
2. 虽然多进程也能实现并发编程,但是线程比进程更轻量。
- 创建线程比创建进程更快
- 销毁线程比销毁进程更快
- 调度线程比调度进程更快
3.为了能够在多线程的基础之上,进一步提高开发的效率,又出现了“线程池(ThreadPool)” 和 “协程(Coroutine)”。
3.多线程具体是什么?
一个可执行的 (.exe)文件运行起来就是一个进程。同一个进程,内部可以并发的完成多组任务。其中,每一个任务就是一个线程。
例如:
微信 是一个程序,视频聊天时,同时使用网络传输、视频录制、声音录制等功能。
4.进程和线程的区别
-
进程包含线程,每一个进程至少有一个线程,即主线程。
-
进程是操作系统进行资源分配的最小单位,线程是操作系统调度执行的最小单位。
-
进程之间具有独立性,一个进程挂了,不会影响到别的进程;同一个进程的多个线程之间,一个线程挂了,可能会把整个进程都带走,从而影响到其他的线程。
-
进程有自己独立的内存空间和文件描述符表。进程和进程之间不共享内存空间;同一个进程的多个线程之间,共享一份地址空间和文件描述符表。
- 共享地址空间是指,一个线定义的变量,在另一个线程中也可以使用。
- 共享文件描述符表,一个线程打开的文件,在另一个线程中也可以继续使用。
只有在进程启动,创建第一个线程的时候,需要花成本去申请系统资源。一旦进程(第一个线程)创建完毕,此后后续在创建线程的,就不需要再申请资源了,或者说申请的资源会少很多,这样创建/销毁的效率就会提高不少。也就是线程之间可以实现资源的复用。
二、线程的创建
1.自定义 MyThread 继承 Thread 创建线程
- 自定义一个子类 MyThread ,子类继承 Java 标准库的 Thread 类,重写 run( )方法
- 通过向上转型的方式,创建子类对象。
- 启动线程
//1.创建子类
class MyThread extends Thread {
//2.重写方法
@Override
public void run() {
while(true){
System.out.println("hello MyThread!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
//3.通过向上转型实例化子类对象
Thread t = new MyThread();
//4.启动线程
t.start();
while(true){
System.out.println("hello main!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- run(),表示创建的线程需要执行的任务。
- start(),通过 start()启动线程,让创建好的线程执行指定的任务。
注意,main()是一个线程,此时执行程序可以看见,频幕上出现了 "hello MyThread!“和"hello main!” 的交替打印,说明 t.start()启动了另外一个执行流,这个新的执行流(新的线程)执行输出 “hello MyThread!”,此时 idea是一个进程,运行的程序属于idea里的java进程,这两个线程是属于java进程的两个线程。并且这两个线程是同时进行的。
为什么说这两个线程是同时进行的呢?仔细看可以发现,这时两个线程中,都分别存在一个死循环,如果这两个线程不是同时进行的话,那么在频幕上不可能出现两个语句交替打印,而是死循环打印其中的一个语句。
2.自定义 MyRnunable 实现 Runnable 接口
- 自定义一个 MyRunnable 的子类,实现Runnable 接口,并重写Runnable的 run( )方法。
- 创建 MyRunnable 的对象
- 创建 Thread 的对象,并将 MyRunnable 的对象作为参数。
- 启动线程
class MyRunnable implements Runnable {
@Override
public void run() {
while (true) {
System.out.println("MyRunnable!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class RunnableDemo {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
while (true) {
System.out.println("main!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
3.继承 Thread ,使用匿名内部类创建线程
public class ThreadDemo2 {
public static void main(String[] args) {
Thread thread = new Thread() {
@Override
public void run() {
while (true) {
System.out.println("匿名内部类");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
thread.start();
while (true) {
System.out.println("main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
4.实现 Runnable 接口,使用匿名内部类创建线程
public class RunnableDemo2 {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("MyRunnable!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
thread.start();
while (true) {
System.out.println("main!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
5.使用 Callable 接口创建线程
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class CallableDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建任务
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 1000; i++) {
sum += i;
}
return sum;
}
};
//Thread 不能直接传 callable,需要再包装一层
FutureTask<Integer> futureTask = new FutureTask<>(callable);
//创建线程,让线程来完成这个任务
Thread t = new Thread(futureTask);
t.start();
System.out.println(futureTask.get());
}
}
6.使用 lambda 表达式创建线程
public class LambdaDemo {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
while (true) {
System.out.println("MyRunnable!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
while (true) {
System.out.println("main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
注意事项:
lambda 表达式的使用,需要遵守变量捕获规则。捕获的变量必须是 final 修饰的或者“实际 final(变量没有使用 final 修饰,但是在代码执行的过程中,没有对变量就行修改)”
三、Thread 类及常见的方法
1.Thread 类的构造方法
方法 | 说明 |
---|---|
Thread() | 创建线程对象 |
Thread ( Runnable target ) | 使用 Runnale 对象创建线程对象 |
Thread ( String name) | 创建线程对象,并命名 |
Thread(Runnable target, String name) | 使用 Runnable 对象创建线程对象,并命名 |
Thread(ThreadGroup group,Runnable target) | 线程可以被用来分组管理,分好的组即为线程组,这个目前我们了解即可 |
2.Thread 类的常见属性
属性 | 获取方法 |
---|---|
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否是后台线程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
- ID 是线程的唯一标识,不同的线程不会重复
- 名称是为了方便调试的时候观察线程用到的
- 状态标识当前线程所处的一个情况
- 优先级高的线程理论上来说更容易被调度到
- 后台进程,JVM 会在一个进程的所有非后台进程结束后,才会结束运行。换言之,前台线程,就是指会影响 JVM 进程结束的线程,前台线程不结束,JVM 的进程就不会结束。
- 是否存活,标识当前线程的 run() 是否运行结束
四、线程中断
通过特定的方式,让一个正在执行的线程停止运行。可以通过设定结束标志位,判断是否需要中断线程。
例如,当你在和你的朋友正在聊天的时候,正巧这个时候你的女朋友发来视频邀请,这个时候你不得不终止和你朋友的交流,然后接通你女朋友的视频通话。
1.自定义结束标志位
设定一个结束标志位 isQuit = false,使用 isQuit 作为条件循环的判定条件。
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.start();
Thread.sleep(3000);
isQuit = true;
}
}
上述代码的预期效果是这样的,我们设立了一个结束标志位 isQuit,在主线程 main 中创建一个线程 t 并通过start 启动线程,主线程 main 在创建线程之后睡眠 3 秒钟,在这 3 秒钟内,每隔 1 秒钟,t 线程在控制台上输出一句 “hello t”,3 秒钟后主线程 main 睡眠结束,通过修改 isQuit = true,t 线程再进行循环条件判定的时候为 false,此时不进入循环,线程 t 终止,并在控制台上输出 “线程终止”。
执行结果如下:
如果将isQuit设置在 main() 方法内,会有结果会怎么样呢?
此时我们会发现,程序报错。为什么会出现这样的结果呢?
- 在上面的 lambda 表达式我们说过,lambda 表达式对变量捕获的要求是,变量必须是 final 修饰的或者是“实际 final(没用 final 修饰,但是在变量的整个生命周期中,变量不能做任何的修改)”,因此在使用自定义的结束标志位的时候,标志位的声明一定得满足变量捕获的规则,因此我们通常是将结束标志位设置为成员变量。
2.Thread 提供的结束标志位, isInterrupted( )
通过调用 Thread 类提供的结束标志位,判定线程是否需要终止。
public class ThreadDemo {
public static void main(String[] args) {
Thread t = new 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();
}
t.interrupt();
}
}
- Thread.currentThread()的作用,获取当前线程实例对象
- isInterrupted( ),Thread对象自带的结束标志位, 有 true和 false 两种状态
- interrupt( ),将线程 t 内部的结束标志位改成 true ,默认是 false
上述代码的预期是通过主线程 main 创建一个新的线程 t,创建线程后主线程睡眠 3 秒,线程 t 不受影响继续,每隔 1 秒钟在 控制台输出一句 “ hello t!”,3 秒钟后主线程睡眠结束,将结束标志位修改为 true ,使线程 t 在就行循环判定的结果为 false,终止线程。
结果展示:
执行代码后,我们发现执行结果和预期效果大相径庭,我们发现,经过 3 秒后,main 线程将结束标志位修改为 true 后,t 线程并没有结束,还是抛出异常后继续执行,为什么会出现这样的情况?
首先我们必须了解 sleep() 和 interrupt() 的工作原理:
interrupt()的作用:
- 将结束标志位设置为 true
- 如果当前线程正在阻塞中(比如正在执行 sleep),此时就会把阻塞状态唤醒,通过抛出异常的方式让 sleep 立即结束。
sleep的工作原理:
- 当代码正常执行到 sleep 时,线程会进行睡眠,当 sleep 的过程中,如果结束标志位被修改为 true,sleep 会被通过抛出异常的方式强制唤醒,唤醒后,sleep 会把结束标志位 isInterrupted() 清空(true -> false),这就导致线程 t 在进行循环判定的结果仍为真 ,线程 t 会继续执行。
那么有没有解决的办法,通过修改上述的代码逻辑,达到预期的效果呢?
显然强制将 sleep 唤醒后,会抛出异常,再进行循环条件的判定,我们只需要在抛出异常后,手动添加 break ,循环就可以结束,就可以实现线程 t 的终止了。
运行结果展示:
- 如果主线程执行 interrupt() 后,标志位由 false 改成了 true ,此时线程 t 正好执行打印语句,那么 sleep 还会将 结束标志为 interrupted 改成 false 么?
执行 interrupt() 后, 虽然此时线程 t 正在执行打印语句,但是当打印语句执行结束后,就会执行到 sleep ,线程 t 进入睡眠,但是因为结束标志位已经修改为 true,线程会立即将 sleep 唤醒,并抛出异常,然后将结束标志位 interrupted() 清空(true -> false),如果此时的代码在抛出异常语句后有 break 语句,那么线程 t 就会终止,如果没有,那么线程 t 继续执行。也就是说:
- 当结束标志位 位 true 的时候,不论是 sleep 准备执行,或者是已经执行到了一半的情况,都会触发两件事,一是立即抛出异常;而是清空结束标志位(true -> false)。此时如果需要结束线程,需要程序员自己收到到 catch 中添加 break 语句结束线程。
- 为什么 sleep 结束后要清空结束标志位?
目的就是为了让线程自身能够对于线程何时结束,有一个更明确的控制。
五、等待一个线程
因为线程的执行是并发的,线程一旦启动之后,我们是不能确定哪个线程会先执行结束的。在多线程环境下实现业务逻辑,有时候我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作。例如,张三看上了一双鞋子,但是,临近月底了,钱包空了,工资又没发。如果张三想要买这双鞋子,只能等到月底,确定老板李四发了工资之后,才能有足够的钱购买他喜欢的鞋子。
我们先看一下代码:
public class JoinDemo {
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("hello t!");
});
t.start();
System.out.println("hello main!");
}
}
结果展示:
我们可以观察到,此时 main 线程先于 t 线程执行结束。如果我们需要让 t 线程先结束,main 线程再结束,此时我们就可以用 join( ),让main 线程等待线程 t 执行结束后再执行。
虽然上述的代码的执行结果是先输出的 “hello main!”,再输出的 “hello t!”,但是实际上这两个代码谁先执行结束,程序员是无法确定的。大部分时候都是先输出 “hello main!” ,因为线程的创建是需要消耗资源的,但是不排除特定的情况下,主线程 “hello mian!” 没有立即执行到。作为开发人员,这样的不确定性,不是我们想要的结果,因此我们有时候需要明确规定线程的结束顺序,此时就可以使用线程等待来实现。
public class JoinDemo {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
System.out.println("hello t!");
});
t.start();
t.join();
System.out.println("hello main!");
}
}
结果展示:
join() 的使用方法:
在需要等待的线程中调用 join(),join() 的调用对象是被等待的线程。例如:在上述的例子中,需要 main 线程等待 t 线程线程先结束,因此需要在 main 线程中调用 join() ,被等待的对象是 t 线程,因此join() 的调用对象就是 t ,因此只需要在 main 线程中,t.start() 的代码后面添加代码 t.join()。
上述的代码实现有两种情况:
- 第一种情况,main 线程调用 t.join( ) 的时候,如果 t 线程还在运行,此时 main 线程阻塞,直到 t 线程执行结束(t 线程的 run() 方法执行结束),main 线程才从阻塞状态解除,继续往下执行。
- 第二种情况,main 线程调用 t.join() 的时候,t 线程已经执行结束了,此时 join()方法就不会造成main 线程阻塞,main 线程会继续往下执行。
join( )有两个版本:
- 无参的 join( )方法,效果是 “死等”
- 有参的 join(超时时间),效果是:如果阻塞的时间超过了超时时间,线程会继续往下执行
六、线程的状态
状态 | 含义 |
---|---|
NEW | 系统中的线程还没有创建出来,只是有个 Thread 的对象 |
TERMINATED | 系统中的线程已经执行完了,但是Thread 的对象还在 |
RUNNABLE | 就绪状态,正在 CPU 上执行,或者准备好随时可以去 CPU 上执行 |
TIMED_WAITING | 指定时间等待,调用 sleep() |
BLOCKED | 表示等待锁出现的状态 |
WAITING | 等待 wait() 出现的状态 |
进程状态的转换:
1.NEW 状态
public class JoinDemo {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
System.out.println("hello t!");
});
System.out.println(t.getState());
t.start();
t.join();
System.out.println("hello main!");
}
}
结果展示:
2.TERMINATED 状态
public class JoinDemo {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
System.out.println("hello t!");
});
t.start();
t.join();
System.out.println(t.getState());
System.out.println("hello main!");
}
}
结果展示:
t 线程执行结束后,main 线程还没有结束,此时的 Thread 的对象还没有被回收,因此Thread 的对象 t 还在,但是 t 线程已经执行结束,因此此时的状态就是 TERMINATED。
3. RUNNABLE 状态
public class JoinDemo {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
System.out.println("hello t!");
});
t.start();
System.out.println(t.getState());
t.join();
System.out.println("hello main!");
}
}
结果展示:
此时的线程 t 刚启动,还处在运行的阶段,因此此时的状态为 RUNNABLE。
4. TIMED_WAITING 状态
package com.demo;
public class JoinDemo {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
try {
System.out.println("hello t!");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t.start();
Thread.sleep(1000);
System.out.println(t.getState());
t.join();
System.out.println("hello main!");
}
}
结果展示:
提示:这里对文章进行总结:
例如:以上就是今天要讲的内容,本文仅仅简单介绍了pandas的使用,而pandas提供了大量能使我们快速便捷地处理数据的函数和方法