1.进程与线程
1.进程
程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的
当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器 等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等)
2.线程
一个进程之内可以分为一到多个线程。
一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行
Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。 在 windows 中进程是不活动的,只是作为线程的容器
3.二者对比
进程基本上相互独立的,而线程存在于进程内,是进程的一个子集进程拥有共享的资源,如内存空间等,供其内部的线程共享
进程间通信较为复杂
同一台计算机的进程通信称为 IPC(Inter-process communication)
不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低
2.并行与并发
单核 cpu 下,线程实际还是
的。操作系统中有一个组件叫做任务调度器,将 cpu 的时间片(windows
串行执行
下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于 cpu 在线程间(时间片很短)的切换非常快,人类感
同时运行的
微观串行,宏观并行
觉是 。总结为一句话就是: ,
一般会将这种 的做法称为并发, concurrent
线程轮流使用 CPU
CPU | 时间片 1 | 时间片 2 | 时间片 3 | 时间片 4 |
---|---|---|---|---|
core | 线程 1 | 线程 2 | 线程 3 | 线程 4 |
多核 cpu下,每个 都可以调度运行线程,这时候线程可以是并行的。
核(core)
CPU | 时间片 1 | 时间片 2 | 时间片 3 | 时间片 4 |
---|---|---|---|---|
core 1 | 线程 1 | 线程 1 | 线程 3 | 线程 3 |
core 2 | 线程 2 | 线程 4 | 线程 2 | 线程 4 |
引用 Rob Pike 的一段描述:
并发(concurrent)是同一时间应对(dealing with)多件事情的能力并行(parallel)是同一时间动手做(doing)多件事情的能力
例子
家庭主妇做饭、打扫卫生、给孩子喂奶,她一个人轮流交替做这多件事,这时就是并发
家庭主妇雇了个保姆,她们一起这些事,这时既有并发,也有并行(这时会产生竞争,例如锅只有一口,一 个人用锅时,另一个人就得等待)
雇了3个保姆,一个专做饭、一个专打扫卫生、一个专喂奶,互不干扰,这时是并行
3.应用
应用之异步调用(案例1)
以调用方角度来讲,如果
需要等待结果返回,才能继续运行就是同步不需要等待结果返回,就能继续运行就是异步
设计
多线程可以让方法执行变为异步的(即不要巴巴干等着)比如说读取磁盘文件时,假设读取操作花费了 5 秒钟,如果没有线程调度机制,这 5 秒 cpu 什么都做不了,其它代码都得暂停…
结论
比如在项目中,视频文件需要转换格式等操作比较费时,这时开一个新线程处理视频转换,避免阻塞主线程tomcat 的异步 servlet 也是类似的目的,让用户线程处理耗时较长的操作,避免阻塞 tomcat 的工作线程ui 程序中,开线程进行其他操作,避免阻塞 ui 线程
应用之提高效率(案例1)
充分利用多核 cpu 的优势,提高运行效率。想象下面的场景,执行 3 个计算,最后将计算结果汇总。
计 算 1 花 费 10 ms
计 算 2 花 费 11 ms
计 算 3 花 费 9 ms
汇总需要 1 ms
如果是串行执行,那么总共花费的时间是
10 + 11 + 9 + 1 = 31ms
但如果是四核 cpu,各个核心分别使用线程 1 执行计算 1,线程 2 执行计算 2,线程 3 执行计算 3,那么 3 个
11ms
12ms
注意
设计
结论
最后加上汇总时间只会花费
- 单核 cpu 下,多线程不能实际提高程序运行效率,只是为了能够在不同的任务之间切换,不同线程轮流使用
cpu ,不至于一个线程总占用 cpu,别的线程没法干活
b. 多核 cpu 可以并行跑多个线程,但能否提高程序运行效率还是要分情况的
有些任务,经过精心设计,将任务拆分,并行执行,当然可以提高程序的运行效率。但不是所有计算任 务都能拆分(参考后文的【阿姆达尔定律】)
也不是所有任务都需要拆分,任务的目的如果不同,谈拆分和效率没啥意义
c. O 操作不占用 cpu,只是我们一般拷贝文件使用的是【阻塞 IO】,这时相当于线程虽然不用 cpu,但需要一直等待 IO 结束,没能充分利用线程。所以才有后面的【非阻塞 IO】和【异步 IO】优化
4.Java 线程
1.本章内容
2.创建和运行线程
1.直接使用 Thread
// 创建线程对象
Thread t = new Thread() { public void run() {
// 要执行的任务
}
};
// 启动线程
t.start();
例如:
// 构造方法的参数是给线程指定名字,推荐
Thread t1 = new Thread("t1") {
@Override
// run 方法内实现了要执行的任务
public void run() { log.debug("hello");
}
;
t1.start();
输出
19:19:00 [t1] c.ThreadStarter - hello
2.使用 Runnable 配合 Thread
把【线程】和【任务】(要执行的代码)分开
Thread 代表线程
Runnable 可运行的任务(线程要执行的代码)
Runnable runnable = new Runnable() { public void run(){
// 要执行的任务
}
};
// 创建线程对象
Thread t = new Thread( runnable );
// 启动线程
t.start();
例如:
public class Thread2 {
public static void main(String[] args) {
Runnable runnable=new Runnable() {
@Override
public void run() {
//要执行的任务
System.out.println("thread is running..");
}
};
//创建线程对象
//任务对象以及线程名
//new Thread(task,线程名称)
Thread thread = new Thread(runnable);
//启动线程
thread.start();
}
}
输出
19:19:00 [t2] c.ThreadStarter - hello
Java 8 以后可以使用 lambda 精简代码
// 创建任务对象
Runnable task2 = () -> log.debug("hello");
// 参数1 是任务对象; 参数2 是线程名字,推荐Thread t2 = new Thread(task2, "t2"); t2.start();
- 原理之 Thread 与 Runnable 的关系分析 Thread 的源码,理清它与 Runnable 的关系小结
方法1 是把线程和任务合并在了一起,方法2 是把线程和任务分开了用 Runnable 更容易与线程池等高级 API 配合
用 Runnable 让任务类脱离了 Thread 继承体系,更灵活
3.FutureTask 配合 Thread
FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况
public class Thread3 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> task3=new FutureTask<Integer>(()->{
System.out.println("hello thread3");
Thread.sleep(2000);
return 100;
});
//创建thread对象
Thread thread = new Thread(task3,"t1");
//启动线程
thread.start();
//获取task3任务的结果
// 主线程阻塞,同步等待 task 执行完毕的结果
System.out.println(task3.get());
}
}
3.观察多个线程同时运行
主要是理解
交替执行
谁先谁后,不由我们控制
public class WatchThread {
public static void main(String[] args) {
new Thread(()->{
while(true){
System.out.println(Thread.currentThread().getName()+"is running..");
}
},"t1").start();
//执行线程t2
new Thread(()->{
while(true){
System.out.println(Thread.currentThread().getName()+"is running...");
}
},"t2").start();
}
}
4.查看进程线程的方法
1.windows
任务管理器可以查看进程和线程数,也可以用来杀死进程查看进程
tasklist
taskkill
杀死进程
2.linux
ps -fe 查看所有进程
ps -fT -p
kill 杀死进程
top 按大写 H 切换是否显示线程
top -H -p
3.Java
jps 命令查看所有 Java 进程
jstack
jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)
jconsole 远程监控配置
需要以如下方式运行你的 java 类
java -Djava.rmi.server.hostname=ip 地 址
-Dcom.sun.management.jmxremote - Dcom.sun.management.jmxremote.port=连接端口
-Dcom.sun.management.jmxremote.ssl=是否安全连接 - Dcom.sun.management.jmxremote.authenticate=是否认证 java类
修改 /etc/hosts 文件将 127.0.0.1 映射至主机名如果要认证访问,还需要做如下步骤
复制 jmxremote.password 文件
修改 jmxremote.password 和 jmxremote.access 文件的权限为 600 即文件所有者可读写连接时填入 controlRole(用户名),R&D(密码)
4.栈与栈帧
Java Virtual Machine Stacks (Java 虚拟机栈)
我们都知道 JVM 中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存。
每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
5.线程上下文切换(Thread Context Switch)
因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码线程的 cpu 时间片用完
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法
当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的
状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
Context Switch 频繁发生会影响性能
5.常见方法
方法名 | 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 中线程状态是用 6 个 enum 表示,分别为: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED | |
isInterrupted() |
判断是否被打断, | 不会清除 打断标记 | |
isAlive() |
线程是否存活 (还没有运行完毕) |
||
interrupt() |
打断线程 |
如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除 打断标 记 ;如果打断的正在运行的线程,则会设置 打断标记 ;park 的线程被打断,也会设置 打断标记 |
|
interrupted() |
static |
判断当前线程是否被打断 | 会清除 打断标记 |
currentThread() |
static |
获取当前正在执行的线程 |
方法名 | static | 功能说明 | 注意 |
---|---|---|---|
sleep(long n) |
static |
让当前执行的线程休眠n毫秒, 休眠时让出 cpu 的时间片给其它线程 | |
yield() |
static |
提示线程调度器让出当前线程对CPU的使用 | 主要是为了测试和调试 |
调用 run
public class Test1 {
public static void main(String[] args) {
Thread t1=new Thread("t1"){
public void run(){
System.out.println(Thread.currentThread().getName()+"t1 is running");
}
};
t1.run();
}
}
输出
19:39:14 [main] c.TestStart - main
19:39:14 [main] c.FileReader - read [1.mp4] start ...
19:39:18 [main] c.FileReader - read [1.mp4] end ... cost: 4227 ms 19:39:18 [main] c.TestStart - do other things ...
程序仍在 main 线程运行, 方法调用还是同步的
FileReader.read()
调用 start
将上述代码的 改为
t1.run()
t1.start();
public class Test1 {
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread("t1"){
public void run(){
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"t1 is running");
}
}
};
t1.start();
Thread.sleep(2000);
System.out.println("main thread is running...");
}
}
输出
19:41:30 [main] c.TestStart - do other things … 19:41:30 [t1] c.TestStart - t1
19:41:30 [t1] c.FileReader - read [1.mp4] start …
19:41:35 [t1] c.FileReader - read [1.mp4] end … cost: 4542 ms
程序在 t1 线程运行, 方法调用是异步的
FileReader.read()
小结
直接调用 run 是在主线程中执行了 run,没有启动新的线程
使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码
6.sleep 与 yield
1.sleep
1. 调用 sleep 会让当前线程从** **_**Running **_**进入 **_**Timed Waiting** _状态(阻塞)
1. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出
InterruptedException
1. 睡眠结束后的线程未必会立刻得到执行
1. 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
public class Test3 {
public static void main(String[] args) {
Thread t1=new Thread(()->{
System.out.println("enter sleep.."+new Date());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
System.out.println("i am wake up .."+new Date());
e.printStackTrace();
}
},"t1");
t1.start();
//主线程睡眠1秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("唤醒t1 "+new Date());
//唤醒
t1.interrupt();
}
}
2.yield(打断)
- 调用 yield 会让当前线程从 Running 进入 Runnable** **就绪状态,然后调度执行其它线程
- 具体的实现依赖于操作系统的任务调度器
3.线程优先级
线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用
public class Test4 {
public static void main(String[] args) {
Runnable task1=()->{
int count=0;
for(;;){
System.out.println("--->1====="+count++);
}
};
Runnable task2=()->{
int count=0;
for(;;){
//礼让
// Thread.yield();
System.out.println("----->2==="+count++);
}
};
Thread t1=new Thread(task1,"t1");
t1.setPriority(Thread.MAX_PRIORITY);
t1.start();
Thread t2 = new Thread(task2, "t2");
t2.setPriority(Thread.MIN_PRIORITY);
t2.start();
}
}
7.join 方法详解
1.为什么需要 join
下面的代码执行,打印 r 是什么?
public class Test5 {
static int r=0;
public static void main(String[] args) {
try {
test1();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private static void test1() throws InterruptedException {
System.out.println("开始..");
FutureTask<Integer> task = new FutureTask<>(() -> {
sleep(1);
r=10;
return 100;
});
Thread t1=new Thread(task,"t1");
t1.start();
//加入子线程t1,让主线程等待t1
//t1.join();
System.out.println("运行结果为:"+r);
System.out.println("结束..");
}
}
分析
因为主线程和线程 t1 是并行执行的,t1 线程需要 1 秒之后才能算出而主线程一开始就要打印 r 的结果,所以只能打印出
r=10
r=0
解决方法
用 sleep 行不行?为什么? 用 join,加在
t1.start()
之后即可
t1.start();
//加入子线程t1,让主线程等待t1
t1.join();
2.应用之同步(案例1)
以调用角度来讲,如果
1.需要等待结果返回,才能继续运行就是同步
2.不需要等待结果返回,才能继续运行就是异步
t1.start
1s 后
r=10
t1 终 止
t1.join
main
3.等待多个结果
问,下面代码 cost 大约多少秒?
public class Test5 {
private static final Logger log = LoggerFactory.getLogger(Test5.class);
static int r1 = 0;
static int r2 = 0;
static int r = 0;
public static void main(String[] args) {
try {
test1();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private static void test1() throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
r1=10;
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread t2 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(2);
r2=20;
} catch (InterruptedException e) {
e.printStackTrace();
}
});
//线程开启
t1.start();
t2.start();
long start=System.currentTimeMillis();
log.info("开启时间是:{}",start);
//加入线程
log.info("t1 join start ..");
//加入子线程t1
t1.join();
log.info("t1 join end ...");
log.info("t2 join start ...");
//此时t2执行了1s,还有1s需要执行
t2.join();
log.info("t2 join end ..");
//加入子线程t1,t2,让主线程等待t1,t2
long end=System.currentTimeMillis();
log.info("结束时间是:{}",end);
log.info("t1:{},t2:{},差值为:{}",r1,r2,(end-start));
}
}
分析如下
第一个 join:等待 t1 时, t2 并没有停止, 而在运行
第二个 join:1s 后, 执行到此, t2 也运行了 1s, 因此也只需再等待 1s
如果颠倒两个 join 呢? 最终都是输出
4.有时效的 join
没等够时间
public class Test6 {
static int r=0;
private static final Logger log = LoggerFactory.getLogger(Test5.class);
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
try {
//休眠2s
TimeUnit.SECONDS.sleep(2);
r=10;
} catch (InterruptedException e) {
e.printStackTrace();
}
});
//启动线程
long start=System.currentTimeMillis();
t1.start();
//执行join方法:线程执行结束会导致join结束
log.info("join start ..");
t1.join(1500);
long end=System.currentTimeMillis();
log.info("r1:{},cost:{}",r,(end-start));
}
}
输出
等够时间
public class Test6 {
static int r=0;
private static final Logger log = LoggerFactory.getLogger(Test5.class);
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
try {
//休眠2s
TimeUnit.SECONDS.sleep(2);
r=10;
} catch (InterruptedException e) {
e.printStackTrace();
}
});
//启动线程
long start=System.currentTimeMillis();
t1.start();
//执行join方法:线程执行结束会导致join结束
log.info("join start ..");
t1.join(2000);
long end=System.currentTimeMillis();
log.info("r1:{},cost:{}",r,(end-start));
}
}
8.interrupt 方法详解
1.打断 sleep,wait,join 的线程
这几个方法都会让线程进入阻塞状态
打断 sleep 的线程, 会清空打断状态,以 sleep 为例
public class Test1 {
private static final Logger log = LoggerFactory.getLogger(Test5.class);
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
log.info("enter sleep..");
try {
TimeUnit.SECONDS.sleep(5);//sleep,wait,join在被打断后,以异常的方式来标识打断标记,将打断标记置为假
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t1");
//启动线程t1
t1.start();
//让主线程休眠1s
TimeUnit.SECONDS.sleep(1);
log.info("打断t1线程...");
t1.interrupt();
log.info("打断标记为:{}",t1.isInterrupted());
}
}
输出
2.打断正常运行的线程
打断正常运行的线程, 不会清空打断状态
public class Test2 {
private static final Logger log = LoggerFactory.getLogger(Test5.class);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.info("子线程is running...");
while (true) {
if(Thread.currentThread().isInterrupted()){
//表明该子线程被打断,其打断标志位true
log.info("子线程打断状态:"+Thread.currentThread().isInterrupted());
break;
}
}
}, "t1");
t1.start();
TimeUnit.SECONDS.sleep(1);
log.info("interrupt..");
t1.interrupt();
}
}
3.模式之两阶段终止
打断 park 线程
打断 park 线程, 不会清空打断状态
public class Test3 {
public static void main(String[] args) throws InterruptedException {
TwoPhaseTermination t = new TwoPhaseTermination();
//启动线程
t.start();
//休眠3s
TimeUnit.SECONDS.sleep(3);
//终止线程
t.stop();
}
}
class TwoPhaseTermination {
private static final Logger log = LoggerFactory.getLogger(Test5.class);
private Thread monitor;
//启动监控线程
public void start() {
monitor = new Thread(() -> {
while (true) {
Thread thread = Thread.currentThread();
if (thread.isInterrupted()) {//标识当前线程是被打断状态
log.info("料理后事..");
break;
}
try {
TimeUnit.SECONDS.sleep(1);
log.info("执行监控..");
} catch (InterruptedException e) {
e.printStackTrace();
//重新设置打断标记即执行中断
thread.interrupt();
}
}
});
//启动监控线程
monitor.start();
}
//停止监控线程
public void stop() {
monitor.interrupt();
}
}
输出
4.打断park
public class Test4 {
private static final Logger log = LoggerFactory.getLogger(Test5.class);
public static void main(String[] args) {
test1();
}
private static void test1() {
Thread t1 = new Thread(() -> {
log.info("park ..");
//执行 LockSupport.park(),后面的语句不再执行
LockSupport.park();
log.info("unpark..");
//1.Thread.currentThread().isInterrupted():打断状态 boolean 值
//2.Thread.interrupted():将打断状态置为false
//3.线程名.interrupt:将该被park的线程打断标置为true
//4.线程在被park一次后,被打断后,park命令对其无效
log.info("打断状态:{}",Thread.interrupted());
LockSupport.park();
log.info("unpark");
});
t1.start();
//打断在park的线程,将打断标志置为true
t1.interrupt();
}
}
还有一些不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁
方法名 | static | 功能说明 |
---|---|---|
stop() | 停止线程运行 | |
suspend() | 挂起(暂停)线程运行 | |
resume() | 恢复线程运行 |
9.主线程与守护线程
默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
例:
public class Test5 {
private static final Logger log = LoggerFactory.getLogger(com.xuge.n1.Test5.class);
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
while (true) {
if (Thread.currentThread().isInterrupted()) {
break;
}
log.info("结束");
}
});
//设置t1为守护线程
t1.setDaemon(true);
//启动t1
t1.start();
TimeUnit.SECONDS.sleep(1);
log.info("结束..");
}
}
输出
注意
- 垃圾回收器线程就是一种守护线程
- Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求
10.五种状态
这是从 操作系统 层面来描述的
【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联
【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
【运行状态】指获取了 CPU 时间片运行中的状态
当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换
【阻塞状态】
如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入阻塞状态
等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑 调度它们
【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
11.六种状态
这是从 Java API 层面来描述的
根据 Thread.State 枚举,分为六种状态
- NEW 线程刚被创建,但是还没有调用start()方法
- RUNNABLE 当调用了start()方法之后,注意,Java API 层面的RUNNABLE状态涵盖了操作系统层面的可运行状态、运行状态、阻塞状态(由于BIO的线程阻塞,在java里面无法区分,认为是可运行)
3.BLOCKED WAITING TIMED_WAITING都是javaApi层面对阻塞状态的细分,后面会在状态转换一节详述
4.TERMINATED 当线程代码运行结束
测试代码
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
@Slf4j(topic = "c.TestState")
public class TestState {
public static void main(String[] args) throws IOException {
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.debug("running...");
}
};
Thread t2 = new Thread("t2") {
@Override
public void run() {
while(true) { // runnable
}
}
};
t2.start();
Thread t3 = new Thread("t3") {
@Override
public void run() {
log.debug("running...");
}
};
t3.start();
Thread t4 = new Thread("t4") {
@Override
public void run() {
synchronized (TestState.class) {
try {
Thread.sleep(1000000); // timed_waiting
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t4.start();
Thread t5 = new Thread("t5") {
@Override
public void run() {
try {
t2.join(); // waiting
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t5.start();
Thread t6 = new Thread("t6") {
@Override
public void run() {
synchronized (TestState.class) { // blocked
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t6.start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("t1 state {}", t1.getState());
log.debug("t2 state {}", t2.getState());
log.debug("t3 state {}", t3.getState());
log.debug("t4 state {}", t4.getState());
log.debug("t5 state {}", t5.getState());
log.debug("t6 state {}", t6.getState());
System.in.read();
}
}
12.习题
阅读华罗庚《统筹方法》,给出烧水泡茶的多线程解决方案,提示
参考图二,用两个线程(两个人协作)模拟烧水泡茶过程
文中办法乙、丙都相当于任务串行
而图一相当于启动了 4 个线程,有点浪费
用 sleep(n) 模拟洗茶壶、洗水壶等耗费的时间
附:华罗庚《统筹方法》
统筹方法,是一种安排工作进程的数学方法。它的实用范围极广泛,在企业管理和基本建设中,以及关系复 杂的科研项目的组织与管理中,都可以应用。 怎样应用呢?主要是把工序安排好。 比如,想泡壶茶喝。当时的情况是:开水没有;水壶要洗,茶壶、茶杯要洗;火已生了,茶叶也有了。怎么 办?
办法甲:洗好水壶,灌上凉水,放在火上;在等待水开的时间里,洗茶壶、洗茶杯、拿茶叶;等水开 了,泡茶喝。 办法乙:先做好一些准备工作,洗水壶,洗茶壶茶杯,拿茶叶;一切就绪,灌水烧水;坐待水开了,泡 茶喝。 办法丙:洗净水壶,灌上凉水,放在火上,坐待水开;水开了之后,急急忙忙找茶叶,洗茶壶茶杯,泡 茶喝。 哪一种办法省时间?我们能一眼看出,第一种办法好,后两种办法都窝了工。这是小事,但这是引子,可以引出生产管理等方面有用的方法来。 水壶不洗,不能烧开水,因而洗水壶是烧开水的前提。没开水、没茶叶、不洗茶壶茶杯,就不能泡茶,因而 这些又是泡茶的前提。它们的相互关系,可以用下边的箭头图来表示: 从这个图上可以一眼看出,办法甲总共要16分钟(而办法乙、丙需要20分钟)。如果要缩短工时、提高工作 效率,应当主要抓烧开水这个环节,而不是抓拿茶叶等环节。同时,洗茶壶茶杯、拿茶叶总共不过4分钟,大 可利用“等水开”的时间来做。 是的,这好像是废话,卑之无甚高论。有如走路要用两条腿走,吃饭要一口一口吃,这些道理谁都懂得。但 稍有变化,临事而迷的情况,常常是存在的。在近代工业的错综复杂的工艺过程中,往往就不是像泡茶喝这 么简单了。任务多了,几百几千,甚至有好几万个任务。关系多了,错综复杂,千头万绪,往往出现“万事俱 备,只欠东风”的情况。由于一两个零件没完成,耽误了一台复杂机器的出厂时间。或往往因为抓的不是关 键,连夜三班,急急忙忙,完成这一环节之后,还得等待旁的环节才能装配。 洗茶壶,洗茶杯,拿茶叶,或先或后,关系不大,而且同是一个人的活儿,因而可以合并成为: 看来这是“小题大做”,但在工作环节太多的时候,这样做就非常必要了。 这里讲的主要是时间方面的事,但在具体生产实践中,还有其他方面的许多事。这种方法虽然不一定能直接 解决所有问题,但是,我们利用这种方法来考虑问题,也是不无裨益的。
- 应用之统筹(烧水泡茶)
- 代码实现
```java
@Slf4j(topic=”c.testAPP”)
public class TestApp {
public static void main(String[] args) {
Thread t1=new Thread(()->{
},”老王”); t1.start(); Thread t2=new Thread(()->{try { log.info("洗水壶花费1min.."); TimeUnit.SECONDS.sleep(1); log.info("烧开水花费5min.."); TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
},”小王”); t2.start();try { log.info("洗茶壶花费1min.."); TimeUnit.SECONDS.sleep(1); log.info("洗茶杯花费2min.."); TimeUnit.SECONDS.sleep(2); log.info("拿茶叶花费1min.."); TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } log.info("可以泡茶了...");
} }
```
解法1缺陷
1.上面模拟的是小王等老王的水烧开了,小王泡茶,如果反过来要实现老王等小王的茶叶拿来了,老王泡茶呢?代码最好能适应两种情况 2.上面的两个线程其实是各执行各的,如果要模拟老王把水壶交给小王泡茶,或模拟小王把茶叶交给老王泡呢?
13.本章小结
本章的重点在于掌握线程创建
线程重要 api,如 start,run,sleep,join,interrupt 等
线程状态应用方面
异步调用:主线程执行期间,其它线程异步执行耗时操作提高效率:并行计算,缩短运算时间
同步等待:join
统筹规划:合理使用线程,得到最优效果
原理方面
线程运行流程:栈、栈帧、上下文切换、程序计数器
Thread 两种创建方式 的源码
模式方面
终止模式之两阶段终止