1、多线程概述
1.1、什么是进程
1)什么是进程?
进程:正在进行的程序,是系统进行资源分配和调用的独立单位。
每一个进程都有它自己的内存空间和系统资源。
2)多进程有和意义?
A:充分使用系统资源
B:提高用户体验
1.2、什么是线程
1)什么是线程?
线程:一个进程中可以执行多条任务,每一个任务,我们可以任务是一条线程
是进程中的单个顺序控制流,是一条执行路径
程序启动,至少要开启1条线程,执行主程序。如果要启动多个任务,就开启多个线 程,多个线程之间有自己独立的内存空间。独立运行,互不干扰
一个进程如果只有一条执行路径,则称为单线程程序。
一个进程如果有多条执行路径,则称为多线程程序。
2)多线程有何意义?
提高了程序的执行效率
3)线程是不是越多越好呢?
不是,对于单个CPU而言,CPU同一时刻,只能执行一条线程。为什么我们的程序看起 来是多线程运行呢?因为CPU会在各个线程之间进行快速的切换,快到你无法察觉,切换是 随机的。
如果线程过多,那么同一时间段,每个线程被执行的时间变少了。
4)JVM运行时,是单线程还是多线程
JVM运行的时候,至少要启动一条线程,来执行我们的main函数。
JVM中,有一个东西:垃圾回收器,这个是在后台运行的,这个程序的运行跟我们的 main函数互不干扰。垃圾回收也有自己独立的线程!
JVM至少有2条线程,main函数线程,和垃圾回收线程。是多线程程序。
5)main函数单线程运行图解
2、实现多线程
2.1、方式一:继承Thread
2.2.1 、Thread类介绍
Java提供了一个API,专门实现多线程的程序:Thread类
要启动一个线程,调用系统资源,开辟内存空间。很麻烦。
创建新执行线程有两种方法。一种方法是将类声明为 Thread
的子类。该子类应重写 Thread
类的 run
方法。接下来可以分配并启动该子类的实例。
2.1.2、实现步骤及演示
1)步骤:
A:自定义类,继承Thread,成为线程类
B:重写run方法
C:创建线程类的对象
D:启动线程
2)代码实现:
/*
* 演示:实现多线程的方式1:继承Thread
* 步骤:
* A:自定义类,继承Thread,成为线程类
* B:重写run方法
* C:创建线程类的对象
* D:启动线程
*/
// A:自定义类,继承Thread,成为线程类
class MyThread extends Thread{
// B:重写run方法
public void run() {
// 写任务要执行的代码
for( int i = 0; i < 100; i++){
System.out.println(i);
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
// C:创建线程类的对象
MyThread mt = new MyThread();
// D:启动线程
// mt.run();// 这里直接调用run方法,并没有启动线程。仅仅是调用了对象中的普通方法。没意义
mt.start();// 首先启动线程,然后会去执行run方法。所以即启动了线程,又执行了run方法
for( int i = 0; i < 100; i++){
System.out.println("main ... " + i);
}
}
}
2.1.3、几个疑问
1)为什么要继承Thread类?
继承了Thread类,我们的类就是线程类,就可以使用线程类中的各种功能。同时也可以重写run方法,写咱们自己要执行的代码
2)为什么要复写run方法?
API的设计者,在设计多线程的时候,不知道后面人们会想执行什么任务代码,怎么办? API是这样设计的:当调用start方法启动线程以后,JVM会自动的去调用run方法。
我们只需要去重写run方法,然后在run写我们自己要执行的任务代码,那么start之后就一定会调用我们重写的run方法。我们的任务代码就一定会被执行到。
3)为什么要调用start而不是run?
run方法:仅仅是用来封装线程的任务代码。无法启动线程。
strart方法:首先调用系统资源,启动一个线程,然后还会去调用我们写好的run方法。这样即启动了线程,又执行了run方法。
面试题:start方法和run方法的区别?
run:封装线程任务的。
start:可以启动线程,并且执行run方法。
4)线程是否可以多次启动?
正确的启动多个线程方法:
2.1.4、获取、设置线程名称
String getName() 返回该线程的名称。
void setName(String name) 改变线程名称,使之与参数 name 相同。
/*
* 演示:设置和获取线程的名称
* String getName() 返回该线程的名称
* void setName(String name) 改变线程名称,使之与参数 name 相同
* 分析:
* main函数所在的线程,不是我们创建的,而是虚拟机创建的。这个线程对象是谁,我们根本不知道。
* 因此,我们无法调用其getName或者setName功能
*
* 所以,我们需要获取main函数所在的线程的对象。怎么获取线程对象呢?
* static Thread currentThread() 返回对当前正在执行的线程对象的引用
* 注意:
* A:线程的默认名称:Thread-x x是一个从0 开始递增的整数!
* B:main函数的所在线程的名称:main
*
*/
class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(this.getName() + " ... " + i);
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
// 创建线程对象
MyThread mt1 = new MyThread();
MyThread mt2 = new MyThread();
// 设置线程的名称
mt1.setName("汤姆");
mt2.setName("杰瑞");
// 启动线程
mt1.start();
mt2.start();
// Thread t = Thread.currentThread();// 谁执行这行代码,获取的就是谁的线程
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " ... " +i);
}
}
}
2.1.5、多线程程序运行路径图解
2.2、方式二:实现Runnable接口
2.2.1、Runnable接口介绍
我们发现,这个接口中有一个run方法:
我们发现,Thread类实现了Runnable接口,很明显,Thread中的run就是Runnable中的run方法。
我们知道run方法是用来封装线程任务的。所以Runnable接口,就是一个用来封装线程任务的接口
如果一个类,实现了Runnable,并且实现了run方法。这个类我们就称为任务类。
2.2.2、实现步骤
创建线程的另一种方法是声明实现 Runnable
接口的类。该类然后实现 run
方法。然后可以分配该类的实例,在创建 Thread
时作为一个参数来传递并启动。
步骤:
A:自定义类,实现Runnable接口,这个类就是任务类
B:实现 run
方法
C:创建任务对象
D:创建Thread对象,并且把任务对象作为参数传递
E:启动线程
Thread类的构造函数:
Thread(Runnable target) 分配新的 Thread 对象。
Thread(Runnable target, String name) 分配新的 Thread 对象。
/*
* 演示:方式2实现多线程:实现Runnable
* Thread类的构造函数:
* Thread(Runnable target) 分配新的 Thread 对象。
* Thread(Runnable target, String name) 分配新的 Thread 对象。
* 步骤:
* A:自定义类,实现Runnable接口,这个类就是任务类
* B:实现 run 方法
* C:创建任务对象
* D:创建Thread对象,并且把任务对象作为参数传递
* E:启动线程
*/
// A:自定义类,实现Runnable接口,这个类就是任务类
class MyTask implements Runnable{
// B:实现 run 方法
@Override
public void run() {
for( int i = 0; i < 10; i++){
System.out.println(Thread.currentThread().getName() + " ... " + i);
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
// C:创建任务对象
MyTask mt = new MyTask();
// D:创建Thread对象,并且把任务对象作为参数传递
Thread t1 = new Thread(mt,"杰克");
Thread t2 = new Thread(mt,"Rose");
// E:启动线程
t1.start();
t2.start();
}
}
2.2.3、 实现原理
方式1:我们自定义类,继承Thread,称为了线程类。现在我们start启动这个线程,JVM自动的调用这个线程的run方法。
方式2:我们自定义类,实现Runnable接口,然后我们创建了Thread类对象,然后我们start启动了这个线程,JVM会自动的调用线程的run方法,也就是Thread中的run方法。但是,结果确实我们的Runnable实现类的run被执行了,为什么?
2.2.4、方式二的好处
A:方式二避免了Java单继承的局限性。
B:将线程的操作代码和线程的任务代码分离了,解耦合。代码的扩展性更好,重用性更好。
C:方式二可以更方便和灵活的实现数据的共享。