-
线程概念
-
线程的创建和停止
-
线程的状态
1. 多线程概述
1.1 多线程介绍
多线程是Java语言的重要特性,大量应用于网络编程、服务器端程序的开发,最常见的UI界面底层原理、操作系统底层原理都大量使用了多线程。
我们可以流畅的点击软件或者游戏中的各种按钮,其实,底层就是多线程的应用。UI界面的主线程绘制界面,如果有一个耗时的操作发生则启动新的线程,完全不影响主线程的工作。当这个线程工作完毕后,再更新到主界面上。
我们可以上百人、上千人、上万人同时访问某个网站,其实,也是基于网站服务器的多线程原理,如果没有多线程,服务器处理速度会极大降低。
在学习多线程之前,我们先要了解几个关于多线程有关的概念。
1.1.1 程序
程序(Program)是一个静态的概念,一般对应于操作系统中一个可执行文件,比如:我们要启动酷狗听音乐,则对应酷狗的可执行程序。当我们双击酷狗,则加载程序到内存中,开始执行该程序,于是产生了“进程”。
1.1.2 进程
执行中的程序叫做进程(Process),是一个动态的概念。确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能。
现代的操作系统都可以同时启动多个进程。比如:我们在用酷狗听音乐、也可以使用IDEA写代码、也可以同时用浏览器查看网页。
多进程有什么意义呢?
单进程的计算机只能做一件事情,而我们现在的计算机都可以做多件事情。例如,一边玩游戏(游戏进程),一边听音乐(音乐进程)。
也就是说现在的计算机都是支持多进程的,可以在一个时间段内执行多个任务。并且,还可以提高CPU的使用率。
1.1.3 线程
线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程,也可以有多个线程。
什么是多线程呢?即就是一个程序中有多个线程在同时执行,我们也称之为多线程程序。
紧接着,我们来区别单线程程序与多线程程序的不同:
-
单线程程序:即,若有多个任务只能依次执行。当上一个任务执行结束后,下一个任务才开始执行。
-
多线程程序:即,若有多个任务可以“同时”执行。多个任务可以并发执行。
1.1.4 Java程序的运行原理
由java命令启动JVM,JVM启动就相当于启动了一个进程,该线程在负责java程序的运行,而且这个线程运行的代码存在于main方法中,我们把这个线程称之为主线程。
JVM虚拟机的启动是单线程的还是多线程的?
答案是多线程。原因是垃圾回收线程也要先启动,否则很容易会出现内存溢出。现在的垃圾回收线程加上前面的主线程,最低启动了两个线程,所以,JVM的启动其实是多线程的。
1.2 并发和并行
在学习多线程之前,我们必须先理解什么是并发,什么是并行,什么是并发编程,什么是并行编程。
1.2.1 并发(concurrency)
指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。
如上图所示,并发就是只有一个CPU资源,程序(或线程)之间要竞争得到执行机会。图中的第一个阶段,在A执行的过程中,B、C不会执行,因为这段时间内这个CPU资源被A竞争到了,同理,第二阶段只有B在执行,第三阶段只有C在执行。其实,并发过程中,A、B、C并不是同时进行的(微观角度),但又是同时进行的(宏观角度)。
在同一个时间点上,一个CPU只能支持一个线程在执行。因为CPU运行的速度很快,CPU使用抢占式调度模式在多个线程间进行着高速的切换,因此我们看起来的感觉就像是多线程一样,也就是看上去就是在同一时刻运行。
1.2.2 并行(parallellism)
指在同一时刻,有多条指令在多个处理器上同时执行。
如图所示,在同一时刻,ABC都是同时执行(微观、宏观)
1.2.3 并发编程和并行编程
-
在CPU比较繁忙,资源不足的时候(开启了很多进程),操作系统只为一个含有多线程的进程分配仅有的CPU资源,这些线程就会为自己尽量多抢时间片,这就是通过多线程实现并发,线程之间会竞争CPU资源争取执行机会。
-
在CPU资源比较充足的时候,一个进程内的多线程,可以被分配到不同的CPU资源,这就是通过多线程实现并行。
至于多线程实现的是并发还是并行?上面所说,所写多线程可能被分配到一个CPU内核中执行,也可能被分配到不同CPU执行,分配过程是操作系统所为,不可人为控制。所以,如果有人问我我所写的多线程是并发还是并行的?我会说,都有可能。
总结:单核CPU上的多线程,只是由操作系统来完成多任务间对CPU的运行切换,并非真正意义上的并发。随着多核CPU的出现,也就意味着不同的线程能被不同的CPU核得到真正意义的并行执行,故而多线程技术得到广泛应用。
不管并发还是并行,都提高了程序对CPU资源的利用率,最大限度地利用CPU资源。
2. 线程的创建
在Java中使用多线程非常简单,我们先学习如何创建和使用线程,然后结合案例再深入剖析线程的特性。
2.1 Thread 类介绍
该如何创建线程呢?通过 API 中搜索,查到 Thread 类。通过阅读 Thread 类中的描述,知道 Thread 类用来描述线程,使其具备线程应该有功能。Java 虚拟机允许应用程序并发地运行多个执行线程。
2.1.1 构造方法
2.1.2 常用方法
继续阅读,发现创建新执行线程有两种方法。
-
一种方法是将类声明为 Thread 的子类。该子类应重写 Thread 类的 run 方法。创建对象,开启线程。run 方法相当于其它线程的 main 方法。
-
另一种方法是声明一个实现 Runnable 接口的类。该类然后重写 run 方法。然后创建Runnable 的子类对象,传入到某个线程的构造方法中,开启线程。
2.2 创建线程方式一:继承Thread类
-
继承Thread类实现多线程的步骤:
1) 定义一个类并继承于Thread类。
2) 重写Thread类的run()方法,run()方法中包含了线程需要执行的任务。
3) 调用的Thread的子类来创建线程对象。
4) 通过调用start()方法开启线程,线程开启后会自动调用线程的run()方法。
【示例】继承Thread类实现多线程
public class MyThread01 extends Thread {
// 继承父类的构造方法
public MyThread01() {
}
public MyThread01(String name) {
super(name);
}
@Override
public void run() {
// 定义线程任务代码: 打印0-9
for (int i = 0; i < 10; i++) {
System.out.println(super.getName() + "---" + i);
}
}
}
/*
创建线程的方式一
1)定义一个类并继承于Thread类。
2)重写Thread类的run()方法,run()方法中包含了线程需要执行的任务。
3)调用的Thread的子类来创建线程对象。
4)通过调用start()方法开启线程,线程开启后会自动调用线程的run()方法
*/
public class ThreadDemo2 {
public static void main(String[] args) {
// 创建两个线程
// 创建线程A
MyThread01 t1 = new MyThread01("线程A");
// 创建线程B
MyThread01 t2 = new MyThread01("线程B");
// 开启两个线程
t1.start();
t2.start();
// 定义主线程任务
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "---" + i);
}
}
}
运行以上案例代码,输出结果如下:
2.3 创建线程深度剖析
思考1:线程对象调用run方法和调用start方法区别?
线程对象调用run方法不开启线程,仅是对象调用方法并在主线程中执行。线程对象调用start开启线程,并让JVM调用run方法在开启的线程中执行。
思考2:我们为什么要继承 Thread 类,并调用其的 start 方法才能开启线程呢?
继承 Thread 类:因为 Thread 类用来描述线程,具备线程应该有功能。那为什么不直接创建 Thread
类的对象呢?如下代码:
public class ThreadDemo3 {
public static void main(String[] args) {
Thread t1 = new Thread();
Thread t2 = new Thread();
/*
此时JVM会开辟新线程,执行Thread类中的run(),但是由于线程任务target为null,所以这个run()无任何任务执行
这个违反了线程的初衷,所以我们需要在Thread子类中重写Run() ,将Run()中代码作为线程的任务执行。
*/
t1.start();
}
}
以上代码语法上没有任何问题,但是该 start 调用的是 Thread 类中的 run 方法,而这个 run 方法没有做什么事情,更重要的是这个 run 方法中并没有定义我们需要让线程执行的代码。
创建线程的目的就是为了建立程序单独的执行路径,让多部分代码实现同时执行。也就是说线程创建并执行需要给定线程要执行的任务。对于之前所讲的主线程,它的任务定义在 main 函数中。自定义线程需要执行的任务都定义在 run方法中。
Thread 类 run 方法中的任务并不是我们所需要的,只有重写这个 run 方法。既然 Thread 类已经定义了线程任务的编写位置(run 方法),那么只要在编写位置(run 方法)中定义任务代码即可,所以进行了重写 run 方法动作。
思考3:多线程执行时,到底在内存中是如何运行的呢?
多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间。进行方法的压栈和弹栈。
在多线程中,每个线程都有自己独立的栈内存,但是都是共享的同一个堆内存。在某个线程中程序执行出现了异常,那么对应线程执行终止,但是不影响别的线程程序执行。
main方法执行完毕之后,虚拟机有可能不会立即结束,只有等所有的线程都执行完毕之后,虚拟机才会结束!
思考4:开启的线程都会有自己的独立运行栈内存,那么这些运行的线程的名字是什么呢?
查阅Thread类的API文档发现有个方法是获取当前正在运行的线程对象,还有个方法是获取当前线程对象的名称。
想要获取运行时线程名称,必须先要得到运行时线程对象(这里的线程对象和继承Thread子类对象是不一样的)。在线程类方法当中有一个方法,叫做currentThread(),返回thread类型,静态的,类名可以直接调用。
【示例】获取当前线程对象和线程名称
public class MyThread02 extends Thread {
// 定义该线程任务类的name属性
private String name;
// 继承父类的构造方法
public MyThread02() {
}
public MyThread02(String name) {
this.name = name;
}
@Override
public void run() {
// 打印当前正在运行的线程对象的名称
String threadName = Thread.currentThread().getName();
System.out.println(name +" 线程名称为:" + threadName );
}
}
public class ThreadDemo4 {
public static void main(String[] args) {
MyThread02 t1 = new MyThread02("线程A");
MyThread02 t2 = new MyThread02("线程B");
t1.start();
t2.start();
String name = Thread.currentThread().getName();
System.out.println("主线程名称:"+name);
}
}
运行以上案例代码,输出结果如下:
通过运行结果观察,发现主线程的名称为:main。自定义的线程名字默认为:Thread-加上编号,编号从0开始递增,th1线程对应的名称为:Thread-0,th2线程对应的名称为Thread-1。
那么自定义线程的默认名字是怎么来的呢? 通过对Thread类的源码分析,我们发现调用Thread类的构造方法时,默认就给该线程对象定义了一个名字,格式为:Thread-加上编号。
由此,我们也可以得出一个结论:当我们创建线程子类对象的时候,它们在创建的同时已经完成了名称的定义。
【示例】获取线程对象的名称
public class ThreadDemo5 {
public static void main(String[] args) {
Thread t1 = new Thread();
Thread t2 = new Thread();
System.out.println("t1线程对象的名称:" + t1.getName());
System.out.println("t2线程对象的名称:" + t2.getName());
}
}
运行以上案例代码,输出结果如下:
思考5:可以手动的设置线程名称吗?
自定义的线程名字默认为:Thread-加上编号,如果我们想要修改默认的线程名字,可以在创建线程对象的时候设置线程的名称,也可以使用Thread类提供的setName()方法来实现。
【示例】设置线程对象的名称
public class MyThread03 extends Thread {
// 继承父类的构造方法
public MyThread03() {
}
public MyThread03(String name) {
super(name);
}
@Override
public void run() {
// 打印当前正在运行的线程对象的名称
String threadName = Thread.currentThread().getName();
System.out.println(" 线程名称为:" + threadName );
}
}
public class ThreadDemo6 {
public static void main(String[] args) {
MyThread03 t1 = new MyThread03("窗口一");
t1.start();
MyThread03 t2 = new MyThread03();
t2.setName("窗口二");
t2.start();
}
}
运行以上案例代码,输出结果如下:
2.4 创建线程方式二:实现 Runnable 接口
使用继承Thread类的方式来创建线程有一个缺点,那就是自定义的类继承Thread类后就不能继承别的父类,如果还想继承别的父类那么可以选用第二种创建线程的方式。
在开发中,我们更多的是通过Runnable接口实现多线程,使用这种方式避免了Java单继承的局限性,所以实现Runnable接口方式要通用一些。
查看 Runnable 接口说明文档:Runnable 接口用来指定每个线程要执行的任务。包含了一个run的无参数抽象方法,需要由接口实现类重写该方法。
2.4.1 接口中的方法
2.4.2 Thread 类构造方法
-
实现Runnable接口实现多线程的步骤:
1) 定义一个类并实现Runnable接口。
2) 重写Runnable接口的run()方法,在run()方法中包含线程需要执行的任务。
3) 通过Thread类创建线程对象,并把Runnable接口的实现类对象作为参数传递。
4) 调用线程对象的start()方法开启线程,并调用Runnable实现类的run()方法。
【示例】实现Runable接口实现多线程
public class MyTask01 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"线程正在执行:" + i);
}
}
}
/*线程的创建方式二
Runnable接口: 指定了每个线程需要执行的任务,
包含了run(),需要通过子类重写run()定义线程任务
步骤:
1. 定义线程任务类实现Runnable接口
2. 重写Runnable接口中的run(),在run()定义了线程的任务代码
3. 创建线程对象并将线程任务类对象作为:参数传入
4. 调用线程对象的start()启动线程,执行线程任务类中的run()
*/
public class ThreadDemo7 {
public static void main(String[] args) {
// 1. 创建两个线程任务对象
MyTask01 myTask1 = new MyTask01();
MyTask01 myTask2 = new MyTask01();
// 2. 创建两个线程
Thread t1 = new Thread(myTask1, "线程A");
Thread t2 = new Thread(myTask2, "线程B");
// 3. 开启两个线程
t1.start();
t2.start();
// 主线程执行任务
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"线程正在执行:" + i);
}
}
}
运行以上案例代码,输出结果如下:
Thread 类用来描述线程,使其具备线程应该有功能,Runnable接口实现类用来封装线程任务,从而实现线程对象和线程任务进行解耦。
实现Runnable接口的好处,不但避免了java单继承的局限性,而且还将线程任务从线程子类相分离,进行了单独的封装,按照面向对象的思想将任务封装成对象。
2.5 模拟Thread类start方法的实现
我们知道,通过继承Thread类创建线程,线程任务是封装在Thread子类的run方法中;通过实现Runnable接口来创建线程,线程任务是封装在Runnable接口实现类的run方法中,那么调用start方法开启线程,在Thread内部是如何正确的执行线程任务的呢?
接下来我们就来模拟Thread类,明确调用start方法开启线程调用之后是如何实现调用run方法来执行对应的线程任务。
【示例】模拟Thread类start方法的实现
class Thread {
private Runnable target;
public Thread() {}
public Thread(Runnable r) {
this.target = r;
}
public void start() {
run(); // 注意此处是重点
}
public void run() {
if(target != null)
target.run();
}
}
模拟Thread类start方法的实现的核心:
如果创建线程采用继承Thread类的方式,也就是通过Thread子类对象调用start方法,那么调用的就是Thread子类对象的run方法。
如果创建线程采用实现Runnable接口的方式,也就是通过Thread对象调用start方法,那么调用的就是Thread的run方法,然后再去调用Runnable接口实现类的run。
接下来,我们就基于两种创建线程的方式,来对我们模拟Thread类的start方法进行测试,看一下我们模拟实现是否成功!
【示例】基于两种创建线程方式的测试
public class MyTask01 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"线程正在执行:" + i);
}
}
}
/*
@Author xiangge
@Date 2023/8/1
@Description 定义子线程类
*/
public class MyThread01 extends Thread {
public MyThread01() {
}
public MyThread01(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "正在执行" + i);
}
}
}
// 基于两种创建线程方式的测试
public class ThreadDemo8 {
public static void main(String[] args) {
// 1.使用创建线程方式一
MyThread01 t1 = new MyThread01("线程A");
// 2. 使用创建线程方式二
MyTask01 task1 = new MyTask01();
Thread t2 = new Thread(task1, "线程B");
//3. 开启线程
t1.start();
t2.start();
}
}
运行以上案例代码,输出结果如下:
2.6 创建线程方式三:Lambda表达式创建线程
Lambda表达式起始于JDK8,此时可以代替匿名内部类创建,使用Lambda可以简化匿名内部类操作。
// 2. 使用创建线程方式二:匿名内部类方式
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"线程正在执行:" + i);
}
}
}, "线程B");
//2. 使用创建线程方式二:Lambda表达式方式
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "线程正在执行:" + i);
}
}, "线程B");