本节目标(全是重点,都必须掌握)
1、了解什么是线程、多线程、进程以及他们之间的关系
2、了解多线程的优势以及各种特性
3、用Java掌握多种创建线程的方法
一、线程、多线程、进程
1、概念
1.基本概念
这三个名词的概念可以用一个餐馆的厨房和厨师来进行超级形象的比喻。
想象一下一个餐馆的厨房。
整个厨房就是一个进程(Process)。这个厨房就是负责给前台客户提供各种菜品的进程。
厨房里得要有厨师吧,“厨房”里的“厨师”就叫做线程(Thread)。
如果是一个大型酒店,厨房里的厨师肯定不止一位对吧,那么如果一个厨房里的厨师有好几位,既一个进程里又好几个线程,那么这就叫做多线程。
每一个厨师在厨房可以独自完成自己的任务,这就叫做线程的独立性
每一个厨师在厨房可以同时工作(做自己的菜),这就叫做线程的并行性(线程之间是并发执行的)
但是每一个厨师都在同一个厨房进行工作(同一个进程),公用一套厨具,一个厨师(线程)如果在用面包机,另一个厨师(线程)可能就不能用了(主要取决于“厨房”获得的资源大小),这就叫做同一进程中的所有线程,资源是共享的。
2.线程、多线程、进程之间的区别
我们在梳理一下刚才所讲的比喻:
厨房——>进程
厨师——>线程
一个厨房多个厨师——>多线程
- 所以一个进程至少包含一个线程,既主线程。
- 不同的进程之间是不共享资源的(比如硬盘,网络资源等),但是一个进程中的每一个线程共享同一份资源。
- 线程是系统进行任务调度的最小单位。
- 进程是系统进行资源分配的最小单位。
- 一个进程挂了一般不会影响其他进程的正常运行,但是一个进程中的一个线程挂了,可能会影响到这个进程的正常运行!
- 任意一个线程,都可以在创建一个新的线程。
- 线程分为前台线程和后台线程,所有前台线程结束,整个进程才会结束,后台线程的结束,不会影响到进程的运行
注意:前台线程和后台线程除了是否能决定进程能否退出,在其他方面没有任何区别。
2、多线程的优点
首先,还是用刚才厨房和厨师的例子,假如说一个餐厅的客人非常的多,但是这么大的后厨,和这么多的需求只有一个厨师在卖命的炒菜,这显然是会被人骂的,餐厅早晚会倒闭。
为了充分利用厨房的各种资源,把每个厨具都用上,提高出餐效率,就可以请做个厨师同时进行工作。
既,充分利用进程所申请到的硬件资源(尤其是cup内核),多线程并发执行,高效完成任务,避免资源的浪费。
其次,虽然多进程也是可以实现并发编程的,但是进程的创建、调度、销毁速度远远慢与线程。既,线程更加轻量。
二、线程的创建方式
在java中创建线程,首先要了解一个类(Thread)和一个函数式接口(Runnable)
他们都有一个run()方法(Runnable的runf方法是抽象的,Thread的run方法是具体的),想要创建线程,就必须重写这个run()方法,然后把对象赋值给Thread类,通过Thread类的start()方法创建线程。
方式1——继承Thread:
class MyThread extends Thread{
@Override
public void run(){
while(true){
System.out.println("MuCreate");
}
}
}
public class Threads {
public static void main(String[] args) {
MyThread t1=new MyThread();
t1.start();
while(true){
System.out.println("主线程执行");
}
}
}
运行结果(截取部分)
以上是通过继承的方式。
Thread类有几个构造方法我们必须了解(等一下还会用到一个哦):
方式2——实现Runnable:
Runnable是一个函数式接口,只包含run这一个抽象方法(注意,Thread类中的run是一个具体的方法)
什么是函数式接口?
1、只包含一个抽象方法
2、可以有默认方法、静态方法(jdk8后引入)
3、和普通接口一样定义的变量必须初始化并且默认被public static final修饰
4、可以用@FunctionalInterface来检查其准确性
因此我们需要实现Runnable才能重写run方法:
class MyThread implements Runnable {//实现并重写run
@Override
public void run() {
while (true) {
System.out.println("MuCreate");
}
}
}
public class Threads {
public static void main(String[] args) {
/*下面两种创建方式都可以*/
//MyThread MyT=new MyThread();
//Thread t1=new Thread(MyT);//把接口对消给到Thread的构造方法
Thread t1 = new Thread(new MyThread());//把接口对消给到Thread的构造方法
t1.start();
while (true) {
System.out.println("主线程执行");
}
}
}
运行结果这里不演示了,和Thread的一样。
注意:
1)对于main方法,系统会自动创建一个线程,这个线程叫主线程
2)只有在调用了Thread对象的start方法,才会创建线程,如果调用run方法,那么run方法里的程序实际上是在主线程上运行的。
3)通过start方法创建的线程默认是前台线程(所有前台线程结束JVM才会退出,同时不论后台线程有没有结束,后台线程也会自动退出)
除了以上两种创建线程的方式,下面还有三种创建线程的方法需要学习,都需要学习掌握。
不过不用怕,下面这三种方式本质上没有区别,都是用到的Thread的这个构造方法:
方式3——匿名内部类之Runnable:
本质就是方式2,既用一个类实现Runnable,不过方式3的这个类没有名字。
Runnable runnable=new Runnable() {
@Override
public void run() {
System.out.println("我是手动创建的线程");
}
};
这段代码的含义是先创建一个匿名内部类,这个匿名内部类实现了Runnable接口,并且重写了run方法。然后把创建好的匿名内部类对象,重新赋值给了runnable,既向上转型。
然后再创建Thread类,调用刚才给的构造方法即可:
Runnable runnable=new Runnable() {
@Override
public void run() {
System.out.println("我是手动创建的线程");
}
};
Thread thread=new Thread(runnable);
thread.start();
当然也可以在Thread的构造方法中直接进行匿名内部类的编写,省去runnable的创建:
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while(true){
System.out.println("我是手动创建的线程");
}
}
});
thread.start();
这样写的话代码的内聚性更强。
方式4——匿名内部类之Thread
本质就是方式1,既用一个类继承Thread,不过方式4的这个类没有名字。
Thread thread = new Thread(){
@Override
public void run(){
while(true){
System.out.println("创建的线程");
}
}
};
thread.start();
while(true){
System.out.println("主线程");
}
方式5——匿名内部类之lambda
方式5的创建和方式3其实是一样的,不过这里换成了lambda表达式
通过lambda表达式重写run方法,并创建一个Runnable的对象,通过赋值的方式,传递给Thread
类的构造方法。
Thread thread = new Thread(() -> {
//下面直接编写方法体
while (true) {
System.out.println("hehe");
}
});
thread.start();
除了这几种,实际上还可以使用Callable\Excuters创建,以后都会学习到,这里先做一个了解即可。
不论是那种创建方式,都需要重写run()方法,run()方法相当于线程入口。
并且都必须通过Thread类调用start()方法才能启动线程,如果调用run()方法是不会创建线程的,它会仍然在主线程中运行run()方法中的程序。
注意:
start()创建的线程,默认都是前台线程,如果不进行命名,默认按照Thread-0、Thread-1这种方式进行取名。
完