线程运行原理
栈和栈帧
- 每个栈由多个栈帧组成,对应着每次方法调用所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
线程上下文切换(Thread Context Switch)
因为以下一些原因导致cpu不再执行当前的线程,转而执行另一个线程的代码
- 线程的cpu时间片用完
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了sleep、yield、wait、join、park、synchronized、lock等方法程序
当Context Switch发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,java中就是用程序计数器完成,它记住下一条jvm指令的执行地址
常见方法
start和run
- 直接调用run方法是主线程中执行了run,没有启动新的线程
- 使用start是启动新的线程,通过新的线程间接执行run中的代码
sleep
- 调用sleep会让当前线程从Running进入Timed Waiting状态
- 其他线程可以使用interrupt方法打断正在睡眠的线程,这时sleep方法会抛出IntertuptedException
- 睡眠结束后的线程未必会立刻得到执行(要等任务调度器分配时间片)
- 建议使用TimeUnit的sleep代替Thread的sleep来获得更好的可读性
yield
- 调用yield会让当前线程从Running进入Runnable就绪状态,然后调度执行其他线程
- 具体的实现依赖于操作系统的任务调度器
sleep和yield区别
- 就绪状态还是有机会让任务调度器调度的,但是time wating不会
- sleep有一段休眠时间,yield没有参数,也就是没有等待时间
线程优先级
- 线程优先级会提示调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽视它
- 如果cpu比较忙,那么优先级高的线程会获得更多的时间片,但cpu闲时,优先级几乎没有作用
- setPriority()设置线程优先级
join
- 等待线程运行结束,谁调用它就等待谁
interrupt
-
打断处于阻塞的线程(sleep、wait、join的线程)
- 打断这些线程会清空打断状态
-
打断正常运行的线程,不会清空打断状态
Thread t1 = new Thread(()->{ while (true){ boolean interrupted = Thread.currentThread().isInterrupted(); if (interrupted){ log.info("被打断了,优雅的退出循环"); break; } } },"t1"); t1.start(); TimeUnit.SECONDS.sleep(1); log.info("interrupt..."); t1.interrupt();
-
打断park线程,不会清空打断状态
守护线程
只要等其他的非守护线程运行结束了,即使守护进程的代码没有执行完,也会强制结束
用setDaemon()设置守护进程,垃圾回收器就是一种守护进程,java应用结束了,垃圾回收线程也会停止
线程状态
从操作系统看,有五种状态:
- 【初始状态】:仅在语言层面创建了线程对象,还未与操作系统线程关联
- 【可运行状态】:(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由CPU调度执行
- 【运行状态】:指获取了CPU时间片运行中的状态
- 当CPU时间片用完,会从运行状态转换为可运行状态,会导致线程的上下文切换
- 【阻塞状态】:
- 如果调用了阻塞API,如BIO读取文件,这时线程实际不会用到CPU,会导致线程上下文切换,进入阻塞状态
- 等BIO操作完毕,会由操作系统唤醒阻塞的线程,进入可运行状态,等待下一次CPU分配时间片
- 与可运行状态的区别是,对阻塞状态的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们
- 【终止状态】:表示线程已经执行完毕,生命周期已经结束
从Java API层面,有六种状态:
- 【NEW】:线程刚刚被创建,还没有调用start()方法
- 【RUNNABLE】:调用了start方法后,这个状态覆盖了操作系统层面的可运行状态、运行状态、阻塞状态(由于BIO造成的线程阻塞,在java中无法区分,仍然认为是可运行的)
- 【BLOCKED】【WAITING】【TIMED_WAITING】:都是Java API层面对阻塞状态的细分
- 【TERMINATED】:当线程代码运行结束