Java并发编程(二)|进程与线程:开启你的多线程学习之路
进程和线程的分析(Java 后端前置底层)
前言
大家好,上一篇我们梳理了进程的底层原理,本篇继续深入讲解进程的轻量级执行单元——线程,把进程与线程的核心区别、线程基础概念、Java线程入门一次性讲透,为后续多线程内容打下基础。
线程的引入
为什么要引入线程?
引入多个线程(轻量级进程)是为了实现并发编程。
早期并发编程依赖进程实现,但进程的开销极大:
1.创建/销毁/调度进程,都需要申请和回收系统资源(进程是资源分配的基本单位),时间成本很高。
2.进程切换时,需要完整保存/恢复进程上下文,效率低。
线程的定义
线程是CPU调度和执行的最小单位,是进程内的独立执行流。
1.线程不能独立存在,必须依附于进程。
2.每个进程至少有一个线程(主线程),也可以创建多个线程。
3.线程创建、销毁、调度的开销远小于进程,效率更高。
线程的资源分配特点
对于同一进程中的每个线程:
- 共享资源:共享进程的PID、内存指针、文件描述符表等系统资源。
- 独立资源:每个线程有独立的运行栈、状态、优先级、上下文、记账信息…,供操作系统调度使用。
线程特点
1.每个线程都可以独立地去cpu上调度执行。
2.同一进程的多个线程之间,共用同一份内存空间和文件资源。
创建线程时不需要重新申请资源,直接复用之前已经分配给进程的资源,省去了资源分配的开销,创建/销毁效率更高。
线程的优势
- 调度更轻量,切换开销远小于进程。
- 同一进程内的线程共享资源,创建时无需重复申请资源,效率更高。
- 支持多线程并发执行,充分利用多核CPU性能。
- 同一进程内的线程可以直接通信。
进程与线程的区别
核心区别
| 对比维度 | 进程 | 线程 |
|---|---|---|
| 资源分配 | 进程是资源分配的基本单位,拥有独立的内存、文件等系统资源 | 线程不拥有独立资源,共享所属进程的全部资源 |
| 调度执行 | 进程是资源容器,真正被调度执行的是进程内的线程 | 线程是CPU 调度和执行的基本单位,由操作系统直接调度 |
| 开销 | 创建、销毁、切换开销大 | 创建、销毁、切换开销极小,效率更高 |
| 独立性 | 进程间完全独立,一个进程崩溃不会影响其他进程 | 同一进程内的线程相互影响,一个线程异常可能导致整个进程崩溃 |
进程与线程的关系
这里用一个通俗比喻帮助理解:进程 = 公司,线程 = 公司里的员工。公司拥有独立的场地、设备、资金等资源,彼此相互独立;员工隶属于同一家公司,共享公司所有资源。
- 进程包含线程,一个进程里面可以有一个线程,也可以有多个线程;但一个线程只能属于一个进程。
- 进程和线程,都是用来实现并发编程场景的,但线程比进程更轻量,更高效。
Java中线程基础
Java线程的本质
运行一个Java程序,本质就是启动一个Java进程,这个进程里至少会有一个线程—主线程。主线程是系统默认创建的,其入口方法为main方法。
- 主线程负责创建其他子线程,也必须最后执行完毕,负责程序的关闭动作。
- Java中的Thread类,是操作系统线程API的封装,new Thread() 会创建Java线程对象,调用start() 方法才会真正创建内核线程。
start() 与 run() 的区别
- run() : 只是线程的执行逻辑,表明该线程要执行的任务是什么 ,直接调用就只是普通方法执行,不会创建新线程。
- start() : 会调用操作系统的native API,真正创建内核线程,实现并发执行,再间接调用run() 方法。
代码示例
package thread;
//创建一个类。继承自Thread,重写run
class MyThread1 extends Thread{
@Override
public void run() {
// 这个方法就是线程的入口方法
while(true) {
System.out.println("hello thread");
try {
//sleep是Thread的静态方法(类方法)
//在Java中,static作用和“静态”没有任何关系,它只是用来修饰类属性的
//与类属性相对的是实例属性
//sleep方法会抛出一个受查异常,必须显示处理
//此处必须try-catch,不能throws,因为这是重写的父类的run方法,父类的run、没有throws,子类方法也就不能有throws
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
}
}
//每个线程,都是一个独立执行的逻辑(独立的执行流)
//操作系统对于多个线程的调度顺序是不确定的,“随机”的(不是概率均等的随机),而是取决于操作系统对于线程调度的模块(调度器)的具体实现
public class Demo{
//main这个线程是JVM自动创建的,和其他线程相比,没啥特殊的
//一个Java进程中,至少会有一个main线程
public static void main(String[] args) throws InterruptedException {
// TODO 自动生成的方法存根
Thread thread=new MyThread1();
//start和run都是Thread的成员
//run只是描述了线程的入口(线程要做什么任务)
//start则是真正调用了系统API,在系统中创建出线程,让线程再调用run
thread.start();
//thread.run();---若调用run,而不是start,此时代码不会创建出新线程,只有一个主线程。run()方法一直循环。
while(true) {
System.out.println("hello main");
Thread.sleep(1000);
}
//此时的并发执行是并行+并发
//并行:多核+真正同时执行,也就是多个任务在同一时刻同时执行,必须多核CPU才能实现
//并发:单核+交替执行,也就是多个任务在同一时间段内交替执行,单核CPU也能实现
}
}
调用start()的运行结果:
调用run()的运行结果:
线程的创建
继承Thread类,重写run方法
//创建一个类。继承自Thread,重写run
class MyThread1 extends Thread{
@Override
public void run() {
while(true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
}
}
public class Demo{
public static void main(String[] args) throws InterruptedException {
// TODO 自动生成的方法存根
Thread thread=new MyThread1();
thread.start();
while(true) {
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
实现Runnable接口,重写run方法
package thread;
//程序的入口:main
//线程的入口:run
//创建一个类,实现Runnable接口,重写run
class MyRunnable implements Runnable{
@Override
public void run() {
while(true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Demo {
public static void main(String[] args) throws InterruptedException {
// TODO 自动生成的方法存根
//Java就愿意写向上转型,能向上转型就向上转型
Runnable runnable=new MyRunnable();
//Runnable表示的是一个“可以运行的任务”,这个任务可以交给线程来负责执行,也可以交给其他的实体来执行
Thread thread=new Thread(runnable);
thread.start();//通过这个操作,调用系统API来完成创建线程的工作
while(true) {
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
/*
* 封装本质上是让调用者不用了解类实现的细节,降低了学习和使用的成本
* 多态则是在封装的基础上更进一步,不需要知道当前是啥类,更不必说类的实现细节了
* */
使用直接继承Thread类和实现Runnable接口的写法之间的区别:解耦合。
把执行的任务本身提取出来,就是解耦合,此时就可以随时把代码改成使用其他方式来执行这个任务。
继承Thread类,使用匿名内部类
package thread;
public class Demo {
public static void main(String[] args) {
// TODO 自动生成的方法存根
//匿名内部类的写法
//先创建一个新的类,类名不知道,只知道这个类是Thread的子类,同时又把这个子类的实例给创建出来了
//new Thread () { ...} → 自动继承 Thread
//这个类是一次性的
Thread thread=new Thread() {
@Override
public void run() {
while(true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
thread.start();
while(true) {
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
}
}
实现Runnable接口,使用匿名内部类
package thread;
public class Demo {
public static void main(String[] args) {
// TODO 自动生成的方法存根
/*Runnable runnable=new Runnable() {
@Override
public void run() {
// TODO 自动生成的方法存根
while(true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
Thread thread=new Thread(runnable);
thread.start();*/
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
// TODO 自动生成的方法存根
while(true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
thread.start();
while(true) {
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
}
}
基于lambda表达式
package thread;
public class Demo4 {
public static void main(String[] args) {
// TODO 自动生成的方法存根
//lambda表达式,本质上是一个匿名函数,主要用来实现“回调函数”的作用
//匿名函数:没有名字的函数,用一次就完了
Thread thread=new Thread(()->{
while(true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.setDaemon(true);//设置为后台线程
thread.start();
while(true) {
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
线程的优缺点
优点
- 可以将耗时任务放到后台执行,提升程序响应速度。
- 充分利用 CPU 空闲时间,提高 CPU 利用率。
- 充分发挥多核处理器的性能,实现并行执行。
- 同一进程内的线程通信简单,无需复杂的进程间通信机制。
缺点
- 线程过多会导致频繁上下文切换,消耗 CPU 性能。
- 线程需要独立的栈空间,过多线程会增加内存开销。
- 多线程并发容易引发线程安全问题、死锁问题,调试难度更高。
- 一个线程异常可能影响整个进程的运行稳定性。
全文总结
- 进程是资源分配的基本单位,线程是 CPU 调度和执行的基本单位。
- 线程依附于进程存在,共享进程资源,开销远小于进程。
- Java 线程通过Thread类实现,start()才是开启线程的正确方式,start () 创建线程,run () 定义任务。
🔗 系列文章导航
本篇是「Java并发编程系列」的连载内容,点击链接查看完整系列:
🔹 上一篇:Java并发编程(一)|进程:并发编程的“幕后大管家”
🔹 下一篇:Java并发编程(三)|线程API:如何让你的线程“听话执行”👉专栏合集:Java并发编程专栏合集
更多推荐
所有评论(0)