1. 认识线程(Thread)
1.1 概念
一个线程就是一个 "执行流". 每个线程之间都可以按照顺讯执行自己的代码. 多个线程之间 "同时" 执行着多份代码
1.2 引入线程的原因
引入多个进程,初心?
实现并发编程 => 多核 CPU 的时代=>可以同时执行多个任务
进程可以满足并发编程,但是效率很低,这时候我们引入了线程。
【进程太重量,效率不高.
创建一个进程,消耗时间比较多
【消耗在申请资源上的.进程是资源分配的基本单位.
分配内存操作,就是一个大活~
操作系统内部有一定的数据结构,把空闲的内存分块管理好,当我们去进行申请内存的时候,系统就会从这样的数据结构中找到一个大小合适的空闲内存,返回给对应的进程
这里虽然通过此处的数据结构,可以一定程度提高效率,整体来说,管理的空间比较多,相比之下还是一个耗时操作.】
销毁一个进程,消耗时间也比较多
调度一个进程消耗时间也比较多】
如果需要频繁的创建/销毁进程,这个时候,开销就不能忽视
为了解决上述问题,就引入了"线程"(Thread)
线程也叫做"轻量级进程"
创建线程,比创建进程,更快;
销毁线程,比销毁进程, 更快,
调度线程,比调度进程, 更快
【线程不能独立存在,而是要依附于进程,(进程包含线程)
进程可以包含一个线程,也可以包含多个线程(一个进程,最开始的时候,至少要有一个线程这个线程负责完成执行代码的工作.也可以根据需要, 创建出更多的线程,从而使当前实现"并发编程"的效果)】
【结构】一个进程,使用 PCB 表示,一个进程可能使用一个 PCB 表示,也可能使用多个 PCB 表示每个 PCB 对应到一个线程 上(状态,优先级,上下文,记账信息....每个线程都有这些信息,辅助调度)(除此之外, 前面谈到的 pid,是相同的.内存指针, 文件描述符表,也是共用同一份的)
上述结构,决定了,线程的特点:
1.每个线程都可以独立的去 CPU 上调度执行,
2.同一个进程的多个线程之间,共用同一份内存空间,和文件资源...
创建线程的时候,不需要重新申请资源了直接复用之前已经分配给进程的资源.省去了资源分配的开销,于是创建效率就更高了
进程是资源分配的基本单位线程是调度执行的基本单位
一个系统中,可以有很多进程
每个进程, 都有自己的资源.
一个进程中,可以有很多线程每个线程都能独立调度,共享内存/硬盘资源
总结:
1.首先, "并发编程" 成为 "刚需".
- 单核 CPU 的发展遇到了瓶颈. 要想提高算力, 就需要多核 CPU. 而并发编程能更充分利用多核 CPU 资源.
- 有些任务场景需要 "等待 IO", 为了让等待 IO 的时间能够去做一些其他的工作, 也需要用到并发编程.
2.其次, 虽然多进程也能实现 并发编程, 但是线程比进程更轻量.
- 创建线程比创建进程更快.
- 销毁线程比销毁进程更快.
- 调度线程比调度进程更快.
3.最后, 线程虽然比进程轻量, 但是人们还不满足, 于是又有了 "线程池"(ThreadPool) 和 "协程"
(Coroutine)
1.3 进程和线程的区别)(总结)
- 进程是包含线程的. 每个进程至少有一个线程存在,即主线程。进程同时可以有多个线程。
- 进程和线程, 都是用来实现 并发编程 场景的,但是线程比进程更轻量,更高效
- 同一个进程的线程之间,共用同一份的资源(内存+硬盘),省去了申请资源的开销
- 进程和进程之间,是具有独立性的,一个进程挂了,不会影响到别人线程和线程之间(前提是同一个进程内),是可能会相互影响的.(线程安全问题 + 线程出现异常)
- 进程是系统分配资源的最小单位,线程是系统调度的最小单位。
1.4 Java 的线程 和 操作系统线程 的关系
线程是操作系统中的概念.
操作系统内核实现了线程这样的机制, 并且对用户层提供了一些 API 供用户使用(例如 Linux 的 pthread 库).
Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装.
2.多线程程序
感受多线程程序和普通程序的区别:
每个线程都是一个独立的执行流
多个线程之间是 "并发" 执行的.
- java.lang下的类不用import
- run是线程的入口方法
每个线程都是一个独立的执行流~~
每个线程都可以执行一系列的逻辑(代码)
一个线程跑起来,从哪个代码开始执行?
就是从它的入口方法入口
一个 Java 程序的 入囗是 main 方法一样~~
【运行 Java 程序,就是跑起来一个 java 进程,这个进程里面至少会有一个线程,主线程,主线程的入口方法就是main 方法. 】
//sleep是Thread的静态方法
- 把上述代码改成 while (true)可以看到, 这两个 while 循环在"同时执行看到的结果,是两边的日志都在交替打印
- 每个线程,都是一个独立执行的逻辑.(独立的执行流)
- 兵分两路,并发执行(并行+并发)->达到并发编程的效果->更好的利用多核
- 这俩线程都是休眠 1000ms, 当时间到了之后,这俩线程谁先执行, 谁后执行,不一定!!
这个过程可以视为是"随机"的.
操作系统,对于多个线程的调度顺序,是不确定的,"随机"的.(此处的随机,不是数学上"概率均等"这种随机,取决于 操作系统 对于线程调度的模块 (调度器)具体实现)
把 t.start 改成 t.run此时,代码中不会创建出新的线程,只有一个 主线程.这个主线程里面只能依次执行循环执行完一个循环再执行另一个t.start兵分两路,一部分往下(main自动创建的线程,与别的线程相比没有什么特殊的,一个Java进程至少有一个main线程),一部分创建新的线程
多线程程序运行的时候,可以使用 IDEA 或者jconsole(jdk带有的程序) 来观察到该进程里的多线程情况找到jdk所在路径
- 在jconsole,可以看到一个 Java 进程即使是最简单的,里面也包含了很多的线程
- 只有Thread-0是Thread t = new MyThread();自己手动创建的,其他的线程都是 JVM 自动创建的
- 一个 Java 进程启动之后,NM 会在后面,默默的帮咱们做很多的事情(比如,垃圾回收,资源统计, 远程方法调用...)
- 线程的详细信息
- 未来写一些多线程程序的时候,就可以借助这个功能能看到该线程实时的运行情况比如,写的程序"卡死了
2.1 Thread 类的其他用法
创建线程,其他的写法
1.继承 Thread, 重写 run
2.实现 Runnable, 重写 run (Runnable是接口不是类)
- 实现 Runnable 接口
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("这里是线程运行的代码");
}
}
- 创建 Thread 类实例, 调用 Thread 的构造方法时将 Runnable 对象作为 target 参数.
Thread t = new Thread(new MyRunnable());
- 调用 start 方法
t.start(); // 线程开始运行
java
class MyRunnable implements Runnable {
@Override
public void run() {
while (true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Test01 {
public static void main(String[] args) {
Runnable runnable = new MyRunnable();
Thread t = new Thread(runnable);
t.start();
while (true) {
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Runnable 表示的是一个"可以运行的任务这个任务是交给线程负责执行,还是交给其他的实体来执行...... Runnable 本身并不关心~~
使用 Runnable 的写法, 和 直接继承 Thread 之间的区别, 主要就是解耦合
创建一个线程,需要进行两个关键操作:
1.明确线程要执行的任务任务本身, 不一定和线程概念强相关的这个任务只是单纯的执行一段代码,这个任务是使用单个线程执行,还是多个线程执行,还是通过其他的方式(信号处理函数/协程/线程池.....)都没啥区别~~(可以把任务本身给提取出来~此时就可以随时把代码改成使用其他方式来执行这个任务)
2. 调用系统 api 创建出线程
3.匿名内部类创建 Thread 子类对象
匿名内部类创建 Thread 子类对象
// 使用匿名类创建 Thread 子类对象
Thread t1 = new Thread() {
@Override
public void run() {
System.out.println("使用匿名类创建 Thread 子类对象");
}
};
4.匿名内部类创建 Runnable 子类对象
// 使用匿名类创建 Runnable 子类对象
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("使用匿名类创建 Runnable 子类对象");
}
});
5.lambda 表达式创建 Runnable 子类对象(lambda自身就是run方法,所以不用重写run)
// 使用 lambda 表达式创建 Runnable 子类对象
Thread t3 = new Thread(() -> System.out.println("使用匿名类创建 Thread 子类对象"));
Thread t4 = new Thread(() -> {
System.out.println("使用匿名类创建 Thread 子类对象");
});
lambda 表达式,本质上是一个匿名函数,主要用来实现回调函数"的效果Java 中不允许函数独立存在的(其他语言叫函数 function, Java 这里叫方法 method)
lambda 本质函数式接口.(本质上还是没有脱离类)
java
public class Demo5 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (true) {
System.out.println("hello thread");
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 类及常见方法
Thread 类是 JVM 用来管理线程的一个类,换句话说,每个线程都有一个唯一的 Thread 对象与之关联。
2.1 Thread 的常见构造方法
|--------------------------------------|--------------------------|
| 方法 | 说明 |
| Thread() | 创建线程对象 |
| Thread(Runnable target) | 使用 Runnable 对象创建线程对象 |
| Thread(String name) | 创建线程对象,并命名 |
| Thread(Runnable target, String name) | 使用 Runnable 对象创建线程对象,并命名 |
创建线程的时候,可以去指定 namename 不影响线程的执行,
只是给线程起个名字后续在调试的时候,比较方便区分
Thread t1 = new Thread();Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");
2.2 Thread 的几个常见属性
|--------|-----------------|
| 属性 | 获取方法 |
| ID | getId() |
| 名称 | getName() |
| 状态 | getState() |
| 优先级 | getPriority() |
| 是否后台线程 | isDaemon() |
| 是否存活 | isAlive() |
| 是否被中断 | isInterrupted() |
- ID 是线程的唯一标识,不同线程不会重复
- 名称是各种调试工具用到
- 状态表示线程当前所处的一个情况
- 优先级高的线程理论上来说更容易被调度到
- 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。(后台线程是否结束,不影响整个进程的接收,前台线程会影响进程结束)(默认情况下一个线程是前台线程)(t.setDaemon(true)即就设置为后台线程了)
- 是否存活,即简单的理解,为 run 方法是否运行结束了(Thread 对象的生命周期,要比系统内核中的线程更长一些~~Thread 对象还在,内核中的线程已经销毁了这样的情况~~)
- 线程的中断问题
2.3 启动一个线程(start)
start 方法,start 方法内部,是会调用到系统 api,来在系统内核中创建出线程,
run 方法,就只是单纯的描述了该线程要执行啥内容.(会在 start 创建好线程之后自动被调用的)
二者之间的差别就是是否创建了新的线程
2.4 中断一个线程(interrupt)
常见的有以下两种方式:
1. 通过共享的标记来进行沟通
java
// 线程的打断
public class Demo8 {
private static boolean isQuit = false;
public static void main(String[] args) throws InterruptedException {
// boolean isQuit = false;
Thread t = new Thread(() -> {
while (!isQuit) {
// 此处的打印可以替换成任意的逻辑来表示线程的实际工作内容
System.out.println("线程工作中");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程工作完毕!");
});
t.start();
Thread.sleep(5000);
isQuit = true;
System.out.println("设置 isQuit 为 true");
}
}
当前咱们这个代码,是使用了一个 成员变量 isQuit,来作为标志位如果把 isQuit 改成 main 方法中的局部变量,是否可以呢??
不可以
lambda 表达式,有一个语法规则,变量捕获,lambda 表达式里面的代码,是可以自动捕获到上层作用域中涉及到的局部变量的~~
所谓的变量捕获, 其实就是让 lambda 表达式把当前作用域中的变量在 lambda 内部复制了一份!!(此时,外面是否销毁, 就无所谓了)
变量捕获
Java 中,变量捕获语法, 还有一个前提限制,就是必须只能,捕获一个 final 或者是实际上是 final 的变量
变量虽然没有使用 final,但是却没有修改内容就是"事实上的 final'。
上述方案,不够优雅.
1.需要手动创建变量.
2.当线程内部在 sleep 的时候, 主线程修改变量,新线程内部不能及时响应.
2. 调用 interrupt() 方法来通知
java
// 线程终止
public class Demo9 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
// Thread 类内部, 有一个现成的标志位, 可以用来判定当前的循环是否要结束.
while (!Thread.currentThread().isInterrupted()) {
System.out.println("线程工作中");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// 1. 假装没听见, 循环继续正常执行.
e.printStackTrace();
// 2. 加上一个 break, 表示让线程立即结束.
// break;
// 3. 做一些其他工作, 完成之后再结束.
// 其他工作的代码放到这里.
break;
}
}
});
t.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("让 t 线程终止. ");
t.interrupt();
}
}
使用 Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替自定义标志位.
Thread 内部包含了一个 boolean 类型的变量作为线程是否被中断的标记
|-------------------------------------|-------------------------------------|
| 方法 | 说明 |
| public void interrupt() | 中断对象关联的线程,如果线程正在阻塞,则以异常方式通知,否则设置标志位 |
| public static boolean interrupted() | 判断当前线程的中断标志位是否设置,调用后清除标志位 |
| public boolean isInterrupted() | 判断对象关联的线程的标志位是否设置,调用后不清除标志位 |
t.interrupt();//这个操作,就是把上述 Thread 对象内部的标志位设置为 true 了.
即使线程内部的逻辑出现阻塞(sleep)也是可以使用这个方法唤醒的~~
正常来说,sleep 会休眠到时间到, 才能唤醒,此处给出的 interrupt 就可以使 sleep 内部触发一个异常,从而提前被唤醒,(这是手动设置标志位无法实现的)
//但存在一个问题
异常确实是出现了.sleep 确实是唤醒了但是上述 t仍然在继续工作!! 并没有真的结束!!
interrupt 唤醒线程之后,此时sleep 方法抛出异常,同时会自动清除刚才设置的 标志位这样就使"设置标志位"这样的效果就好像没有生效一样~~【这么设定的原因】
Java 是期望, 当线程收到"要中断"这样的信号的时候,他能够自由决定,接下来怎么处理~~就可以让咱们有更多的"可操作性空间
可操作性空间的前提,是通过"异常"的方式唤醒的
如果没有 sleep,则没有上述操作空间,(此时没有异常,目的就非常明确,如果有 异常,就需要在出现异常之后,再确认一下)
2.5 线程等待join()
有时,我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作。
让一个线程,等待另一个线程执行结束,再继续执行.本质上就是控制线程结束的顺序
join 实现线程等待效果,
主线程中,调用 t.join()
1.此时就是主线程等待 t线程先结束
java
package thread;
public class Demo10 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("t 线程工作中!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
// 让主线程来等待 t 线程执行结束.
// 一旦调用 join, 主线程就会触发阻塞. 此时 t 线程就可以趁机完成后续的工作.
// 一直阻塞到 t 执行完毕了, join 才会解除阻塞, 才能继续执行
System.out.println("join 等待开始");
t.join();
System.out.println("join 等待结束");
}
}
t.join 工作过程:
1)如果t线程正在运行中,此时调用 join 的线程就会阻塞,一直阻塞到t线程执行结束为止2)如果t线程已经执行结束了,此时调用 join 线程, 就直接返回了.不会涉及到阻塞~~
3)可以设置超时时间(一般来说不建议死等)
|------------------------------------------|----------------------|
| 方法 | 说明 |
| public void join() | 等待线程结束 |
| public void join(long millis) | 等待线程结束,最多等 millis 毫秒 |
| public void join(long millis, int nanos) | 同理,但可以更高精度 |
2.6 获取当前线程引用
java
public class ThreadDemo {
public static void main(String[] args) {
Thread thread = Thread.currentThread();
System.out.println(thread.getName());
}
}
2.7 休眠当前线程
也是我们比较熟悉一组方法,有一点要记得,因为线程的调度是不可控的,所以,这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的
|------------------------------------------------------------------------------|-----------------|
| 方法 | 说明 |
| public static void sleep(long millis) throws InterruptedException | 休眠当前线程 millis毫秒 |
| public static void sleep(long millis, int nanos) throws InterruptedException | 可以更高精度的休眠 |