文章目录
- 线程概述
- 进程/线程
- 多线程的作用
- JVM关于线程资源的规范
- 关于Java程序的运行原理
- 并发与并行
- 并发(concurrency)
- 并行(parallellism)
- 并发编程与并行编程
- 线程的调度策略
- 分时调度模型
- 抢占式调度模型
- 创建线程
- 线程类分析入门
- 实现线程的第一种方式
- 实现线程的第二种方式
- 线程的生命周期
- 线程的生命周期概述
- 线程生命周期间的关系(含UML图)
线程概述
进程/线程
- 进程是指操作系统中的一段程序, 它是一个正在执行的程序实例, 具有独立的内存空间和系统资源, 所以进程之间资源不共享, 如文件, 网络端口等, 在计算机运行时, 一般是先创建进程, 后创建线程, 一个进程通常可以包含多个线程
- 线程是指的是进程中的一个执行单元, 是进程的一部分, 负责在进程中执行代码, 每一个线程都有自己的栈和程序计数器, 并且可以共享进程的资源, 多个线程可以在同一个时刻执行不同的操作, 从而提高程序的执行效率, 线程与线程之间的资源并不是完全共享的, 下面会详细介绍…
大白话总结:
- 一个应用程序就是一个进程
- 一个进程里面有多个线程执行任务
多线程的作用
最重要的就是提高处理问题的效率, 能够使CPU在处理一个任务时同时处理多个线程, 这样可以充分利用CPU的资源, 提高CPU的资源利用率
JVM关于线程资源的规范
我们用下面的一张图来说明

在同一个进程的多个线程之间, Heap(堆), Method Area(方法区)资源是共享的, Java Virtual Machine Stack(Java虚拟机栈), Native Method Stack(本地方法栈), The pc Register(程序计数器)这些都是不能够共享的
所以就存在线程安全的问题
比如对于变量来说, 局部变量存在Java虚拟机栈, 所以不存在线程安全问题, 但是实例变量, 静态变量都存在于堆中, 就会存在线程安全的问题
关于Java程序的运行原理
- 当JVM启动的时候, JVM会自动开启一个主线程(main-thread), 然后去调用main方法
所以main方法都是在主线程中执行的 - 除了主线程之外, 还会启动一个垃圾回收线程(GC), 因此启动JVM, 至少启动了两个线程
- 除上述线程之外, 程序员可以手动创建其他的线程并启动
并发与并行
早期的人们使用的单核的CPU, 那是不是不能同时运行多个程序呢? 答案是否定的, 下面的关于并发与并行就可以解释这个问题
并发(concurrency)
- 在使用单核心CPU时候, 微观层面一个时间点只能执行一个指令, 但多个指令被快速的轮换执行,使得在宏观上具有多个指令同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干端,使多个指令快速交替的执行
下面的这张图就可以很好的说明这个问题

横轴对应的是时间, 纵轴对应的是ABC三个程序, 围观层面, 某一时间点只能执行一个程序, 但是通过CPU高速的在ABC三个程序中进行调度切换, 让我们宏观上看起来是三个程序同时执行的, 这种情况就是并发
并行(parallellism)
- 这种情况针对的就是多核心CPU的情况, 此时在一个时间点, 微观层面上也可以通过多个核心同时多个程序来达到真正意义上的"同时执行"

并发编程与并行编程
- 在多核心CPU资源紧缺(或者是单核心CPU)的前提下, 如果开启了多个线程, 但是只有一个CPU核心可以提供资源, 那么这些线程就会抢夺CPU的时间片, 竞争执行机会, 这就是通过 并发 的方式实现多线程
- 当多核心CPU资源比较充足的情况下, 此时有多个CPU核心可以来提供资源, 此时一个进程中的线程就会被分配到多个CPU核心上同时执行, 这就是通过 并行 的方式实现多线程
- 不管并发还是并行,都提高了程序对CPU资源的利用率,最大限度地利用CPU资源,而我们使用多线程的目的就是为了提高CPU资源的利用率
那么Java实现多线程的方式是并发还是并行呢?
- 至于Java多线程实现的是并发还是并行?上面所说,所写多线程可能被分配到一个CPU内核中执行,也可能被分配到不同CPU执行,分配过程是操作系统所为,不可人为控制。所以,如果有人问我我所写的多线程是并发还是并行的?我会说,都有可能
线程的调度策略
存在线程调度的原因是因为, 当多个线程被分配到同一个CPU核心执行的时候, 此时这些程序就会抢夺CPU的时间片从而或者执行权, 所以就存在执行的先后问题
分时调度模型
- 所有线程轮流使用CPU的执行权, 并且平均每个线程的占用时间, 也就是绝对平均
抢占式调度模型
- 让优先级高的线程以较大的概率优先获得CPU的执行权, 如果线程的优先级相同, 那么就会随机选择一个线程获得CPU的执行权, 而Java采用的就是抢占式调度模型
创建线程
线程类分析入门
这个是JDK17帮助文档的链接
JDK17帮助文档
通过API文档, 我们可以查询到我们需要的一些信息内容, 从而完成编程(这里不推荐查看中文的帮助文档)
构造方法

构造方法可以通过传入一个字符串然后指定该线程的名称
static Thread currentThread()

这是一个静态方法, 通过这个方法可以获取到当前线程的Thread信息, 其实有点类似于之前学的this
void setName(String name)

