- 专栏内容:Java
- ⛪个人主页:子夜的星的主页
- 座右铭:前路未远,步履不停
嗨!想象一下,你的面前有一张展开的白纸,而你的左手和右手各握一支画笔。你的任务是在同一时间,左手画出一个方形,右手画出一个圆形。你觉得这能够完成吗?我相信大多数人会认为这是不可能完成的任务。因为我们的大脑是单线程的,一次只能专注于一项任务,一心二用在绝大多数情况下是不可行的。然而,计算机不同,它具备多线程编程的能力,可以同时处理多个任务。现在,让我们一起探索Java中多线程这个神奇的功能吧。
进程是计算机中执行中的程序的实例。它是操作系统进行管理和调度的基本单位。
线程,有时被称为轻量级进程,是程序中的一个单独的执行流。它是操作系统调度的最小单位。与进程相比,线程之间的上下文切换通常更为迅速和高效。一个线程就是一个 “执行流”。每个线程之间都可以按照顺讯执行自己的代码、多个线程之间 “同时” 执行着多份代码。
那我们来简单的类比一下吧!
计算机的CPU就像一个工厂,而进程就是工厂里面的车间,线程就是车间里面的工人。
我们能够发现,多核CPU对应多个工厂,这些工厂可以从事不同的生产任务。比如有炼铁的、炼油的、食品加工等等。在一个工厂中,又可以包含一个或多个车间(进程),但是由于电力资源有限,同一时间就只能有一个车间在运行。换句话说,对于单核CPU,同一时间只能处理一个进程,其他进程处在等待状态。而一个车间中,可以有一个或多个工人(线程)协同工作,他们共享内部资源,共同完成任务。
同一个进程中,这些线程共用同一份资源(内存 + 硬盘…),但是每个线程独立去CPU上调度(状态、上下文、优先级、记账信息…)各自有各自的一份
所以,进程是操作系统进行分配的基本单位。而线程是操作系统进行调度执行的基本单位。
【面试题】谈谈进程和线程的区别和联系
进程是包含线程的,都是实现并发编程的方式,但是线程比进程更轻量。
进程是系统分配资源的基本单位,线程是系统调度的基本单位。创建进程的时候,把分配资源(虚拟地址空间、文件描述符表)的工作给做了,后面创建线程,直接共用资源即可。
进程有独立的地址空间,彼此之间不会相互影响到,进程的独立性。多个线程共用这份地址空间,一个线程一旦抛出异常,就可能会导致进程异常结束。
进程和进程之间不共享内存空间,同一个进程的线程之间共享同一个内存空间。
在 Java 中,有两种创建多线程的方式,分别是继承 Thread
类和实现 Runnable
接口。下面让我们一起来学习下这两种创建多线程的方法吧。
Thread
类class Mythread extends Thread{
public void run() {
System.out.println("这是线程运行代码!");
}
}
public class Main {
public static void main(String[] args) {
Mythread mythread = new Mythread();
mythread.start();
}
}
创建一个类,继承 Thread
,重写run
方法,该方法包含线程的实际执行逻辑。
start()
方法:start()
方法是Thread
类的一个方法,它用于启动线程并调用线程的run()
方法。
在这个例子中,myThread.start()
会启动一个新的线程,并在新线程中执行 run()
方法中定义的代码。
如果把myThread.start();
改成myThread.run();
后,运行的结果也是hello world。这两个有什么不一样呢?
run
只是上面的入口方法,并没有调用系统api也没有创建真正的线程来。
Runnable
接口class Myrunnable implements Runnable{
public void run() {
System.out.println("这是线程运行代码!");
}
}
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new Myrunnable());
thread.start();
}
}
该方法主要是创建一个类,实现 Runnable
接口,重写run
方法。
Thread
这里是直接把要完成的工作,放到了Thread
的Run
方法中
Runnable
这里,则是分开了,把要完成的工作放到了Runnable
中,再让Runnable
和Thread
配合。
把线程要执行的任务和线程本身进一步解耦合了。并发编程中,来完成某个工作,使用
Runnable
描述这个工作的具体细节。这样使用多线程的方式,就可以使用Runnable来搭配线程使用。使用线程池的方式,就可以使用Runnable来搭配线程池使用,使用协议的方式,也可以使用Runnable搭配协程。
特征 | 继承 Thread 类 |
实现 Runnable 接口 |
---|---|---|
类的继承关系 | 直接继承 Thread 类。 |
类实现 Runnable 接口,可以继承其他类。 |
创建方式 | 创建类的子类,重写 run() 方法。 |
创建类,实现 Runnable 接口,实现 run() 方法。 |
多线程对象创建 | 直接创建线程对象的实例。 | 先创建 Runnable 对象,再通过 Runnable 对象创建 Thread 对象。 |
启动线程 | 直接调用 start() 方法。 |
调用 Thread 类的构造方法,传递 Runnable 对象,然后调用 start() 方法。 |
资源共享 | 较难实现资源共享。 | 更容易实现资源共享,因为可以共享同一个 Runnable 对象。 |
多态使用 | 难以使用多态,因为已经继承了 Thread 类。 |
更容易使用多态,因为类可以继续实现其他接口或继承其他类。 |
回到上面的问题。把myThread.start();
改成myThread.run();
后,运行的结果也是hello world。
这两个有什么不一样呢?我们来举个栗子看看吧。
class MyThread extends Thread{
@Override
public void run() {
while(true){
System.out.println("hello thread");
}
}
}
public class Demo1 {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
//myThread.run();
while(true){
System.out.println("hello main");
}
}
}
使用myThread.start();
运行的程序,会同时打印hello thread 和 hello main
两个循环都在执行,两个线程分别执行自己的循环,并发性的执行。
而使用myThread.run();
运行的程序,只会打印hello main
只执行了一个循环。
我们可以通过匿名内部类来Thread
和Runnable
这两种写法。
什么是匿名内部类?
匿名内部类是一种在声明和创建类的对象同时进行的特殊形式,它没有显式的类名称。这种方式通常用于在使用类的地方定义类的实例,尤其是在创建接口的实现或继承抽象类的实例时。
Thread
匿名内部类写法 public static void main(String[] args) {
Thread thread = new Thread(){
@Override
public void run() {
System.out.println("这是线程运行代码!");
}
};
thread.start();
}
Runnable
匿名内部类写法 public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("这是线程运行代码!");
}
});
thread.start();
}
Lambda
表达式Lambda
表达式主要用于实现函数式接口,Lambda
表达式本质上就是一个匿名函数,像这样的匿名函数可以做回调函数来使用。
匿名函数,就是没有命名的函数。它具有传递函数体的能力,却无需提前声明整个函数的结构。
回调函数则是在程序执行中,不需要程序员主动调用的函数。相反,它会在特定的时机或条件下,由系统或其他代码自动触发执行。程序员通过将函数传递给其他代码,实现了一种回调机制,使得在适当的时候,这个函数会被调用,完成特定的操作。
好了,废话不多说,先用Lambda
表达式写一下吧。
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("这是线程运行代码!");
});
thread.start();
}
Thread thread = new Thread(() -> {
这里使用Lambda
表达式创建了一个新的Thread
对象,构造方法接受一个Runnable
接口的实现作为参数。Lambda
表达式的内容在大括号中,用于定义线程运行时执行的代码块。
线程的创建并不是只有这五种写法,还可以用线程池和Callable写法。这个我们后面再聊!