文章目录
- 线程概述
- 进程/线程
- 多线程的作用
- 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
状态…