这是一个实例方法, 通过一个线程的引用调用之后可以设置当前线程的名称(其实每一个线程都有一个默认的名称)
String getName()

这是一个实例方法, 作用就是调用之后可以获得当前线程的名称
实现线程的第一种方式
执行逻辑如下
- 创建一个类继承
Thread(java.lang)包下的, 默认导入)类 - 重写
Thread中的run()方法(这个run()相当于每个线程的main函数) - 创建这个类的对象, 然后调用
start()方法, 开启线程
我们给一个代码的案例测试一下
package thread_demo.thread_demo01;public class Thread01 {public static void main(String[] args) {// 当执行main函数的时候, 系统自动的就开启了两个线程Thread t = new MyThread();t.start();// 主线程的内容for(int i = 0; i < 100; i++){System.out.println(Thread.currentThread().getName() + "---->" + i);}}
}/*** 创建线程的第一种方式是创建一个类继承Thread(所以这个类其实也就是一个线程)* 1. 创建一个类继承Thread这个线程类(所以此时这就是一个线程)* 2. 重写run方法(这就相当于每一个线程运行的入口)* 3. new一个线程对象然后调用start()方法启动一个线程*/
class MyThread extends Thread {@Overridepublic void run() {for(int i = 0; i < 100; i++){System.out.println(Thread.currentThread().getName() + "---->" + i);}}
}
执行结果如下

两个线程会无规则的交替执行
初学者好多不理解这个代码的执行逻辑, 首先我们在main中创建了一个线程对象, start()的作用就是开启一个线程, 然后就弹栈了, 此时JVM中就存在了两个线程同时执行(不含GC), 所有就会出现交替执行的情况
实现线程的第二种方式
执行逻辑如下
- 创建一个类实现
Runnable接口 - 重写其中的
run()方法 - 创建这个类的对象(匿名的也可以),
start()开启一个线程
给一个代码案例测试一下
package thread_demo.thread_demo02;public class Thread02 {public static void main(String[] args) {// 直接new一个对象调用创建一个t线程Runnable r = new MyThread();Thread t = new Thread(r, "test1");// 开启t线程t.start();// 利用匿名内部类的方式创建一个线程对象Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println(Thread.currentThread().getName() + "--->" + i);}}}, "test2");// 开启t1线程t1.start();// 直接就不接收变量直接开启一个线程new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println(Thread.currentThread().getName() + "--->" + i);}}}, "test3").start();// 最后在操作一下主线程Thread mainThread = Thread.currentThread();mainThread.setName("main");for (int i = 0; i < 100; i++) {System.out.println(Thread.currentThread().getName() + "--->" + i);}}
}/*** 创建线程的第二种方式是定义一个类去实现Runnable接口, 我们推荐使用这种方式开启多线程* 1. 定义一个类实现Runnable接口* 2. 重写run方法* 3. Thread t = new Thread(传入这个类的对象)* 4. t.start() 去开启一个线程*/
class MyThread implements Runnable{@Overridepublic void run() {for(int i = 0; i < 100; i++){System.out.println(Thread.currentThread().getName() + "--->" + i);}}
}
执行结果也是多个线程交替执行, 原理和第一种是一致的

线程的生命周期
线程的生命周期概述
线程的生命周期主要就是指的一个线程在不同时期的状态情况, 大致可以分
- 6种(JDK层面)
- 7种(我们平时常说的)
首先我们从JDK层面分析一下为什么是6种
我们查看一下帮助文档找到Thread.State这个枚举类型


NEW: 新建状态, 也就是执行start()之前的状态RUNNABLE: 可运行状态, 这个状态分为两个子状态
就绪状态 和 运行状态WAITING: 等待状态, 不限时间(比如等待用户输入)TIMED_WAITING: 超时等待状态, 限制时间(比如Thread.sleep(毫秒)BLOCKED: 阻塞状态, 比如遇到了一些关于锁的操作TERMINATED: 时亡状态,run()方法结束
线程生命周期间的关系(含UML图)
下面我们尝试绘制一个UML来描述一下各个状态之间的关系

图上说明的十分的详细了…, 我们再阐述一下
- 首先线程处于一个
NEW的新建状态, 然后通过start()创建一个线程, 此时线程处于RUNNABLE(就绪状态) - 位于
RUNNABLE(就绪状态)的线程拥有抢夺CPU时间片的能力, 当抢夺到了CPU时间片之后, 线程的run()方法就开始执行, 此时线程处于RUNNABLE(运行状态), 当抢夺到的CPU时间片用完了之后, 线程会再次进入到RUNNABLE(就绪状态), 下次执行时会接着上一次的执行, 这个过程中靠的是CPU的调度机制 - 当程序在
RUNNABLE(运行状态)遇到Thread.sleep(毫秒)类似的间隔的时候, 会进入到TIMED_WAITING超时等待状态, 此时会返还先前抢到的CPU时间片, 所有的等待休眠期度过之后, 会进入到RUNNABLE(就绪状态)等待下一次抢夺CPU时间片 - 当程序在
RUNNABLE(运行状态)遇到例如接收到需要等待用户输入的指令的时候, 就会进入到WAITING等待状态, 这个等待状态是没有时间的限制的(比如接收到输入为止) - 当
run()方法彻底执行结束之后, 就会触发到TERMINATED状态…
