1. 多线程的概念
1.1 进程、线程、多进程的概念
进程:正在进行中的程序(直译)。
线程是程序执行的一条路径, 一个进程中可以包含多条线程。
一个应用程序可以理解成就是一个进程。
多线程并发执行可以提高程序的效率, 可以同时完成多项工作。
1.2 多线程应用场景
VNC同时共享屏幕给多个电脑。
迅雷开启多条线程一起下载。
QQ同时和多个人一起视频。
服务器同时处理多个客户端请求。
1.3 并行和并发的区别
并行就是两个任务同时运行,就是甲任务进行的同时,乙任务也在进行。(需要多核CPU)。
并发是指两个任务都请求运行,而处理器只能按受一个任务,就把这两个任务安排轮流进行,由于间时间隔较短,使人感觉两个任务都在运行(画图-任务调度)。
1.4 Java程序运行原理
Java命令会启动java虚拟机(JVM),等于启动了一个应用程序,也就是启动了一个进程。
该进程会自动启动一个 “主线程” ,然后主线程去调用某个类的 main 方法
一个应用程序有且只有一个主线程,程序员不能New主线程,可以New子线程。
1.5 JVM启动的是多线程吗?
JVM启动至少启动了垃圾回收线程和主线程,所以是多线程的。
main方法的代码执行的位置就是在主线程(路径)。
一个进程有多个线程。
finalize() 这个方法在子线程(垃圾回收线程)执行。
package day03;
public class Demo01 {
public static void main(String[] args) {
/*JVM的启动是多线程的吗?【面试题】
1.JVM启动至少启动了垃圾回收线程和主线程,所以是多线程的。
2.main方法的代码执行的位置就是在主线程(路径)
3.一个进程有多个线程
4.finalize()这个方法在子线程(垃圾回收线程)执行*/
System.out.println("AAAAA");
System.out.println("BBBBB");
System.out.println("CCCCC");
System.out.println("DDDDD");
//打印线程名称
System.out.println(Thread.currentThread());//主线程
for (int i = 0; i < 3; i++) {
new Student();
System.gc(); //启动垃圾回收
}
}
}
class Student {
//被垃圾回收器回收时,会调用
//对象从内存释放时,会调用
@Override
protected void finalize() throws Throwable {
// TODO Auto-generated method stub
System.out.println("student 被回收了...");
//打印线程名称
System.out.println(Thread.currentThread());//子线程
}
}
2. Java中线程的实现方式
2.1 方式一: 继承Thread
使用步骤:
定义一个类继承Thread类。
覆盖Thread类中的run方法。
直接创建Thread的子类对象创建线程。
调用start方法开启线程并调用线程的任务run方法执行。
package day03.lesson03;
public class demo0 {
public static void main(String[] args) {
for (int i = 1; i <= 12; i++) {
ThreadDemo threadDemo = new ThreadDemo();
threadDemo.start();
}
}
}
class ThreadDemo extends Thread {
@Override
public void run() {
System.out.println("购买火车票任务……" + Thread.currentThread());
System.out.println("线程名称:" + this.getName());
}
}
注: 不能通过threadDemo.run()的方式来执行任务,因为这种试的任务是在主线程执行的。
正确的执行任务的方式,调用threadDemo.start()方法,内部会开启新线程,调用run方法。
P.S.
可以通过Thread的getName方法获取线程的名称,名称格式:Thread-编号(从0开始)。
Thread在创建的时候,该Thread就已经命名了。源码如下:
JVM创建的主线程的任务都定义在了主函数中。而自定义的线程,它的任务在哪儿呢?
Thread类用于描述线程,线程是需要任务的。所以Thread类也有对任务的描述。这个任务就是通过Thread类中的run方法来体现。也就是说,run方法就是封装自定义线程运行任务的函数,run方法中定义的就是线程要运行的任务代码。
总结:
开启线程是为了运行指定代码,所以只有继承Thread类,并复写run方法,将运行的代码定义在run方法中即可。
2.2 方式二: 实现Runnable接口
使用步骤:
定义类实现Runnable接口。
实现run方法。
把新线程要做的事写在run方法中。
创建自定义的Runnable的子类对象,创建Thread对象传入Runnable。
调用start()开启新线程, 内部会自动调用Runnable的run()方法。
package day03;
public class demo02 {
public static void main(String[] args) {
//1.创建runable对象
TrainTask task = new TrainTask();
//2.创建Thread对象
Thread t1 = new Thread(task);
//3.启动线程
t1.start();
for (int i=1;i<=12;i++){
Thread td = new Thread(task);
td.start();
}
}
}
class TrainTask implements Runnable{
@Override
public void run() {
System.out.println("购买火车票任务……" + Thread.currentThread());
System.out.println("线程名称:" + Thread.currentThread().getName());
}
}
实现Runnable接口的好处:
1. 将线程的任务从线程的子类中分离出来,进行了单独的封装,按照面向对象的思想将任务封装成对象。
2. 避免了Java单继承的局限性。所以,创建线程的第二种方式较为常用。
2.3 两种方式的区别
查看源码的区别:
继承Thread : 由于子类重写了Thread类的run(), 当调用start()时直接找子类的run()方法。
实现Runnable : 构造函数中传入了Runnable的引用, 有个成员变量记住了它, 调用run()方法时内部判断成员变量Runnable的引用是否为空。
继承Thread
好处是: 可以直接使用Thread类中的方法,代码简单。
弊端是: 如果已经有了父类,就不能用这种方法。
实现Runnable接口
好处是: 即使自己定义的线程类有了父类也没关系,因为有了父类也可以实现接口,代码更灵活。
弊端是: 不能直接使用Thread中的方法,需要先获取到线程对象后,才能得到Thread的方法,代码复杂。