Java多线程

时间:2023-02-10 20:54:36

​Java多线程 超详细!​

什么是线程?多线程?

线程是一个程序内部的一条执行路径,我们之前启动程序执行后,main方法的执行其实就是一条单独的执行路径。

多线程是指从软硬件上实现多条执行路径的技术。


线程

Thread类

Java是通过 java.lang.Thread 类来代表线程的,其提供了实现多线程的方式,其直接继承了Object类,并实现了Runnable接口。

Java多线程

构造器

Thread([String name]) ;

Thread(Runnable [,String name]);//若不取名则有默认名:Thread-0、Thread-1……

线程对象操作

String getName()  //获取线程名字

setName(String name) //设置线程名字

static Thread currentThread() //获取当前线程对象

线程状态操作

static sleep(long time);  //让当前线程休眠指定的时间后再继续执行,单位为毫秒。

run(); //线程的任务方法,在这个书写需要执行的代码逻辑

start(); //启动一个线程,启动成功后JVM会自动调用该线程的run方法(任务)

start()和run()的区别

  1. **run()**不会启动线程,只是普通的调用方法而已,不会分配新的分支栈。
  2. start()的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码任务完成之后,瞬间就结束了。启动成功的线程会自动调用run方法,并且run方法在分支栈的栈底部(压栈)。run方法在分支栈的栈底部,main方法在主栈的栈底部,run和main是平级的。

:speech_balloon:为什么启动线程不能直接调用run()方法,而要调用start()方法?

​start()​​方法包含了创建新线程的特殊代码逻辑,而​​run()​​方法是我们自己写的代码,很显然没有这个能力。

当你调用一个线程对象的​​run()​​方法时,你实际上只是在调用一个普通的函数,它将在你的主线程中顺序执行,而不是在新的线程中并行执行。

相反,如果你调用线程对象的​​start()​​方法,它会创建一个新的线程,并在新的线程中执行线程对象的​​run()​​方法。这样,你的程序中就有了两个并行执行的线程:主线程和新创建的线程。

线程生命周期

