Java多线程
- 一级目录
- Java多线程
-
- 1.多线程的引入
-
- [1.1 引入的原因](#1.1 引入的原因)
- [1.2 线程的概念](#1.2 线程的概念)
- [1.3 线程与进程之间的区别](#1.3 线程与进程之间的区别)
- 2.创建线程的5中方法
-
- [2.1 方法一:继承Thread,重写run](#2.1 方法一:继承Thread,重写run)
- [2.2 方法二:实现Runnable,重写run](#2.2 方法二:实现Runnable,重写run)
- [2.3 方法三:方法一+匿名内部类](#2.3 方法三:方法一+匿名内部类)
- [2.4 实现Runnable,重写run+匿名内部类](#2.4 实现Runnable,重写run+匿名内部类)
- [2.5 针对方法3,4引入lambda表达式](#2.5 针对方法3,4引入lambda表达式)
- [3. Thread类以及常见方法](#3. Thread类以及常见方法)
-
- [3.1 Thread类的概念](#3.1 Thread类的概念)
- [3.2 Thread常见的构造方法](#3.2 Thread常见的构造方法)
- [3.3 Thread类中常见的几个属性](#3.3 Thread类中常见的几个属性)
-
- [3.3.1. ID:](#3.3.1. ID:)
- [3.3.2. isDaemon()](#3.3.2. isDaemon())
- [3.3.3. isAlive():用来判断线程是否存活](#3.3.3. isAlive():用来判断线程是否存活)
- [3.3.4. isInterrupted()判断线程是否被中断](#3.3.4. isInterrupted()判断线程是否被中断)
- [4. 线程的启动,中断,等待](#4. 线程的启动,中断,等待)
-
- [4.1 线程的启动](#4.1 线程的启动)
- [4.2 线程的中断](#4.2 线程的中断)
- [4.3 线程的等待](#4.3 线程的等待)
- [4.4 线程的休眠](#4.4 线程的休眠)
一级目录
二级目录
三级目录
Java多线程
1.多线程的引入
1.1 引入的原因
- 单核 CPU 的发展遇到了瓶颈. 要想提高算力, 就需要多核 CPU,与多核cpu相配套的是并发编程,来提高cpu的利用率
- 虽然多进程也能实现 并发编程, 但是线程比进程更轻量 .
• 创建线程比创建进程更快.
• 销毁线程比销毁进程更快.
• 调度线程比调度进程更快. - 线程虽然比进程轻量, 但是人们还不满足, 于是又有了 "线程池" 和 "协程"
1.2 线程的概念
一个线程就是一个执行流 . 每个进程都包含一个或者多个线程。每个线程之间都可以按照顺序执行自己的代码. 多个线程之间同时执行着多份代码.
1.3 线程与进程之间的区别
既然线程和进程都可以完成并发编程,那么,他们之间有什么区别吗?
- 进程包含线程,一个进程包含一个或者多个线程
- 进程是系统资源分配的基本(最小)单位,而线程是cpu调度的基本(最小)单位
a.对于前半句的解释:进程和进程之间所涉及到的资源是各自独立的(cpu,内存,硬盘资源,网络带宽等等),这些进程的创建,销毁都需要申请资源,线程不需要吗?
对于线程,只是第一个线程创建的时候需要申请资源,后续在创建线程不涉及资源申请操作,而且只有所有线程都销毁了才能真正释放资源。
b.对于后半句的解释:若一个进程包含多个线程,此时,多个线程之间是各自去cpu上调度执行的,比如有线程1,2,3 很有可能线程1去cpu核心1上面去执行,2去核心2执行,3去核心3去执行,也就是并行执行,在这期间伴随着这几个线程在cpu核心上的来回切换(具体实现由操作系统中的调度器 完成,程序员无法干预)


- 一个进程挂了一般不会影响到其他进程. 但是一个线程挂了, 可能把同进程内的其他线程一起带走(整
个进程崩溃). - 进程和进程 之间不共享 内存空间. 同一个进程的线程 之间共享 同一个内存空间.

2.创建线程的5中方法
2.1 方法一:继承Thread,重写run
java
class MyThread extends Thread{
@Override
public void run(){
while(true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class Demo1 {
public static void main(String[] args) throws InterruptedException {
Thread t = new MyThread();
t.start();
while(true){
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
运行结果:

我们借助第三方调试工具,可以看到:

注意:
-
sleep是静态方法,目的是让线程暂时放弃cpu,休息一会儿,过了指定时间再去执行
-
在执行结果中,hello main与hello thread有先有后,因为:多个线程调度顺序是随机的(操作系统剥夺式调度线程),这两个线程谁先谁后执行都有可能,无法预测。
-
t.start()与t.run()的区别:
a. t.start()是在系统中真正创建出来一个线程
b. t.run()这个操作没有创建线程,只是调用了刚才重写MyThread类中的run方法,整个系统中,只有main线程
-
MyThread类中,重写的run方法:是线程的入口方法,新的线程启动了,就自动执行这里面的代码,run不需要我们手动调用,即新的线程创建好了之后,自动去执行。
javaclass MyThread extends Thread{ @Override public void run(){ while(true){ System.out.println("hello thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } } } public class Demo1 { public static void main(String[] args) throws InterruptedException { Thread t = new MyThread(); // t.start(); t.run(); while(true){ System.out.println("hello main"); Thread.sleep(1000); } } }如上的代码所述,只是把t.start()替换为t.run()

借助线程调试工具,前台进程中只能看到main进程

在这里面没有看到我们之前的线程t,说明t.run()方法不能创建线程
,t.run()只能调用重写的run()函数,因此我们就看到了在控制台中仅仅打印hello thread,而且在调试工具中没有看懂线程Thread-0(我们刚才创建的线程t)的结果啦!
2.2 方法二:实现Runnable,重写run
java
class MyRunnable implements Runnable{
@Override
public void run() {
while(true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class Demo2 {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new MyRunnable();
Thread t = new Thread(runnable);
t.start();
while(true){
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
注意:
-
我们用个比喻来说明将runnable对象传参给Thread的构造方法的这一过程
→ 1.你写好了工作清单(MyRunnable重写run())
→ 2. 把清单打印出来(new MyRunnable()创建任务对象)
→ 3. 把清单交给工人(new Thread(runnable))
→ 4. 喊工人开始干活(t.start())
→ 5. 工人照着清单做事(执行run()里的循环打印)
Runnable是任务载体,重写run()方法定义线程要执行的逻辑;
new Thread(runnable)的核心是给线程绑定执行任务 ,让线程知道启动后该做什么;
线程调用start()后 ,才会真正执行 Runnable中run()方法的代码,而非直接调用run()(直接调用 run () 只是普通方法执行,不会创建新线程) -
为什么要把任务和线程分离?
Java 这么设计,是为了解耦 ------ 这样的话,我们可以:
同一个Runnable任务,传给多个Thread对象,让多个线程执行相同的任务;
java// 示例:多个线程执行同一个任务 Runnable task = new MyRunnable(); Thread t1 = new Thread(task); // 工人1执行这个任务 Thread t2 = new Thread(task); // 工人2执行同一个任务 t1.start(); t2.start(); // 两个线程都会打印"hello thread"灵活替换任务 :比如后续想改线程执行的逻辑,只需要改Runnable的实现,不用动Thread的代码;
-
异常分为受查异常和非受查异常,而InterruptedException属于受查异常,即编译时必须处理 (要么 catch 捕获,要么 throws 声明抛出)
抛出的异常必须满足:子类重写方法抛出的受查异常 ≤ 父类方法声明的受查异常 (要么不抛,要么抛子类异常,不能抛父类没有声明的新受查异常)。
这里的关键是:InterruptedException 是受查异常(编译时必须处理,要么 catch 要么 throws),而 RuntimeException 是非受查异常(编译时不用处理)。

这里父类Runnablemei没有抛出受查异常,子类MyRunnable也不能抛出父类没有声明的新受查异常
这里的解决方法就是:
我们必须要在 run() 方法内部 catch 住 InterruptedException,然后把它包装成 RuntimeException 抛出
(这是一个非受查异常 ,父类没声明也能抛)
2.3 方法三:方法一+匿名内部类
java
public class Demo3 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(){
@Override
public void run(){
while(true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
t.start();
while(true){
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
运行结果为:

注意:
- Thread t = new Thread(){};做了三件事情
- 创建了一个Thread的子类,子类是匿名的
- {}里面就可以编写子类的定义代码。子类里面的方法,属性,重写的方法。。。
- 创建了这个匿名内部类的实例,并且把实例的引用赋值给t.
- 使用匿名内部类的好处?
这样可以少定义一些类了,如果某些代码是一次性的,就可以使用匿名内部类
2.4 实现Runnable,重写run+匿名内部类
java
public class Demo4 {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
while(true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
Thread t = new Thread(runnable);
t.start();
while(true){
try {
System.out.println("hello main");
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
运行结果:

2.5 针对方法3,4引入lambda表达式
java
public class Demo5 {
public static void main(String[] args) {
//lamda表达式
Thread t = new Thread(()->{
while(true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
while(true){
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
运行结果:

lambda表达式:本质就是一个匿名函数 ,最主要的用途是作为回调函数,格式为()->{},这样,创建了匿名的函数式接口的子类 ,并且创建出对应的实例,并且重写了里面的方法
3. Thread类以及常见方法
3.1 Thread类的概念
Thread 类是 JVM 用来管理线程的一个类,换句话说,每个线程都有一个唯一的 Thread 对象与之关联
3.2 Thread常见的构造方法
| 方法 | 说明 |
|---|---|
| Thread() | 创建线程对象 |
| Thread(Runnable target) | 使用 Runnable 对象创建线程对象 |
| Thread(String name) | 创建线程对象,并命名 |
| Thread(Runnable target, String name) | 使用 Runnable 对象创建线程对象,并命名 |
| 【了解】Thread(ThreadGroup group, Runnable target) | 线程可以被用来分组管理,分好的组即为线程组,这个目前我们了解即可 |
java
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");
3.3 Thread类中常见的几个属性

这几个属性的使用方法
java
public static void main(String[] args) throws InterruptedException {
Thread myThread = new Thread(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
System.out.println("线程被中断:" + e.getMessage());
}
System.out.println("线程执行完毕");
}, "我的测试线程"); // 给线程命名
// 2. 设置线程为后台线程(可选,演示isDaemon())
myThread.setDaemon(true);
// 设置线程优先级(演示getPriority())
myThread.setPriority(Thread.MAX_PRIORITY);
// 3. 启动线程
myThread.start();
// 4. 主线程休眠一下,确保myThread进入运行状态
Thread.sleep(500);
// ========== 演示六个属性的获取方法 ==========
System.out.println("1. 线程ID: " + myThread.getId());
System.out.println("2. 线程名称: " + myThread.getName());
System.out.println("3. 线程状态: " + myThread.getState());
System.out.println("4. 线程优先级: " + myThread.getPriority());
System.out.println("5. 是否后台线程: " + myThread.isDaemon());
System.out.println("6. 是否存活: " + myThread.isAlive());
System.out.println("7. 是否被中断: " + myThread.isInterrupted());
// 演示中断线程,观察isInterrupted()的变化
myThread.interrupt();
System.out.println("中断线程后,是否被中断: " + myThread.isInterrupted());
// 等待myThread执行结束
myThread.join();
System.out.println("线程结束后,是否存活: " + myThread.isAlive());
System.out.println("线程结束后,状态: " + myThread.getState());
}
运行结果:

上述的注意事项:
3.3.1. ID:
是java中给每个运行的线程分配id标识线程身份的效果,类似与PID
3.3.2. isDaemon()
这是判断是否为守护线程(后台线程)
为了理解这个概念,我们先看一下如下的代码
java
public static void main(String[] args) {
Thread t1 = new Thread(()->{
while(true){
System.out.println("hello 1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"t1");
t1.start();
Thread t2 = new Thread(()->{
while(true){
System.out.println("hello 2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"t2");
t2.start();
Thread t3 = new Thread(()->{
while(true){
System.out.println("hello 3");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"t3");
t3.start();
while(true){
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
运行结果:

在这个代码中虽然main线程结束了,但是线程t1,t2,t3仍然存在,所以进程仍然存在,用下面的调试工具也可以验证上述结果

像t1,t2,t3这样的线程的存在,就能够影响到进程继续存在 ,这样的线程,就称为前台线程 ,上图所示中,除了t1,t2,t3之外,他们的存在不影响线程结束 ,这样的线程称为后台线程
|--------------------------------------------------------|
| 总结一下:咱们自己代码创建的线程,包括main主线程默认都是前台线程,可以通过setDaemon方法来修改! |
3.3.3. isAlive():用来判断线程是否存活
我们先来看一下下面的代码
java
public static void main(String[] args) {
Thread t = new Thread(()->{
for (int i = 0; i < 3; i++) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
while(true){
System.out.println(t.isAlive());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
运行结果:

-
可能有的同学会有疑问,为什么有关主线程的for循环了三次,但是true被打印了四次--> 第4次打印和t的结束不一定是谁先谁后
-
Thread对象的生命周期,和系统中的线程生命周期,是不同的(可能存在,Thread对象还存活,但是系统中的线程已经销毁的情况)-->如图下所示:main线程已经结束,但是t线程仍然存活
java
public static void main(String[] args) {
Thread t = new Thread(()->{
while(true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
//设置t为后台线程
t.setDaemon(true);
t.start();
for (int i = 0; i < 3; i++) {
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("main 结束");
System.out.println("t线程是否存活"+t.isAlive());
}

3.3.4. isInterrupted()判断线程是否被中断
我们先来看第一段代码
java
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
//当当前的线程没有被打断时
while(!Thread.currentThread().isInterrupted()){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
Thread.sleep(3000);
t.interrupt();
}
如果我们运行上述代码,会有如下报错:

它的原因是:每次执行循环的时候,绝大部分时间都是在sleep,当主线程调用Interrupt极大的概率下,此时t线程都是在sleep中,这个Interrupt操作能够唤醒sleep, sleep会抛出异常InterruptedException
接着看第二段代码:
java
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
//当当前的线程没有被打断时
while(!Thread.currentThread().isInterrupted()){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
});
t.start();
Thread.sleep(3000);
t.interrupt();
}
运行结果如下:

大家可能对这个结果感到奇怪:执行完t.interrupt();之后,while循环的条件!Thread.currentThread().isInterrupted()应该是false,因此会退出while循环。
其实,是sleep在搞鬼。由于上述代码,是吧sleep给唤醒了,在这种唤醒的前提下,sleep就会在唤醒之后,把isInterruptted标志位给设置会false,因此while循环的条件为true,循环会正常执行。
这样设置的目的:为了让程序员在catch语句中有更多的选择空间。程序员可以自行决定,这个程序是否结束,还是等会结束,还是不结束(忽略这个终止型号)
因此,针对上述问题,共有三种解决方法
java
catch (InterruptedException e) {//sleep操作没按照约定正常执行
throw new RuntimeException(e);
//1.加上break就是立即终止
//break;
//2.什么都不写,就是不终止
//3.catch中执行一些其他逻辑再break,就是稍后终止
}
java
public static void main(String[] args) {
boolean isFinished = false;
Thread t = new Thread(()->{
while(!isFinished){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("thread结束");
});
t.start();
try {
Thread.sleep(3000);
isFinished = true;
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
上述代码有如下编译报错:

出现这个问题的原因:
lambda表达式其实是回调函数 ,它的执行时机,是在很久之后(操作系统真正创建出线程之后,才会执行的)很有可能,后续线程创建好了,当前的main这里的方法都执行完了,对应的isFinished就销毁了。。。
这个问题的解决方法:
Java的方法是,把捕获的变量给拷贝一份到lambda表达式里面来,外面的变量是否销毁,就不影响lambda里面的执行了,因此,拷贝,就意味着这样的变量就不适合进行修改了。我们就要对于这个isFinished变量进行修改!
java
public class Demo10 {
private static boolean isFinished = false;
public static void main(String[] args) {
// boolean isFinished = false;
Thread t = new Thread(()->{
while(!isFinished){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("thread结束");
});
t.start();
try {
Thread.sleep(3000);
isFinished = true;
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
运行结果:

这样为何就行了?
本质是把isFinished修改成成员变量,此时不再是变量捕获的语法了,而是切换到"内部类访问外部类成员"的语法 。lambda本质上是函数式接口,相当于一个内部类,isFinished变量本身就是外部类(Demo10)的成员。内部类本来就可以访问外部类的成员。成员变量的生命周期是让GC(garbage collection 垃圾回收)来管理的。在lambda表达式里面不担心变量生命周期失效的问题,也就不必拷贝。
4. 线程的启动,中断,等待
4.1 线程的启动
之前我们已经看到了如何通过覆写 run 方法创建一个线程对象,但线程对象被创建出来并不意味着线程就开始运行了。
• 覆写 run 方法是提供给线程要做的事情的指令清单(要执行的任务 )
• 线程对象可以认为是把 李四、王五叫过来了
• 而调用 start() 方法,就是喊一声:"行动起来!",线程才真正独立去执行了
4.2 线程的中断
我们在3.3.4. isInterrupted()判断线程是否被中断这一节当中提到过相同的内容
4.3 线程的等待
等待一个线程 - join()
有时,我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作。例如,张三只有等李四转账成功,才决定是否存钱,这时我们需要一个方法明确等待线程的结束。
由于多个线程之间是并发执行,随即调度的。然而,我们程序员不期望随机,使用join可以实现控制多个线程之间结束的先后顺序
java
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
for (int i = 0; i < 4; i++) {
System.out.println("thread t");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
System.out.println("main线程结束");
}
运行结果如下:

如果我们想要控制上述t线程和main线程的执行顺序,需要使用join,比如我们让main等待t线程执行完,再开始执行main线程
java
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
for (int i = 0; i < 4; i++) {
System.out.println("thread t");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
t.join();
System.out.println("main线程结束");
}
运行结果如下,这就是我们期望的结果main结束在所有的t之后

注意:
- t.join()的效果,在main函数里面,让main线程等待t先结束
java
t.join(3000);
join中可以添加参数,这样的意思,如果t线程运行时间在3000ms之内结束,此时,就会立即执行join之后的代码,如果t线程运行的时间超过3000,t还没有结束,此时join也继续往下走,就不用等了
4.4 线程的休眠
对于sleep的理解

| 方法 | 说明 |
|---|---|
| public static void sleep(long millis) throws InterruptedException | 休眠当前线程 millis 毫秒 |
| public static void sleep(long millis, int nanos) throws InterruptedException | 可以更高精度的休眠 |