进程与线程
进程与线程
进程
- 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至CPU,数据加载至内存。在指定运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理IO的。
- 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就启动了一个进程。
- 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器等),也有的程序只能启动一个实例进程(例如网易云音乐、360安全卫士等)。
线程
- 一个进程之内可以分为一到多个线程。
- 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行(最终执行指令的还是进程)。
- Java中,线程作为最小调度单位,进程作为资源分配的最小单位。在 windows 中进程是不活动的,只是作为线程的容器。
二者对比
- 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集。
- 进程拥有共享的资源,如内存空间等,供其内部的线程共享。
- 进程间通信较为复杂
- 同一台计算机的进程通信称为 IPC (Inter-process communication)。
- 不同计算机直接的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP。
- 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量。
- 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低。
并行与并发
单核CPU下,线程实际还是 串行执行
的。操作系统中有一个组件叫任务调度器,将CPU的时间片(windows下时间片最小约为15毫秒)分给不同的程序使用,只是由于CPU在线程间(时间片很短)的切换非常快,人类感觉是 同时运行的
。总结一句话: 微观串行,宏观并行
。
一般会将这种 线程轮流使用 CPU
的做法称为并发(concurrent)。
引用 Rob Pike 的一段描述:
- 并发(concurrent)是同一时间对应(dealing with)多件事情的能力
- 并行(parallel)是同一时间动手做(doing)多件事情的能力
生活例子:
- 家庭主妇做饭、打扫卫生、洗衣服,她一个人轮流交替做多件事,这时就是并发
- 家庭主妇雇了一个保姆,她们一起做这些事,这时既有并发、也有并行(这时会产生竞争,例如洗衣机只有一台,一个人用洗衣机时,另一个人只能等待)
- 家庭妇女雇了三个保姆,一个专门做饭、一个专门打扫卫生、一个专门洗衣服,互不干扰,这时就是并行
应用
应用之异步调用
以调用方角度来讲,如果
- 需要等待结果返回,才能继续运行就是同步
- 不需要等待结果返回,就能继续运行就是异步
设计
多线程可以让方法执行变为异步的。
比如说读取磁盘文件时,假设读取操作花费了5秒钟,如果没有线程调度机制,这5秒钟CPU什么都做不了,其他代码只能等待。
简单异步例子:
代码语言:javascript复制Thread t1 = new Thread() {
@Override
public void run() {
try {
Thread.sleep(1000);
log.debug("t1");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t1.setName("t1");
t1.start();
log.debug("main");
运行结果
代码语言:javascript复制13:51:29.131 [main] DEBUG io.ray.threadstudy.test.Test01 - main
13:51:30.130 [t1] DEBUG io.ray.threadstudy.test.Test01 - t1
结果说明
main 线程不需要等待 t1 线程执行完才输出,此时程序是异步进行的。
结论
- 比如在项目中,视频文件需要转换格式等操作比较费时,这时开一个新线程处理视频转换,避免阻塞主线程
- tomcat 的异步servlet 也是类似的目的,让用户线程处理耗时较长的操作,避免阻塞tomcat 的工作线程
- ui 程序中,开线程进行其他操作,避免阻塞 ui 线程
应用之提高效率
充分利用多核 CPU 的优势,提高运行效率。想象下面的场景,执行3个计算,最后将计算结果汇总
代码语言:javascript复制计算 1 花费 10ms
计算 2 花费 11ms
计算 3 花费 9ms
汇总需要 1ms
- 如果是串行执行,那么总共花费的时间是
10 11 9 1 = 31ms
- 如果是四核CPU,各个核心分别执行计算,那么此时的线程是并行的,花费时间只取决于最长的那个线程运行的时间,即
11ms
最后加上汇总时间只会花费12ms
注意: 需要多核 CPU 才能提高效率,单核仍然是轮流执行
结论
- 单核 CPU 下,多线程不能实际提高程序运行效率,只是为了能够在不同的任务之间切换,不同线程轮流使用 CPU,不至于一个线程总占用CPU,别的线程无法干活。
- 多核 CPU 可以并行跑多个线程,但能否提高程序运行效率还是要分情况的
- 有些任务,经过精心设计,将任务拆分,并行执行,当然可以提高程序的运行效率。但不是所有计算任务都能拆分(参考【阿姆达尔定律】)
- 也不是所有任务都需要拆分,任务的目的如果不同,谈拆分和效率没意义
- IO 操作不占用 CPU,只是我们一般拷贝文件使用的是【阻塞IO】,这时相当于线程虽然不同 CPU,但需要一直等待IO结束,没能充分利用线程。所以才有了后面的【非阻塞IO】和【异步IO】优化
Java 线程
创建和运行线程
每个Java程序启动的时候,默认都创建了一个主线程(main方法),如果想在主线程外创建线程,可以使用如下方法。
方法一:直接使用Thread
代码语言:javascript复制@Slf4j(topic = "io.ray")
public class Test02 {
public static void main(String[] args) {
// 创建线程对象
// 构造方法的参数是给线程指定名称(推荐)
Thread t1 = new Thread("t1") {
// run 方法内部实现了要执行的任务
@Override
public void run() {
// 要执行的任务
log.debug("t1 running");
}
};
// 启动线程
t1.start();
log.debug("main running");
}
}
输出
代码语言:javascript复制13:27:35.538 [main] DEBUG io.ray - main running
13:27:35.542 [t1] DEBUG io.ray - t1 running
方法二:使用 Runnable 配合 Thread
把【Thread 线程】 和 【Runnable 任务】分开,更为灵活,推荐这种写法。
代码语言:javascript复制@Slf4j(topic = "io.ray")
public class Test03 {
public static void main(String[] args) {
// 创建任务对象
Runnable r1 = new Runnable() {
// run 方法内部实现了要执行的任务
@Override
public void run() {
// 要执行的任务
log.debug("r1 running");
}
};
// 创建线程对象
Thread t1 = new Thread(r1, "r1");
// 启动线程
t1.start();
log.debug("main running");
}
}
结果
代码语言:javascript复制13:37:07.419 [main] DEBUG io.ray - main running
13:37:07.419 [r1] DEBUG io.ray - r1 running
Java 8 以后可以使用 lambda 精简代码
代码语言:javascript复制// ------- lambda - 1
Runnable r2 = () -> { log.debug("r2 running"); };
Thread t2 = new Thread(r2, "r2");
t2.start();
// ------- lambda - 2
Thread t3 = new Thread(() -> { log.debug("r3 running"); }, "r3");
t3.start();
源码分析
当创建 Thread 指定参数 Runnable时,会调用其 init 方法
代码语言:javascript复制public Thread(Runnable target) {
init(null, target, "Thread-" nextThreadNum(), 0);
}
// 继续往下寻找
private void init(ThreadGroup g, Runnable target, String name,
long stackSize) {
init(g, target, name, stackSize, null, true);
}
// 继续往下寻找
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name;
Thread parent = currentThread();
SecurityManager security = System.getSecurityManager();
if (g == null) {
/* Determine if it's an applet or not */
/* If there is a security manager, ask the security manager
what to do. */
if (security != null) {
g = security.getThreadGroup();
}
/* If the security doesn't have a strong opinion of the matter
use the parent thread group. */
if (g == null) {
g = parent.getThreadGroup();
}
}
/* checkAccess regardless of whether or not threadgroup is
explicitly passed in. */
g.checkAccess();
/*
* Do we have the required permissions?
*/
if (security != null) {
if (isCCLOverridden(getClass())) {
security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
}
}
g.addUnstarted();
this.group = g;
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
this.inheritedAccessControlContext =
acc != null ? acc : AccessController.getContext();
// 将参数的runnable对象赋给了成员变量target
this.target = target;
setPriority(priority);
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;
/* Set thread ID */
tid = nextThreadID();
}
// 可以看到,target 成员变量实际在run方法中用到
@Override
public void run() {
// 如果 target 不为空的时候,调用的是 target 中的 run 方法
// 反之,调用的是自己重写的 run 方法
if (target != null) {
target.run();
}
}
小结
- 用 Runnable 更容易与线程池等高级 API 配合
- 用 Runnable 让任务类脱离了 Thread 继承体系,更灵活
方法三: 使用 FutureTask 配合 Thread
FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况
代码语言:javascript复制@Slf4j(topic = "io.ray")
public class Test04 {
public static void main(String[] args) {
// 创建任务对象
FutureTask<Integer> task3 = new FutureTask<>(() -> {
log.debug("hello");
return 100;
});
// 参数1 是任务对象;参数2 是线程名称,推荐
new Thread(task3, "t3").start();
try {
// 主线程阻塞,同步等待 task 执行完毕的结果
Integer result = task3.get();
log.debug("结果是:[{}]", result);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
结果
代码语言:javascript复制13:47:22.044 [t3] DEBUG io.ray - hello
13:47:22.049 [main] DEBUG io.ray - 结果是:[100]
原理之线程运行
栈和栈帧
Java Virtual Machine Stacks (Java虚拟机栈)
JVM 由堆、栈、方法区所组成,其中栈内存是给谁用的呢?
其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存。
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
- 每个先只能有一个活动栈帧,对应着当前正在执行的那个方法
线程上下文切换(Thread Context Switch)
以下的原因会导致CPU不再执行当前的线程,转而执行另一个线程的代码
- 线程的CPU时间片用完
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了
sleep
、yield
、wait
、join
、park
、synchronized
、lock
等方法
当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java中对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条 JVM 指令的执行地址,是线程私有的。
- 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
- Context Switch 频繁发生会影响性能
常见方法
方法名 | static | 功能说明 | 注意 |
---|---|---|---|
start() | 启动一个新线程,在新的线程运行 run 方法中的代码 | start 方法只是让线程进入就绪状态,里面的代码不一定会立刻执行( CPU 的时间片还没有分配给它)。每个线程对象的 start 方法只能调用一次,如果调用了多次会出现 IllegalThreadStateException 异常。 | |
run() | 新线程启动后会调用的方法 | 如果在构造 Thread 对象时传递了 Runnable 参数,则线程启动后会调用 Runnable 中的 run 方法,否则默认不执行任何操作。但可以创建 Thread 的子类对象,来覆盖默认行为。 | |
join() | 等待线程运行结束 | ||
join(long n) | 等待线程运行结束,最多等待 n 毫秒 | ||
getId() | 获取线程长整型的 id | id 唯一 | |
getName() | 获取线程名 | ||
setName(String) | 修改线程名 | ||
getPriority() | 获取线程优先级 | ||
setPriority(int) | 修改线程优先级 | Java 中规定线程优先级是 1~10 的整数,较大的优先级能提高线程被CPU调度的概率。 | |
getState() | 获取线程状态 | Java API 中线程状态是用 6个 enum 表示,分别是:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED | |
isInterrupted() | 判断是否被打断 | 不会清除打断标记 | |
isAlive() | 线程是否存活(还没执行完毕) | ||
interrupt() | 打断线程 | 如果被打断的线程正在 sleep、wait、join 会导致被打断的线程抛出 InterruptedException ,并清除打断标记;如果打断的是正在运行的线程,则会设置打断标记;park 的线程被打断,也会设置打断标记。 | |
interrupted() | static | 判断当前线程是否被打断 | 会清除打断标记 |
currentThread() | static | 获取当前正在执行的线程 | |
sleep(long n) | static | 让当前执行的线程休眠 n 毫秒,休眠时会让出 CPU 的时间片给其他线程 | |
yield() | static | 提示线程调度器让出当前线程对 CPU 的使用 | 主要是为了测试和调试 |
start 与 run
- 直接调用 run 是在主线程中执行了 run,没有启动新的线程
- 使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码
@Slf4j
public class Test05 {
public static void main(String[] args) {
Test05 test05 = new Test05();
test05.testRun();
test05.testStart();
}
/**
* @Description: 调用run
* 方法的执行还是在 main 线程
**/
public void testRun() {
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.debug(Thread.currentThread().getName());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t1.run();
log.debug("do other things ..");
};
/**
* @Description: 调用start
* 方法的执行在 t2 线程
**/
public void testStart() {
Thread t2 = new Thread("t2") {
@Override
public void run() {
log.debug(Thread.currentThread().getName());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t2.start();
log.debug("do other things ..");
};
}
sleep 与 yield
sleep
- 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
- 其他线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出
InterruptedException
- 睡眠结束后的线程未必会立刻得到执行
- 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
yield
- 调用 yield 会让当前线程从 Running(新生状态) 进入 Runnable(就绪状态),然后调度执行其他线程
- 具体的实现依赖于操作系统的任务调度器
线程优先级
- 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
- 如果 CPU 比较忙,那么优先级高的线程会获得更多的时间片,但 CPU 闲时,优先级几乎没有作用
@Slf4j
public class Test06 {
public static void main(String[] args) {
Runnable task1 = new Runnable() {
int count = 0;
@Override
public void run() {
for (;;) {
log.debug(" ----> 1 {}", count );
}
}
};
Runnable task2 = new Runnable() {
int count = 0;
@Override
public void run() {
for (; ; ) {
log.debug(" ----> 2 {}", count );
}
}
};
Thread t1 = new Thread(task1, "t1");
Thread t2 = new Thread(task2, "t2");
// 在启动前设置优先级(可选,默认 NORM_PRIORITY = 5)
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();
}
}
join 方法详解
为什么需要 join?
join 的作用: 等待线程运行结束
下面的代码执行,打印 r 是什么?
代码语言:javascript复制@Slf4j(topic = "io.ray")
public class Test07 {
static int r = 0;
public static void main(String[] args) throws InterruptedException {
test();
}
private static void test() throws InterruptedException {
log.debug("开始 - main");
Thread t1 = new Thread(() -> {
log.debug("开始 - t1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("结束 - t1");
r = 10;
}, "t1");
t1.start();
// 使用 join,main 线程进入阻塞状态,等待 t1 线程的结束
//t1.join();
log.debug("r 的结果:[{}]", r);
log.debug("结束 - main");
}
}
结果(没使用 join)
代码语言:javascript复制07:38:27.364 [main] DEBUG io.ray - 开始 - main
07:38:27.531 [main] DEBUG io.ray - r 的结果:[0]
07:38:27.537 [main] DEBUG io.ray - 结束 - main
07:38:27.541 [t1] DEBUG io.ray - 开始 - t1
07:38:28.544 [t1] DEBUG io.ray - 结束 - t1
结果(使用 join)
代码语言:javascript复制07:38:57.085 [main] DEBUG io.ray - 开始 - main
07:38:57.205 [t1] DEBUG io.ray - 开始 - t1
07:38:58.205 [t1] DEBUG io.ray - 结束 - t1
07:38:58.205 [main] DEBUG io.ray - r 的结果:[10]
07:38:58.208 [main] DEBUG io.ray - 结束 - main
分析
- 因为主线程和线程 t1 是并行执行的,t1 线程需要 1秒之后才能算出
r=10
- 而主线程一开始就要打印 r 的结果,所以只能打印出
r=0
用 sleep 行不行 ?
解答:用 sleep 也可以,但时间不好把握,不知道 t1 线程要运行多久。
等待多个结果
代码语言:javascript复制@Slf4j(topic = "io.ray")
public class Test08 {
static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
test();
}
private static void test() throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
Thread.sleep(1000);
r1 = 10;
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1");
Thread t2 = new Thread(() -> {
try {
Thread.sleep(2000);
r2 = 20;
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t2");
t1.start();
t2.start();
long start = System.currentTimeMillis();
log.debug("join begin");
t1.join();
log.debug("t1 join end");
t2.join();
log.debug("t2 join end");
long end = System.currentTimeMillis();
log.debug("r1: {} r2: {} cost: {}", r1, r2, end-start);
}
}
结果
代码语言:javascript复制08:01:25.764 [main] DEBUG io.ray - join begin
08:01:26.763 [main] DEBUG io.ray - t1 join end
08:01:27.762 [main] DEBUG io.ray - t2 join end
08:01:27.762 [main] DEBUG io.ray - r1: 10 r2: 20 cost: 2000
分析执行流程
- 25秒的时候,join 开始
- 26秒的时候,t1 线程的 join 结束,等待了1秒
- 27秒的时候,t2 线程的 join 结束,等待了2秒,实际上真的等待2秒吗?结果是不需要的,因为 t1 、t2 线程是同时启动的,所以 t1 线程执行的时候, t2 线程也在执行,所以实际上是从启动线程开始算2秒,相对 t1 线程而言,只需要等待 1秒
- 因此总耗时是 2000 毫秒
有时效的 join
代码语言:javascript复制@Slf4j(topic = "io.ray")
public class Test09 {
static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
test();
}
public static void test() throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
Thread.sleep(3000);
r1 = 10;
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1");
long start = System.currentTimeMillis();
t1.start();
log.debug("join begin");
// 有时效的 join,等待2秒
t1.join(2000);
long end = System.currentTimeMillis();
log.debug("r1: {} cost: {}", r1, end-start);
}
}
结果
代码语言:javascript复制08:09:54.335 [main] DEBUG io.ray - join begin
08:09:56.340 [main] DEBUG io.ray - r1: 0 cost: 2007
分析(超过时效)
t1 线程需要等待 3秒后才给 r1 赋值,但 t1 的 join 方法等待如果超过 2秒,直接往下执行,不需要等待 t1 线程结束,此时输出 r1 的值为 0,因为还差 1秒线程 t1 才给 r1 赋值,所以程序总耗时是 2 秒。
分析(未超过时效)
如果未超过时效,以 t1 实际的调用完毕为准,程序会提前结束,不会说等超过时效才结束程序。
interrupt 方法详解
interrupt 打断阻塞
代码语言:javascript复制@Slf4j(topic = "io.ray")
public class Test10 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("sleep..");
try {
Thread.sleep(5000); // sleep、wait、join 方法同理
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1");
t1.start();
// 主线程要等待一下才执行打断,不然并发执行,主线程打断的不是 t1 的阻塞线程,而是正常运行的线程,打断标记返回 true,不是我们想要的结果
Thread.sleep(100);
// 打断线程
t1.interrupt();
log.debug("打断标记: {}", t1.isInterrupted());
}
}
结果
代码语言:javascript复制08:32:04.102 [t1] DEBUG io.ray - sleep..
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at io.ray.threadstudy.test.Test10.lambda$main$0(Test10.java:19)
at java.lang.Thread.run(Thread.java:748)
08:32:04.201 [main] DEBUG io.ray - 打断标记: false
分析
对于 sleep
、wait
、join
这种方法被打断后,以异常的方式表示被打断了,并且会抹去打断标记,设置为 false,将来这个打断标记可以用于被打断后,程序是否继续运行,还是结束程序。
sequenceDiagram
participant t1 as 线程一
打断正常运行的线程
打断正常运行的线程,不会清空打断状态
代码语言:javascript复制@Slf4j(topic = "io.ray")
public class Test11 {
public static void main(String[] args) throws InterruptedException {
test2();
}
private static void test2() throws InterruptedException {
Thread t2 = new Thread(() -> {
while (true) {
Thread current = Thread.currentThread();
boolean interrupted = current.isInterrupted();
if (interrupted) {
log.debug("打断状态:{}", interrupted);
break;
}
}
}, "t2");
t2.start();
Thread.sleep(500);
t2.interrupt();
}
}
输出
代码语言:javascript复制16:42:58.213 [t2] DEBUG io.ray - 打断状态:true
不推荐的方法
还有一些不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁
方法名 | static | 功能说明 |
---|---|---|
stop() | 停止线程运行 | |
suspend() | 挂起(暂停)线程运行 | |
resume() | 恢复线程运行 |
主线程与守护线程
默认情况下,Java进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其他非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
代码语言:javascript复制@Slf4j(topic = "io.ray")
public class Test12 {
public static void main(String[] args) {
log.debug("开始运行...");
Thread t1 = new Thread(() -> {
log.debug("t1 开始运行");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("t1 结束运行");
}, "t1");
// 设置该线程为守护线程,必须在启动前设置
t1.setDaemon(true);
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("运行结束...");
}
}
输出
代码语言:javascript复制16:51:58.073 [main] DEBUG io.ray - 开始运行...
16:51:58.223 [t1] DEBUG io.ray - t1 开始运行
16:51:59.221 [main] DEBUG io.ray - 运行结束...
t1 线程是守护线程,所以main线程结束后,程序直接结束了,不会等待 t1 线程执行完毕
注意:
- 垃圾回收器线程就是一种守护线程
- Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到的 shutdown 命令后,不会等待它们处理完当前请求
五种状态
五种状态是以操作系统层面来描述的
- 初始状态:仅是在语言层面创建了线程对象,还未与操作系统线程关联
- 可运行状态(就绪状态):指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
- 运行状态:指获取了 CPU 时间片运行中的状态,当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换
- 阻塞状态:如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入【阻塞状态】;等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】;与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们
- 终止状态:表示线程已经执行完毕,生命周期已经结束,不会再转换成其他状态
六种状态
六种状态是从 Java API 层面来描述的
根据 Thread.State
枚举,分为六种状态
- NEW:线程刚被创建,但是还没有调用
start()
方法 - RUNNABLE:当调用了
start()
方法之后,注意,Java API 层面的RUNNABLE
状态涵盖了操作系统层面的【可运行状态】、【运行状态】、【阻塞状态】 - BLOCKED、WAITING、TIMED_WAITING:都是 Java API 层面对【阻塞状态】的细分
- TERMINATED:当线程代码运行结束
共享模型之管程
两个线程对初始值为0的静态变量一个做自增,一个做自减,各做5000次,结果是0吗?
代码语言:javascript复制@Slf4j(topic = "io.ray")
public class Test13 {
// 静态变量
static int counter = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i ) {
counter ;
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i ) {
counter--;
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("counter = [{}]", counter);
}
}
输出
代码语言:javascript复制17:11:18.889 [main] DEBUG io.ray - counter = [-393]
问题分析
以上的结果可能是正数、负数、零。 为什么呢?因为 Java 中对静态变量的自增、自减并不是原子操作,要彻底理解,必须从字节码来进行分析。