Java总共定义了6种状态,6种状态都定义在Thread类的内部枚举类中。(Thread.state

<img src="​​https://agust.oss-cn-guangzhou.aliyuncs.com/images/202302101946145.png​​" alt="img" style="zoom:67%;" />

状态

描述

NEW(新建)

线程刚被创建,但是并未启动。

Runnable(可运行)

线程已经调用了start()等待CPU调度

Blocked(锁阻塞)

线程在执行的时候未竞争到锁对象,则该线程进入Blocked状态

Waiting(无限等待)

一个线程进入Waiting状态,另一个线程调用notify或者notifyAll方法才能够唤醒

Timed Waiting(计时等待)

同waiting状态,有些方法带有超时参数(Thread.sleep、0bject.wait),调用他们将进入Timed Waiting状态。

Teminated(被终止)

因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

Java多线程

并发与并行

正在运行的程序就是一个独立的进程,线程是属于进程的,多个线程是并发运行的。

并发

  • CPU同时处理线程的数量有限,会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行。
  • 在一段时间内(很小)多个线程被调度,可以称这几个线程并发。

并行

  • 在同一个时刻上,同时有多个线程在被CPU处理并执行。

现在的java程序中至少有两个线程并发,一个是 垃圾回收线程,一个是 执行main方法的主线程

父线程和子线程是并发运行的,哪个先执行是未知的。

内存共享

进程和线程是什么关系?

进程:可以看做是现实生活当中的公司。

线程:可以看做是公司当中的某个员工。

线程A和线程B,堆内存方法区 内存共享,但是 栈内存 独立,一个线程一个栈。

假设启动10个线程,会有10个栈空间,每个栈和每个栈之间,互不干扰,各自执行各自的,这就是多线程并发。

守护线程 & 用户线程

java语言中线程分为两大类:用户线程 和 守护线程(后台线程)

其中具有代表性的就是:垃圾回收线程(守护线程)。

:speech_balloon:守护线程的特点:

一般守护线程是一个死循环,所有的用户线程只要结束,守护线程自动结束。

注意:主线程main方法是一个用户线程。

:speech_balloon:守护线程用在什么地方呢?

每天00:00的时候系统数据自动备份。这个需要使用到定时器,并且我们可以将定时器设置为守护线程。

一直在那里看着,每到00:00的时候就备份一次。所有的用户线程如果结束了,守护线程自动退出,没有必要进行数据备份了。

方法:

void setDaemon(boolean on)// on为true表示把线程设置为守护线程

boolean isDaemon()// 判断是否为守护线程

下面是示例:

public class Main {
public static void main(String[] args) {
Thread t = new DaemonThread();//DaemonThread——自定义的守护线程
t.setName("Back Data Thread");

// 启动线程之前,将线程设置为守护线程
t.setDaemon(true);
t.start();

// 主线程:主线程是用户线程
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "--->" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

//自定义的守护线程
class DaemonThread extends Thread {

@Override
public void run() {
int i = 0;
// 即使是死循环,但由于该线程是守护者,当用户线程结束,守护线程自动终止。
while (true) {
System.out.println(Thread.currentThread().getName() + "--->" + (++i));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

线程任务

Java中定义了一些接口用来定义线程中所需要执行的任务,如Runnable、Callable、FutureTask

Runnable接口

Runnable是函数式接口,只有一个任务方法​​void run()​(位于Java.lang)

Java多线程

Thread类实现了Runnable接口,实现Runnable接口的类对象可以创建线程,线程启动后就会运行 ​​run()​​ 。

Callable接口

(位于java.util.concurrent)

Callable也是一个函数式接口,只有一个​​V call()​​:用于计算结果,其中V为返回结果的类型;如果不能正常计算结果则抛出异常。

Java多线程

Callable接口类似于Runnable,但是 Runnable的​​run()​​不返回结果,也不能抛出检查异常。

Future接口

Java多线程

Future接口的作用就是为了调用其他线程完成好后的结果,再返回到当前线程中,如上图举例:小王自己是主线程,叫外卖等于使用Future接口,叫好外卖后小王就接着干自己的事去了,当外卖到了的时候,Future.get()获取,继续做接下来的事情;要注意的是当还没获取外卖的时候,主线程中用餐这一步是卡住的。

Java多线程

future接口定义的方法:

  • cancel():如果等太久,你可以直接取消这个任务
  • isCancelled():任务是不是已经取消了
  • isDone():任务是不是已经完成了
  • get():有2个get()方法,不带参数的表示无穷等待,或者你可以只等待给定时间

FutureTask类

FutureTask类实现了Runnable接口和Future接口的集合——RunnableFuture接口,Runnable为其提供了多线程的能力,Future为其提供了获取线程任务结果的能力。

FutureTask实现了Runnable接口,可以作为​​Thread(Runnable)​​的参数,以此实现多线程。

FutureTask类有一个成员变量 ​​private Callable<V> callable​​,可以通过构造器将Callable对象封装成FutureTask对象,通过​​get()​​获取Callable对象的任务结果(​​Call()​​)。

构造器

FutureTask(Runnable runnable, V result) 把Runnable对象封装成FutureTask对象

FutureTask<>(Callable call) 把Callable对象封装成FutureTask对象

获取计算结果的返回值

V get() throws Exception 获取线程执行call方法返回的结果

**FutureTask(Runnable runnable, V result)**的用法

Creates a ​​FutureTask​​ that will, upon running, execute the given ​​Runnable​​, and arrange that ​​get​​ will return the given result on successful completion.

这里构造函数说明中提示到:当Runnable执行完毕之后,可以用Future接口的get()获取执行结果,但是Runnable是没有返回结果,那这个result 有什么用呢?实际上这个result是由你设置好传进去的,FutureTask只是在Runnable执行完之后返回预先设置好的result,以便通知任务已完成。

public class Main {
public static void main(String[] args) {
MyThread myThread = new MyThread();
FutureTask<String> futureTask = new FutureTask<>(myThread, "123456");
// 1.构造线程并启动任务
new Thread(futureTask).start();
try {
// 2.获取预设好的线程任务结果
System.out.println("get():" + futureTask.get());
} catch (Exception e) {
e.printStackTrace();
}
}
}

// 自定义线程类
class MyThread implements Runnable {

// 任务方法
@Override
public void run() {
System.out.println("hello");
}
}

创建多线程

Java中创建多线程有以下几种方法:

继承Thread类

  1. 定义一个类继承Thread类,重写​​run()​
  2. 调用构造器执行​​start()​​运行线程

优点:编码简单

缺点:线程类已经继承了Thread类,无法继承其他类,不利于拓展

public class ThreadDemo1 {
public static void main(String[] args) {
for (int i = 0; i < 50; i++) {
new MyThread(i).start();
}
System.out.println("Dad Process");
// 父进程和子进程是并行运行的,先运行哪个是不确定的
}
}

class MyThread extends Thread {
int i;

public MyThread(int i) {
this.i = i;
}

@Override
public void run() {
System.out.println("Child Process:" + i);
}
}

实现Runnable接口

  1. 定义一个类实现Runnable接口并重写​​run()​
  2. 通过​​Thread(Runnable target)​​ 接收Runnable接口(任务对象)创建线程
  3. 再通过​​start()​​ 启动线程的任务。

优点:线程任务类只是实现了Runnale接口,可以继续类和实现接口。

缺点:如果线程有执行结果是不能直接返回的。

public class Main {
public static void main(String[] args) {

for (int i = 0; i < 50; i++) {
Runnable runnable = new RunnableImpl(i);
new Thread(runnable).start();
}
}
}

class RunnableImpl implements Runnable {
private int i;

public MyThread(int i) {
this.i = i;
}

@Override
public void run() {
System.out.println("child process output:" + i);
}
}

一个Runnable对象只能创建一个线程,多次创建的线程其实是同一对象。

public class Main {
public static void main(String[] args) {
Runnable runnable = new MyThread();
// 这里使用一个Runnable对象作为参数构造了多个Thread对象,打印出的对象地址是一致的
for (int i = 0; i < 10; ++i) {
new Thread(runnable).start();
}
}
}

class MyThread implements Runnable {
@Override
public void run() {
System.out.println(this);
}
}

实现Callable接口+FutureTask类

前2种线程创建方式都存在一个问题——重写的run方法均不能直接返回结果。JDK 5.0提供了Callable和FutureTask来解决这个问题:

  1. Callable其实就是可以返回结果的Runnable,其使用FutureTask将自己封装成线程对象。
  2. FutureTask实现了Runnable,可以被​​Thread(Runnable)​​调用。
  3. FutureTask的​​get()​​ 获取线程任务返回的值,解决了无法返回结果的问题 。

new Thread(new FutureTask(new CallableImpl()))

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class ThreadDemo3 {
public static void main(String[] args) {
//new Thread(new FutureTask(new CallableImpl()))
Callable<String> callable = new CallableImpl();
FutureTask<String> futureTask = new FutureTask<>(callable);
new Thread(futureTask).start();
try {
//通过FutureTask获取返回值
System.out.println("ans = " + futureTask.get());
} catch (Exception e) {
e.printStackTrace();
}
}
}

//这里的泛型类型也就是返回值类型
class CallableImpl implements Callable<String> {

@Override
public String call() {
int sum = 0;
for (int i = 0; i < 5; i++) {
sum += i;
}
return "" + sum;
}
}
  1. Callable是个泛型接口,其传入的参数类型和返回值类型是一致的
  2. 使用同一个Callable对象多次构造的FutureTask对象是同一个,Thread也同理

线程同步

多个线程同时操作同一个共享资源的时候可能会出现业务安全问题,称为线程安全问题。线程同步的出现就是为了解决线程安全问题。

如何才能保证线程安全呢? 让多个线程实现先后依次访问共享资源,这样就解决了安全问题。这种依次访问共享资源的方式我们称之为同步同步的核心就是加锁,把共享资源进行上锁,每次只能一个线程进入访问完毕以后解锁,然后其他线程才能进来。

同步与互斥:

  • 互斥是指某一资源同时只允许一个访问者对其进行访问,但无法控制对资源的访问顺序。
  • 同步是指在互斥的基础上实现对资源的有序访问。

Java中有三大变量:

  1. 实例变量:在中。
  2. 静态变量:在方法区
  3. 局部变量:在中。

局部变量永远都不会存在线程安全问题,因为局部变量不共享,一个线程一个栈。

**成员变量(实例+静态)**可能存在线程安全问题,堆和方法区都是多线程共享的。

在Java中同步可以有好几种实现方式,如 同步代码块同步方法lock锁

同步代码块

把出现线程安全问题的核心代码加上锁,每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行。

synchronized(同步锁对象){
操作共享资源的代码
}

同步锁对象相当于锁的名字,也可以使用字符串常量来充当,对不同的同步需要使用不同的锁名,不然会锁错

  1. 建议使用共享资源作为锁对象
  2. 对于实例方法,通常我们使用​​this​​作为同步锁对象,这样就不会造成使用不同同步使用同样的锁了
  3. 对于静态方法,建议使用​​类名.class​​作为所对象
//账户类
class Account {
private String cardId;
private int money;

public Account(String cardId, int money) {
this.cardId = cardId;
this.money = money;
}

//取钱方法(添加了同步锁)
public void withdrawMoney(int money) {
String name = Thread.currentThread().getName();
synchronized (this) {
if (this.money >= money) {
System.out.print(name + "取钱成功,取走了" + money + "元");
this.money -= money;
} else {
System.out.print("余额不足," + name + "取钱失败");
}
System.out.println(" 剩余金额为:" + this.money + "元");
}
}
}


//线程类
class MyThread extends Thread {
private Account account;

public MyThread(Account account, String name) {
super(name);
this.account = account;
}

@Override
public void run() {
//使用账户类的取钱方法
int v = (int) (Math.random() * 1000);
account.withdrawMoney(v);
}
}

public class ThreadSyncDemo1 {
public static void main(String[] args) {
//定义一个账户,余额为100000
Account account = new Account("10086", 100000);
//定义多个线程操作这个账户
new MyThread(account, "小红").start();
new MyThread(account, "小明").start();
new MyThread(account, "小蓝").start();
new MyThread(account, "小绿").start();
new MyThread(account, "小黄").start();
}
}

同步方法

把出现线程安全问题的核心方法给上锁,每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。

修饰符 synchronized 返回值类型  方法名称 (形参列表){
操作共享资源的代码
}
  • 同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。
  • 如果方法是实例方法:同步方法默认用​​this​​作为的锁对象。但是代码要高度面向对象!
  • 如果方法是静态方法:同步方法默认用​​类名.class​​作为的锁对象。

Lock锁

  1. 为了更清晰的表达如何加锁和释放锁, JDK5以后提供了一个新的锁对象Lock。
  2. Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作。
  3. Lock是接口不能直接实例化,通常采用它的实现类​​ReentrantLock​​来构建Lock锁对象。

方法

描述

ReentrantLock()

获得Lock锁的实现类对象

lock()

获得锁

unlock()

释放锁

//加上final修饰,别人就撬不了锁
final Lock lock = new ReentrantLock();
lock.lock();//上锁
//锁的共享内容
lock.unlock();//解锁

线程通信

所谓线程通信就是线程间相互发送数据,线程间通信的模型有两种:共享内存消息传递

案例:有两个线程,A 线程向一个集合里面依次添加元素“abc”字符串,一共添加十次,当添加到第五次的时候,希望 B 线程能够收到 A 线程的通知,然后 B 线程执行相关的业务操作。

volatile 关键字

基于 volatile 关键字来实现线程间相互通信是使用共享内存的思想。大致意思就是多个线程同时监听一个变量,当这个变量发生变化的时候 ,线程能够感知并执行相应的业务,这也是最简单的一种实现方式。

public class TestDemo1 {
//定义共享变量来实现通信,它需要volatile修饰,否则线程不能及时感知
static volatile boolean notice = false;

public static void main(String[] args) {
List<String> list = new ArrayList<>();
//线程A(这里用lambda实现了RunnableImpl)
Thread threadA = new Thread(() -> {
for (int i = 1; i <= 10; i++) {
list.add("abc");
System.out.println("线程A添加元素,此时list的size为:" + list.size());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (list.size() == 5)
notice = true;
}
});
//线程B
Thread threadB = new Thread(() -> {
while (true) {
if (notice) {
System.out.println("线程B收到通知,开始执行自己的业务...");
break;
}
}
});
//需要先启动线程B
threadB.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 再启动线程A
threadA.start();
}
}

Object 类的 wait()/notify()

Object类提供了线程相关方法 :

方法

描述

wait()

让当前线程等待并释放所占锁????,直到另一个线程调用 ​​notify​​​或 ​​notifyAll​

notify()

唤醒正在等待的单个线程,并获得锁????

notifyAll()

唤醒正在等待的所有线程,并获得锁????

wait / notify 必须配合 synchronized 使用,不使用 synchronized 就使用 wait / notify 会报出异常

在调用前一定要获得相同的锁,如果在调用前没有获得锁,程序会抛出异常;如果获得的不是同一把锁,notify不起作用。

wait 方法释放锁,notify 方法不释放锁

public class TestDemo3 {
public static void main(String[] args) {
//定义一个锁对象
Object lock = new Object();
List<String> list = new ArrayList<>();
// 线程A
Thread threadA = new Thread(() -> {
synchronized (lock) {
for (int i = 1; i <= 10; i++) {
list.add("abc");
System.out.println("线程A添加元素,此时list的size为:" + list.size());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (list.size() == 5)
lock.notify();//唤醒B线程
}
}
});
//线程B
Thread threadB = new Thread(() -> {
while (true) {
synchronized (lock) {
if (list.size() != 5) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程B收到通知,开始执行自己的业务...");
}
}
});
//需要先启动线程B
threadB.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//再启动线程A
threadA.start();
}
}

由输出结果得知,在线程 A 发出 notify() 唤醒通知之后,依然是走完了自己线程的业务之后,线程 B 才开始执行,正好说明 notify() 不释放锁,而 wait() 释放锁。

可以理解为 wait()是释放共享资源,notify()是让线程获取共享资源

生产者与消费者模型

生产者线程负责生产数据,消费者线程负责消费生产者产生的数据。

public class Main {
public static void main(String[] args) {
//同一个仓库
BufferArea bufferArea = new BufferArea();

//三个生产者
Producer p1 = new Producer(bufferArea);
Producer p2 = new Producer(bufferArea);
Producer p3 = new Producer(bufferArea);
//三个消费者
Consumer c1 = new Consumer(bufferArea);
Consumer c2 = new Consumer(bufferArea);
Consumer c3 = new Consumer(bufferArea);
//创建线程,并给线程命名
Thread t1 = new Thread(p1, "生产者1");
Thread t2 = new Thread(p2, "生产者2");
Thread t3 = new Thread(p3, "生产者3");
Thread t4 = new Thread(c1, "消费者1");
Thread t5 = new Thread(c2, "消费者2");
Thread t6 = new Thread(c3, "消费者3");
//使线程进入就绪状态
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
t6.start();
}
}


class Producer implements Runnable {

private final BufferArea bufferArea;

//通过传入参数的方式是使得对象相同,具有互斥锁的效果。
public Producer(BufferArea bufferArea) {
this.bufferArea = bufferArea;
}

@Override
public void run() {

while (true) {
try {
Thread.sleep((int) (Math.random() * 10000));//设置生产的时间间隔
} catch (InterruptedException e) {
e.printStackTrace();
}
bufferArea.set();//生产产品
}
}

}

class Consumer implements Runnable {
private final BufferArea bufferArea;

public Consumer(BufferArea bufferArea) {
this.bufferArea = bufferArea;
}

@Override
public void run() {
while (true) {
try {
Thread.sleep((int) (Math.random() * 10000));//设置生产的时间间隔
} catch (InterruptedException e) {
e.printStackTrace();
}
bufferArea.get();//消费产品
}
}
}

//缓冲区
class BufferArea {
private int currNum = 0;//当前仓库的产品数量
private int maxNum = 10;//仓库最大产品容量

//同步方法(实例)都是使用this作为锁对象,即每次只允许一个对象(生产者或消费者)访问仓库(缓冲区)

//同步方法,每次只允许一个生产者进行生产
public synchronized void set() {
if (currNum < maxNum) {
currNum++;
System.out.println(Thread.currentThread().getName() + " 生产了一件产品!当前产品数为:" + currNum);
notifyAll();
} else {//当前产品数大于仓库的最大容量
try {
System.out.println(Thread.currentThread().getName() + " 开始等待!当前仓库已满,产品数为:" + currNum);
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

//同步方法,每次只允许一个消费者进行消费
public synchronized void get() {
if (currNum > 0) {//仓库中有产品
currNum--;
System.out.println(Thread.currentThread().getName() + " 获得了一件产品!当前产品数为:" + currNum);
notifyAll();
} else {
try {
System.out.println(Thread.currentThread().getName() + " 开始等待!当前仓库为空,产品数为:" + currNum);
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

线程调度

常见的线程调度模型有哪些?

抢占式调度模型
哪个线程的优先级比较高,抢到的CPU时间片的概率就高一些。java采用的就是抢占式调度模型

均分式调度模型
平均分配CPU时间片。每个线程占有的CPU时间片时间长度一样。

Thread类的线程调度方法

int getPriority() 获得线程优先级

void setPriority(int newPriority) 设置线程优先级

  • 最低优先级1
  • 默认优先级是5
  • 最高优先级10

static void yield() 让位方法,当前线程暂停,回到就绪状态,让给其它线程。

yield()方法不是阻塞方法,其会让当前线程从“运行状态”回到“就绪状态”。

注意:在回到就绪之后,有可能还会再次抢到。

void join() 将一个线程合并到当前线程中,当前线程受阻塞,加入的线程执行直到结束

join方法的本质调用的是Object中的wait方法实现线程的阻塞

在很多情况下,主线程创建并启动子线程,如果子线程中要进行大量的耗时运算,主线程将可能早于子线程结束。如果主线程需要知道子线程的执行结果时,就需要等待子线程执行结束了。主线程可以sleep(xx),但这样的xx时间不好确定,因为子线程的执行时间不确定,join()方法比较合适这个场景。

//现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完之后执行,T3在T2执行完后执行?
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("t1 is running......");
}
});

Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2 is running......");
}
});

Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
try {
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t3 is running.......");
}
});
t2.start();

t1.start();

t3.start();
}
}

​join()​​需要在​​start()​​后进行调用才有效果。可以这样理解:若是线程都还没有​​start()​​,那么久不存在所谓的同步的

public class Main {
public static void main(String[] args) {
Thread3 thread3 = new Thread3();
thread3.start();
}

}

class Thread1 extends Thread {
@Override
public void run() {
System.out.println("Thread 1");
}
}

class Thread2 extends Thread {
@Override
public void run() {
Thread1 thread1 = new Thread1();
try {
//join需要在start后进行调用
thread1.start();
thread1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 2");
}
}

class Thread3 extends Thread {
@Override
public void run() {
Thread2 thread2 = new Thread2();
try {
//join需要在start后进行调用
thread2.start();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 3");

}
}