关于进程与线程的知识,基本是面试中必问的知识点。它不仅涉及操作系统,而且在Java开发中也十分重要。所以,笔者在自己理解的前提下,把学习过程中涉及的知识进行总结。对于文章中的任何问题,欢迎在评论区进行留言!
文章目录
一、进程与线程
1.什么是进程
- 进程是程序的一次执行过程,可以看做运行中的程序,是动态的,存在生命周期。而程序是静态的,程序没被编译执行时只是呆呆地位于硬盘中。
- 它是资源分配的基本单位,系统在运行时为每个进程分配不同的内存区域。
2.线程
- cpu调度和执行的基本单位,一个进程内部的一条执行路径。通常一个进程不会同时只做一件事,比如Word进程,在打字的时候会进行拼写检查等,所以,这种在一个进程内部运行的多个子任务就称为线程。
- 这些线程相当于不同的执行路径,比如一个进程中,main函数是一个主线程,内部会调用不同的方法(子线程),是并行执行的
- 一个进程内的每个线程都有独立的虚拟机栈和程序计数器。
- 一个java程序至少有三个线程:main()、gc垃圾回收线程、异常处理线程
- 一个进程内的每个线程共享进程内的内存单元/内存地址空间,他们在同一堆中分配对象,可以访问相同的变量和对象。这里共享的区域包括:堆(使得线程通信更方便、高效,但这就带来了安全问题,所以需要同步)、方法区。
- 线程切换:指的是CPU保存现场,执行新线程;恢复线程,继续执行原线程的过程。
3.单核CPU和多核CPU
- 单核cpu: 假的多线程,因为同一时间单元内,也只能执行一个线程的任务。在使用单核CPU的电脑时,可以运行多个程序的原因是因为主频太高,给了我们一种多线程执行的幻觉,实际上任务是交替执行的。
- 多核cpu: 同一时间单元内,能执行多个线程的任务。
4.并发和并行
并发和并行是十分容易混淆的概念,这里我们从程序设计和CPU两个角度来理解并发和并行。
4.1程序设计的角度
- 并发:指程序的结构,即程序采用支持并发的设计,并发设计使得并发执行成为可能。并发执行就是一个程序还没结束,另一个程序就开始执行了,多个程序交替执行。
- 设计标准:多个操作可以在重叠的时间段内进行
- 优点:提高cpu的使用效率
- 并行:指的是程序的运行时的状态,即同时执行
- 判断标准:是否有超过一个“工作单位”在运行。如单线程无法达到并行。
4.2 CPU的角度
- 并行:多个cpu同时执行多个任务。类似多个人同时做不同的事情
- 并发:一个cpu(采用时间片策略切换不同的任务)“同时”执行多个任务
5.为什么需要多线程
- 提高cpu利用率。现在的cpu是多核心多线程的,写的程序不支持多线程的话,就不能充分利用cpu的资源,响应用户的时间就慢
- 改变程序结构。将冗长复杂的进程分为多个线程,独立运行,利于理解和修改
- java中,一个java程序是一个jvm进程,jvm进程用一个主线程来执行main()方法,main内部,可以启动多个线程来执行不同的操作。
6.何时需要多线程
- 程序需要同时执行两个以上的任务
- 程序需要实现一些需要等待的任务,如用户输入、文件读写操作。例子:新闻阅读APP页面中加载每一个条目左边的图片和右边的新闻标题是通过不同的线程实现的,这样在网速慢不能加载图片的时候,仍然可以加载下方的新闻的标题。
二、线程调度
1.调度策略
- 时间片轮转策略
- 抢占式策略:高优先级的线程抢占cpu
2.java中的调度方法
- 同优先级线程组成先进先出队列,使用时间片策略;
- 对于高优先级的线程,使用抢占式策略。
3.线程生命周期
- 新建
- 就绪
- 被start()后,进入线程就绪队列等待cpu时间片
- 运行
- 就绪状态的线程获得cpu资源时进入运行状态
- 阻塞
- 特殊情况下,被人为挂起或输输入输出操作时,让出cpu临时终止执行
- 死亡
- 完成全部工作或被提前终止或出现异常没处理
- 完成全部工作或被提前终止或出现异常没处理
三、创建线程
1.继承Thread类
实现步骤:
1. 创建一个继承自Thread的子类
2. 重写run方法,在内部声明线程要做的事情
3. 主方法中创建子类的对象,调用start()方法启动线程
代码如下(示例):
package com.li.java;
/**
* 创建线程方式1
*/
public class ThreadTest {
public static void main(String[] args) {
//3 创建子类的对象
MyThread t1 = new MyThread();
//4 调用start()
t1.start(); //当前开始执行;jvm调用当前线程的run()
//以下操作仍然在main线程中执行
System.out.println("hello");
for (int i = 0; i < 20; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i + "!!!!!");
}
}
}
}
//1 创建一个继承自thread的类
class MyThread extends Thread {
@Override
public void run() {
//2 重写run()、将此线程要做的事声明在此处
for (int i = 0; i < 20; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
运行结果:
在以上示例中,启动了两个线程,分别为主线程main和Thread-0,这两个线程交替执行,在主线程执行完毕后,Thread-0继续执行,直到所有非守护线程执行完成,程序结束。
2.实现Runnable接口
实现步骤:
1. 创建一个实现了Runnable接口的类;
2. 重写run(),在内部声明线程要做的事情;
3. 创建实现类的对象,将此对象作为参数传递到Thread类的构造器中,创建Thread对象;
4. 通过thread对象调用start()启动线程
示例代码如下:
package com.li.java;
/**
* 创建线程方式2:实现Runnable接口
* 1 创建一个实现了runnable接口的类
* 2 实现类去实现runnable中的抽象方法:run()
* 3 创建实现类的对象
* 4 将此对象作为参数传递到 Thread类中的构造器中 创建thread的对象
* 5 通过thread对象调用start() *
* @Author:lxc
* @Date:2021/03/04 20:34
*/
public class ThreadTest1 {
public static void main(String[] args) {
MThread mThread = new MThread();
Thread t1 = new Thread(mThread);
t1.start();
//再创建一个线程
Thread t2 = new Thread(mThread);
t2.start();
}
}
class MThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
if (i % 2 == 0)
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
运行结果:
3.实现Callable接口
实现步骤:
1. 创建实现Callable的实现类;
2. 重写call(),声明需要的操作;
3. 创建实现类的对象,将以上对象作为参数传递到FutureTask类的构造器中,创建FutureTask的对象;
4. 将FutureTask对象作为参数传递到Thread类的构造器中,创建Thread对象;
5. 调用start()启动线程
示例代码如下:
package com.li.java;
/**
* 实现callable方式创建线程,输出100以内的偶数,并在主线程中输出总和
*
* @Author:lxc
* @Date:2021/03/14 12:56
*/
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
//1
class NumThread implements Callable {
//2
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 1; i <= 20; i++) {
if (i % 2 == 0) {
System.out.println(i);
sum += i;
}
}
return sum;
}
}
public class ThreadTest2 {
public static void main(String[] args) {
//3
NumThread numThread = new NumThread();
//4
FutureTask futureTask = new FutureTask(numThread);
//5
new Thread(futureTask).start();
//在主线程中输出和
try {
//返回值为FutureTask构造器的参数(Callable实现类)重写的call方法的返回值
Object o = futureTask.get();
System.out.println("总和为:" + o);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
运行结果:
4.三种实现方式的比较
- 联系与相同点:前两种方式都需要重写run方法,并且类Thread也是实现了Runnable接口;而Callable封装成FutureTask,FutureTask实现RunnableFuture,RunnableFuture继承Runnable,所以Callable也算是一种Runnable,所以三种实现方式本质上都是Runnable实现;
- 相较于继承自Thread类的方式,后两种方式更好,能实现一种共享数据的概念。因为将实现类的对象作为参数传递给Thread类中的构造器中创建多个线程的时候,这时候实现类的对象自然就成为了几个线程的共享的数据;
- Callable方式更加强大:(1)其中的call方法具有返回值;(2)可以抛出异常,被外面的操作捕获,获取异常的信息;(3)Callable支持泛型