JUC系列:(二)线程
创建线程
方法一:继承于Thread类
- 继承Thread类,重写run方法
- 创建MyThread对象,并调用start方法
1 | class MyThread extends Thread { |
方法二:实现Runnable接口
- 实现Runnable接口,实现run方法
- 创建MyRunnable对象,作为Thread构造方法的参数
- 创建Thread对象,调用start方法
1 | class MyRunnable implements Runnable { |
开发中优先选择Runnable接口的方式,因为:
- 没有单继承的局限性
- 可以实现多个线程共用一个Runnable,共享数据
方法三:实现Callable接口
Callable提供带返回值的call方法,需要和Future实现类连用,后续章节会讲到
1 | class MyCallable implements Callable<String> { |
Thread和Runnable的关系
Runnable作为参数,构造线程源码
1 | // 先调用它 |
设计模式(代理模式)
Thread是代理类,用户自定义核心功能的实现,辅助功能交由Thread类来实现
1 | // 如果实现了runnable接口,那么target不会为null,最终调用实现的run方法 |
设计模式(策略模式)
Runnable接口使用了策略模式,将执行逻辑(run方法)和程序的执行单元(start0方法)分离出来。使得用户可以定义自己的程序处理逻辑,更符合面向对象的思想。
为什么线程的启动不直接使用run()而必须使用start():
- run的调用是在本地方法start0中
- start0方法依赖于不同的操作系统实现
- JNI(Java Native Interface),Java本地接口
Thread的构造方法
- 创建Thread对象,默认有一个线程名(Thread-0,Thread-1,Thread-2)
- 如果构造线程对象时,未传入ThreadGroup,Thread会默认使用父线程的ThreadGroup
- stackSize可以提高线程栈的深度,放更多栈帧,会减少能创建线程的数目
- stackSize默认是0,代表着被忽略,该参数会被JNI函数调用
- JVM一旦启动,虚拟机栈的大小已经确定了
- 但是如果你创建Thread的时候传了stackSize,该参数会被JNI函数去使用
- 某些平台可能会失效,可以通过
-Xss10m
设置
线程的方法
Thread类的API
静态方法:
方法 | 说明 |
---|---|
static Thread currentThread() | 获取当前线程对象 |
static void sleep(long time) | 让当前线程休眠多少毫秒再继续执行Thread.sleep(0) : 让操作系统立刻重新进行一次 CPU 竞争 |
static native void yield() | 提示线程调度器,让出当前线程对CPU的使用 |
static boolean interrupted() | 判断当前线程是否被打断,清除打断标记 |
实例方法:
方法 | 说明 |
---|---|
void start() | 启动一个新线程,JVM会调用此线程的run方法 |
void run() | 线程启动后,调用该方法 |
void setName(String name) | 设置线程名字 |
void getName() | 获取当前线程名字 线程存在默认名称:子线程是Thread-索引,主线程是main |
final int getPriority() | 获取此线程的优先级 |
final void setPriority(int priority) | 设置此线程的优先级,常用1,5,10 |
void interrupt() | 中断这个线程,异常处理机制(不清除打断标记) |
boolean isInterrupted() | 判断当前线程是否被打断(不清除打断标记) |
final void join() | 等待这个线程结束 |
final void join(long millis) | 等待这个线程结束,最多millis毫秒,0意味着永远等待 |
final native boolean isAlive() | 判断线程是否存活(还没有运行完毕) |
final void setDaemon(boolean on) | 将此线程标记为守护线程或用户线程 |
sleep与yield方法
Thread.sleep:
- 当前线程从Running进入Timed Waiting状态
- 其他线程可以使用interrupt方法打断它的睡眠,此时sleep方法抛出InterruptedException
- 睡眠结束后,未必立即执行,需要抢占CPU
- 锁资源不会释放
Thread.yield:
- 提示线程调度器,让出当前线程对CPU的使用
- 锁资源不会释放
join方法
作用:等待这个线程结束
原理:调用者轮询检查线程alive
1 | public final synchronized void join(long millis) throws InterruptedException { |
setDaemon方法
作用:将此线程标记为守护线程
- 只要其他非守护线程(用户线程)结束了,守护线程会强制结束
常见的守护线程:
- 垃圾回收线程
- Tomcat中的Acceptor和Poller线程。所以Tomcat接收到shutdown命令后,不会等待它们处理完当前请求
interrupt方法
方法 | 说明 |
---|---|
void interrupt() | 中断这个线程,清空打断标记,异常处理机制 |
- sleep、wait、join方法都会让线程进入阻塞状态
- 此时打断线程,会清空打断状态(interrupted = false)
1 | Thread t1 = new Thread(()->{ |
- 打断正常线程,不会清空打断状态(interrupted = true)
1 | Thread t2 = new Thread(()->{ |
打断park方法
park方法作用类似于sleep方法,但是打断它,不会清空打断状态
1 | Thread t1 = new Thread(() -> { |
优雅的终止线程
thread.stop()
:会真正的杀死线程,其资源将永远不能释放,因此需要更优雅的方式
- 使用中断
1 | class MyThread { |
线程原理
运行机制
虚拟机栈:
- 每个线程启动后,虚拟机就会为其分配一块栈内存
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用
- 每个线程只有一个活动栈帧
线程上下文切换:
CPU不再执行当前线程,转而执行另一个线程
线程的 CPU 时间片用完
垃圾回收
有更高优先级的线程需要运行
线程自己调用了sleep,yield,wait,join,park等方法
程序计数器:
- 线程私有的,记住下一条JVM指令的地址
上下文切换:
当上下文切换发生时,由操作系统保存当前线程的状态,并恢复另一个线程的状态
- 包括:程序计数器,虚拟机栈中每个栈帧的信息(如局部变量,操作数栈,返回地址等等)
JVM 规范并没有限定线程模型,以HotSpot为例
Java 的线程是内核级线程(1:1线程模型),每个Java 线程都映射到一个操作系统原生线程,需要消耗一定的内核资源
线程的调度是在内核态运行的,而线程中的代码是在用户态运行,所以线程切换(状态改变)会导致用户与内核态转换进行系统调用,这是非常消耗性能
线程调度
方式有两种:
- 协同式线程调度:线程做完任务才通知系统切换到其他线程
- 抢占式线程调度(Java选择):线程的执行时间由系统分配
线程状态
1 | public enum State { |
查看线程
Windows:
tasklist
:查看进程taskkill
:杀死进程
Linux:
ps -ef
:查看所有进程ps -fT -p <PID>
:查看某个进程的线程kill
:杀死进程top + 大写H
:显示所有进程 + 开启显示线程top -H -p <PID>
:查看某个进程的所有线程
Java:
jps
:查看所有Java进程jstack <PID>
:查看某个Java进程的所有线程jconsole
查看所有进程中线程的运行情况(图形界面)
- 标题: JUC系列:(二)线程
- 作者: 布鸽不鸽
- 创建于 : 2024-04-10 20:34:10
- 更新于 : 2024-01-10 14:58:18
- 链接: https://xuedongyun.cn//post/19310/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论