1. 线程创建与运行
1.1 线程创建的四种方式
方法1. 使用Thread
@Slf4j(topic = "c.CreateThread1")
public class CreateThread1 {
public static void main(String[] args) {
Thread t1 = new Thread("t1"){
public void run(){
log.debug("t1-running");
}
};
t1.start();
log.debug("main-running");
}
}
13:40:33.366 [main] DEBUG c.CreateThread1 - main-running
13:40:33.366 [t1] DEBUG c.CreateThread1 - t1-running
方法2.使用 Runnable 配合 Thread(推荐)
把【线程】和【任务】(要执行的代码)分开,Thread 代表线程,Runnable 可运行的任务(线程要执行的代码)
@Slf4j(topic = "c.CreateThread2")
public class CreateThread2 {
public static void main(String[] args) {
// 任务对象
Runnable runnable = new Runnable() {
@Override
public void run() {
log.debug("running");
}
};
// 线程对象
Thread thread = new Thread(runnable, "t2");
// 启动
thread.start();
}
}
13:43:42.104 [t2] DEBUG c.CreateThread2 - running
小结
- 方法1 是把线程和任务合并在了一起,
- 方法2 是把线程和任务分开了(推荐)
- 用 Runnable 更容易与线程池等高级 API 配合,用 Runnable 让任务类脱离了 Thread 继承体系,更灵活。通过查看源码可以发现,方法二其实到底还是通过方法一执行的!
源码:
- 创建线程为Thread类的init方法的重载,target对象为自定义的Runnable对象,
在执行Thread的run()方法时,他会判断是否target对象,如果有,就用。
方法3 FutureTask 配合 Thread
FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况。FutureTask 同时实现了Runnable, Future接口(FutureTask实现了RunnableFuture接口,该接口继承Runnable接口和Future接口)。Future接口可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。FutrueTask是Futrue接口的唯一的实现类。 它既可以作为 Runnable被线程执行,又可以作为Future得到Callable的返回值。
Thread类中没有一个方法是执行Callable的,所以需要外界帮助。通过适配器模式将Callable和Runnable接口建立联系。在FutureTask类中通过构造方法将实现了Callable接口的类放入。即对象适配器思想。一方面,FutureTask可以当做Runnable被Thread执行,另一方面实现了Future接口,可以用Future接口的get方法获取到Callable接口中call方法的计算结果)
- Callable接口:与Runnable相比,功能更强
- 相比run方法,可以有返回值
- 方法可以抛出异常,被外面的操作捕获,获取异常信息
- 支持泛型的返回值
- 需要借助FutureTask类(是Future接口的唯一实现类,可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等)
future接口与Callable接口的关系
- 可以用Future.get()来获取Callable接口返回的执行任务,
- Futuren.isDone()来判断任务是否已经执行完成或者取消任务。
- 如果call()方法还没执行完,调用get()方法的线程会被阻塞,直到call()方法返回结果,主线程才会恢复。
可以认为Future是一个存储器,存储了call()这个任务的结果。
@Slf4j(topic = "c.CreateThread3")
public class CreateThread3 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 创建FutureTask对象(作为线程要实现的内容),将实现了Callable接口的对象传入,
FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
log.debug("running");
Thread.sleep(1000);
return 100;
}
});
// 将task对象传入t1线程中
Thread t1 = new Thread(task, "t1");
t1.start();
// 等待task返回结果
Integer integer = task.get();
log.debug("{}", integer);
}
}
14:24:25.084 [t1] DEBUG c.CreateThread3 - running
14:24:26.092 [main] DEBUG c.CreateThread3 - 100
1.2 查看进程线程的方法
windows
- 任务管理器可以查看进程和线程数,也可以用来杀死进程
- tasklist 查看进程
- taskkill 杀死进程
- linux
- ps -fe 查看所有进程
- ps -fT -p
查看某个进程(PID)的所有线程 - kill 杀死进程
- top 按大写 H 切换是否显示线程
- top -H -p
查看某个进程(PID)的所有线程
Java
每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存
-
1.3.2 线程上下文切换(Thread Context Switch)
为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码
线程的 cpu 时间片用完
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了sleep、yield、wait、join、park、synchronized、lock等方法
当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念 就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的
1.4 Thread的常见方法
1.4.1 start() vs run()
1. start
public static void main(String[] args) {
Thread thread = new Thread(){
@Override
public void run(){
log.debug("我是一个新建的线程正在运行中");
FileReader.read(fileName);
}
};
thread.setName("新建线程");
thread.start();
log.debug("主线程");
}
输出:程序在 t1 线程运行,run()方法里面内容的调用是异步的
11:59:40.711 [main] DEBUG com.concurrent.test.Test4 - 主线程
11:59:40.711 [新建线程] DEBUG com.concurrent.test.Test4 - 我是一个新建的线程正在运行中
11:59:40.732 [新建线程] DEBUG com.concurrent.test.FileReader - read [test] start ...
11:59:40.735 [新建线程] DEBUG com.concurrent.test.FileReader - read [test] end ... cost: 3 ms
2. run
将上面代码的thread.start();改为thread.run();输出结果如下:程序仍在 main 线程运行,run()方法里面内容的调用还是同步的
12:03:46.711 [main] DEBUG com.concurrent.test.Test4 - 我是一个新建的线程正在运行中
12:03:46.727 [main] DEBUG com.concurrent.test.FileReader - read [test] start ...
12:03:46.729 [main] DEBUG com.concurrent.test.FileReader - read [test] end ... cost: 2 ms
12:03:46.730 [main] DEBUG com.concurrent.test.Test4 - 主线程
3.小结
直接调用run()是在主线程中执行了run(),没有启动新的线程;使用start()是启动新的线程,通过新的线程间接执行run()方法中的代码
start方法对线程状态的影响
@Slf4j(topic = "c.test1")
public class ThreadTest1 {
public static void main(String[] args) {
Thread t1 = new Thread("t1"){
public void run(){
log.debug("running...");
}
};
System.out.println(t1.getState());
t1.start();
System.out.println(t1.getState());
}
}
NEW
RUNNABLE
15:29:00.095 [t1] DEBUG c.test1 - running...
1.4.2 sleep() vs yield()
1. sleep
- 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
- 其它线程可以使用 interrupt 方法打断正在睡眠的线程,那么被打断的线程这时就会抛出InterruptedException异常【注意:这里打断的是正在休眠的线程,而不是其它状态的线程】
- 睡眠结束后的线程未必会立刻得到执行(需要分配到cpu时间片)
- 建议用 TimeUnit 的sleep()代替 Thread 的sleep()来获得更好的可读性
sleep的使用: Timed Waiting
@Slf4j(topic = "c.ThreadTest2")
public class ThreadTest2 {
public static void main(String[] args) {
Thread t1 = new Thread("t1"){
@Override
public void run() {
try {
// 让t1线程进入休眠状态
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t1.start();
// 打印t1状态,由于主线程创建之后,t1线程才创建出来,当主线程执行该方法时,t1线程可能还没有休眠
log.debug("t1 state:{}" , t1.getState()); // RUNNABLE
// 解决办法,给主线程休眠一下,
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("t1 state:{}" , t1.getState()); // TIMED_WAITING
}
}
15:39:04.488 [main] DEBUG c.ThreadTest2 - t1 state:RUNNABLE
15:39:04.997 [main] DEBUG c.ThreadTest2 - t1 state:TIMED_WAITING
sleep的使用:打断睡眠
@Slf4j(topic = "c.ThreadTest3")
public class ThreadTest3 {
public static void main(String[] args) {
Thread t1 = new Thread("t1"){
@Override
public void run() {
log.debug("t1 enter sleep...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
log.debug("wake up");
e.printStackTrace();
}
}
};
t1.start();
try {
Thread.sleep(1000);
// 打断
log.debug("interrupt");
t1.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
15:52:31.713 [t1] DEBUG c.ThreadTest3 - t1 enter sleep...
15:52:32.716 [main] DEBUG c.ThreadTest3 - interrupt
15:52:32.717 [t1] DEBUG c.ThreadTest3 - wake up
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.ll.ch2.ThreadTest3$1.run(ThreadTest3.java:14)
TimeUnit
@Slf4j(topic = "c.ThreadTest4")
public class ThreadTest4 {
public static void main(String[] args) {
try {
log.debug("enter");
TimeUnit.SECONDS.sleep(1);
log.debug("end");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
15:55:27.245 [main] DEBUG c.ThreadTest4 - enter
15:55:28.255 [main] DEBUG c.ThreadTest4 - end
2. yield
- 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
- 具体的实现依赖于操作系统的任务调度器(就是可能没有其它的线程正在执行,虽然调用了yield方法想把cpu的使用权让出去,但是还是执行该线程)
3.小结
yield(Runnable状态)使cpu调用其它线程,但是cpu可能会再分配时间片给该线程;而sleep(Time waiting状态)需要等过了休眠时间之后才有可能被分配cpu时间片4. sleep的应用
在没有利用cpu来计算时,不要让while(true)空转浪费cpu,这时可以使用yield或sleep来让出cpu的使用权给其他程序。可以使用wait或条件变量达到类似效果;不同的是,后两种需要加锁,适用于同步场景;sleep适用于无需锁同步的场景。1.4.3 线程优先级
- 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它。
- 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用。
设置优先级的源码
public final void setPriority(int newPriority) {
ThreadGroup g;
checkAccess();
if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
throw new IllegalArgumentException();
}
if((g = getThreadGroup()) != null) {
if (newPriority > g.getMaxPriority()) {
newPriority = g.getMaxPriority();
}
setPriority0(priority = newPriority);
}
}
不使用yield
@Slf4j(topic = "c.ThreadTest5")
public class ThreadTest5 {
public static void main(String[] args) {
Runnable task1 = new Runnable() {
@Override
public void run() {
int cnt = 0;
for (;;){
System.out.println("--->1 " + cnt++);
}
}
};
Runnable task2 = new Runnable() {
@Override
public void run() {
int cnt = 0;
for (;;){
System.out.println(" --->2 " + cnt++);
}
}
};
Thread t1 = new Thread(task1, "t1");
Thread t2 = new Thread(task2, "t2");
t1.start();
t2.start();
}
}
t1与t2势均力敌
使用yield
@Slf4j(topic = "c.ThreadTest5")
public class ThreadTest5 {
public static void main(String[] args) {
Runnable task1 = new Runnable() {
@Override
public void run() {
int cnt = 0;
for (;;){
System.out.println("--->1 " + cnt++);
}
}
};
Runnable task2 = new Runnable() {
@Override
public void run() {
int cnt = 0;
for (;;){
Thread.yield();
System.out.println(" --->2 " + cnt++);
}
}
};
Thread t1 = new Thread(task1, "t1");
Thread t2 = new Thread(task2, "t2");
t1.start();
t2.start();
}
}
可以看到t1明显比t2的数大,t2让步了。
加入优先级
@Slf4j(topic = "c.ThreadTest5")
public class ThreadTest5 {
public static void main(String[] args) {
Runnable task1 = new Runnable() {
@Override
public void run() {
int cnt = 0;
for (;;){
System.out.println("--->1 " + cnt++);
}
}
};
Runnable task2 = new Runnable() {
@Override
public void run() {
int cnt = 0;
for (;;){
//Thread.yield();
System.out.println(" --->2 " + cnt++);
}
}
};
Thread t1 = new Thread(task1, "t1");
Thread t2 = new Thread(task2, "t2");
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();
}
}
1.4.4 join的使用
例子1:
@Slf4j(topic = "c.JoinTest1")
public class JoinTest1 {
private static int r = 0;
public static void main(String[] args) throws InterruptedException {
log.debug("开始");
Thread t1 = new Thread(() -> {
try {
log.debug("开始");
Thread.sleep(1000);
log.debug("结束");
r = 10;
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1");
t1.start();
// t1.join();
log.debug("结果为:{}", r);
log.debug("结束");
}
}
18:03:36.497 [main] DEBUG c.JoinTest1 - 开始
18:03:36.567 [t1] DEBUG c.JoinTest1 - 开始
18:03:36.567 [main] DEBUG c.JoinTest1 - 结果为:0
18:03:36.570 [main] DEBUG c.JoinTest1 - 结束
18:03:37.569 [t1] DEBUG c.JoinTest1 - 结束
从结果中可以看到:r的值为0。因为t1的赋值在sleep方法之后进行的,但是主线程执行时,t1线程也开始执行了,主线程打印的时候t1线程还在sleep中,没有进行赋值。
解决办法
- sleep时间不好把控
使用join方法,等待线程运行结束 ```java @Slf4j(topic = “c.JoinTest1”) public class JoinTest1 { private static int r = 0;
public static void main(String[] args) throws InterruptedException {
log.debug("开始");
Thread t1 = new Thread(() -> {
try {
log.debug("开始");
Thread.sleep(1000);
log.debug("结束");
r = 10;
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1");
t1.start();
t1.join();
log.debug("结果为:{}", r);
log.debug("结束");
} }
```java
18:03:08.987 [main] DEBUG c.JoinTest1 - 开始
18:03:09.057 [t1] DEBUG c.JoinTest1 - 开始
18:03:10.062 [t1] DEBUG c.JoinTest1 - 结束
18:03:10.063 [main] DEBUG c.JoinTest1 - 结果为:10
18:03:10.064 [main] DEBUG c.JoinTest1 - 结束
主线程等待t1结束才运行,得到了赋值的结果10
线程的同步
同步:需要等待结果返回,才能继续运行;
异步:不需要等待结果返回,就能继续运行。
例子2
@Slf4j( topic = "c.JoinTest2")
public class JoinTest2 {
private static int r1 = 0;
private static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
sleep(1000);
r1 = 10;
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread t2 = new Thread(() -> {
try {
sleep(2000);
r2 = 20;
} catch (InterruptedException e) {
e.printStackTrace();
}
});
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);
}
}
18:10:13.041 [main] DEBUG c.JoinTest2 - join begin
18:10:14.044 [main] DEBUG c.JoinTest2 - t1 join end
18:10:15.044 [main] DEBUG c.JoinTest2 - t2 join end
18:10:15.044 [main] DEBUG c.JoinTest2 - r1: 10 r2: 20 cost: 2005
从结果可以看出,等待时间为2秒钟,因为开始的时候主线程和t1、t2线程同时开始,所以只需等待2秒钟即可,不用等待3秒钟。(t2相较于t1晚1秒打印)
交换两个join
@Slf4j( topic = "c.JoinTest2")
public class JoinTest2 {
private static int r1 = 0;
private static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
sleep(1000);
r1 = 10;
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread t2 = new Thread(() -> {
try {
sleep(2000);
r2 = 20;
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
t2.start();
long start = System.currentTimeMillis();
log.debug("join begin");
t2.join();
log.debug("t2 join end");
t1.join();
log.debug("t1 join end");
long end = System.currentTimeMillis();
log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}
18:18:02.773 [main] DEBUG c.JoinTest2 - join begin
18:18:04.775 [main] DEBUG c.JoinTest2 - t2 join end
18:18:04.775 [main] DEBUG c.JoinTest2 - t1 join end
18:18:04.776 [main] DEBUG c.JoinTest2 - r1: 10 r2: 20 cost: 2005
先对t2线程join,到t1线程join时,已经不需要等了,因为t1线程早已经结束了(t1与t2的打印时间相同)
限时同步
@Slf4j(topic = "c.JoinTest3")
public class JoinTest3 {
private static int r1 = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
sleep(2000);
r1 = 10;
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long start = System.currentTimeMillis();
t1.start();
// 线程执行结束会导致 join 结束
log.debug("join begin");
// 设置join等待1500ms就不再等了
t1.join(1500);
long end = System.currentTimeMillis();
log.debug("r1: {} cost: {}", r1, end - start);
}
}
18:30:12.519 [main] DEBUG c.JoinTest3 - join begin
18:30:14.025 [main] DEBUG c.JoinTest3 - r1: 0 cost: 1508
因为设置了join的等待时间,主线程等待1500毫秒就不再等了,而t1线程会在2000毫秒赋值,所以r1值为0
@Slf4j(topic = "c.JoinTest3")
public class JoinTest3 {
private static int r1 = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
Thread.sleep(2000);
r1 = 10;
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long start = System.currentTimeMillis();
t1.start();
// 线程执行结束会导致 join 结束
log.debug("join begin");
// 设置join等待3000ms就不再等了
t1.join(3000);
long end = System.currentTimeMillis();
log.debug("r1: {} cost: {}", r1, end - start);
}
}
18:31:41.227 [main] DEBUG c.JoinTest3 - join begin
18:31:43.228 [main] DEBUG c.JoinTest3 - r1: 10 cost: 2003
将等待时间设置为3000毫秒时,能获取到正常赋值,同时在t1结束的时候主线程也就结束了。
1.4.5 interrupt 方法详解
1.打断 sleep,wait,join 的线程
sleep,wait,join 的线程,这几个方法都会让线程进入阻塞状态,interrupt用于打断阻塞状态,同时会清除打断标记
以 sleep 为例
@Slf4j(topic = "InterruptTest1")
public class InterruptTest1 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread("t1"){
@Override
public void run() {
log.debug("sleep...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t1.start();
Thread.sleep(500);
log.debug("interrupt");
t1.interrupt();
log.debug("打断标记:{}", t1.isInterrupted());
}
}
18:41:22.355 [t1] DEBUG InterruptTest1 - sleep...
18:41:22.856 [main] DEBUG InterruptTest1 - interrupt
18:41:22.857 [main] DEBUG InterruptTest1 - 打断标记:false
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.ll.ch2.InterruptTest1$1.run(InterruptTest1.java:13)
2. 打断正常运行的线程
打断正常运行的线程, 线程并不会暂停,只是调用方法Thread.currentThread().isInterrupted();的返回值为true,可以判断Thread.currentThread().isInterrupted();的值来手动停止线程
@Slf4j(topic = "InterruptTest2")
public class InterruptTest2 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread("t1"){
@Override
public void run() {
while (true){
boolean flag = Thread.currentThread().isInterrupted();
if (isInterrupted()){
log.debug("被打断了,退出循环");
break;
}
}
}
};
t1.start();
Thread.sleep(1000);
log.debug("打断t1");
t1.interrupt();
}
}
11:42:52.312 [main] DEBUG InterruptTest2 - 打断t1
11:42:52.316 [t1] DEBUG InterruptTest2 - 被打断了,退出循环
两阶段终止模式
- 使用线程对象的stop方法停止线程
- stop方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁,其他线程将永远无法获取锁。
- 使用System.exit(int) 方法停止线程
- 目的仅是停止一个线程,这种做法会让整个程序停止。
Two Phase Termination,就是考虑在一个线程T1中如何优雅地终止另一个线程T2?这里的优雅指的是给T2一个料理后事的机会(如释放锁),监控线程在监控过程中停止监控了就可以采用这个方式。
如下所示:线程的isInterrupted()方法可以取得线程的打断标记,如果线程在睡眠sleep期间被打断,打断标记是不会变的,为false,但是sleep期间被打断会抛出异常,我们据此手动设置打断标记为true;如果是在程序正常运行期间被打断的,那么打断标记就被自动设置为true。处理好这两种情况那我们就可以放心地来料理后事啦!
@Slf4j(topic = "c.TerminationModeTest")
public class TerminationModeTest {
public static void main(String[] args) throws InterruptedException {
TwoPhaseTermination t = new TwoPhaseTermination();
// 启动监控线程
t.start();
// 主线程等3.5秒后
Thread.sleep(3500);
// 优雅的停止监控线程
t.stop();
}
}
@Slf4j(topic = "c.TwoPhaseTermination")
class TwoPhaseTermination {
private Thread monitor;
public void start(){
monitor = new Thread("monitor"){
@Override
public void run() {
while (true){
// 拿到当前线程
Thread current = Thread.currentThread();
// 判断是否被打断
if (current.isInterrupted()){
// 被打断
log.debug("处理后事");
break;
}
try {
Thread.sleep(1000);
log.debug("监控记录");
} catch (InterruptedException e) {
// 睡眠期间被打断
e.printStackTrace();
// 重新设置打断标记
current.interrupt();
}
}
}
};
monitor.start();
}
public void stop(){
monitor.interrupt();
}
}
11:34:43.983 [monitor] DEBUG c.TwoPhaseTermination - 监控记录
11:34:44.990 [monitor] DEBUG c.TwoPhaseTermination - 监控记录
11:34:45.995 [monitor] DEBUG c.TwoPhaseTermination - 监控记录
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.ll.ch2.TwoPhaseTermination$1.run(TerminationModeTest.java:34)
11:34:46.483 [monitor] DEBUG c.TwoPhaseTermination - 处理后事
3.打断park线程
park线程让当前线程停下来
@Slf4j(topic = "ParkTest")
public class ParkTest {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()-> {
log.debug("park....");
LockSupport.park();
log.debug("unpark.......");
log.debug("打断状态{}", Thread.currentThread().isInterrupted());
LockSupport.park();
log.debug("unpark....");
}, "t1");
t1.start();
Thread.sleep(1000);
t1.interrupt();
}
}
11:49:54.913 [t1] DEBUG ParkTest - park....
11:49:55.916 [t1] DEBUG ParkTest - unpark.......
11:49:55.917 [t1] DEBUG ParkTest - 打断状态true
11:49:55.919 [t1] DEBUG ParkTest - unpark....
park()将t1线程挂起,主线程睡1秒后打断t1线程的park状态,此时t1线程继续执行,注意park一旦被打断就无法继续挂起线程。
4.isInterrupted() 与 interrupted() 比较
首先,isInterrupted 是实例方法,interrupted 是静态方法,它们的用处都是查看当前打断的状态,但是 isInterrupted 方法查看线程的时候,不会将打断标记清空,也就是置为 false,interrupted 查看线程打断状态后,会将打断标志置为 false,也就是清空打断标记,简单来说,interrupt() 方法类似于 setter 设置中断值,isInterrupted() 类似于 getter 获取中断值,interrupted() 类似于 getter + setter 先获取中断值,然后清除标志。
1.4.6 过时的方法
stop():停止线程运行
suspend():挂起(暂停)线程运行
resume():恢复线程运行
这些方法已过时,会破坏同步代码块,造成线程死锁。
1.4.7 守护线程
默认情况下,java进程需要等待所有的线程结束后才会停止,但是有一种特殊的线程,叫做守护线程,在其他线程全部结束的时候即使守护线程还未结束代码未执行完java进程也会停止。普通线程t1可以调用t1.setDeamon(true);方法变成守护线程。
代码示例:
@Slf4j(topic = "DaemonTest")
public class DaemonTest {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
while (true){
if (Thread.currentThread().isInterrupted()){
break;
}
}
log.debug("t1结束");
}, "t1");
//t1.setDaemon(true);
t1.start();
Thread.sleep(1000);
log.debug("主线程结束");
}
}
可以看到主线程结束了,但是t1线程还没有结束,所以java进程不会 停止,还在运行中
守护线程
设置t1为守护线程
@Slf4j(topic = "DaemonTest")
public class DaemonTest {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
while (true){
if (Thread.currentThread().isInterrupted()){
break;
}
}
log.debug("t1结束");
}, "t1");
t1.setDaemon(true);
t1.start();
Thread.sleep(1000);
log.debug("主线程结束");
}
}
可以随着主线程结束,进程就终止了。守护线程t1不管结没结束,也就随着终止了。
注意
- 垃圾回收器线程就是一种守护线程。进程结束了,垃圾回收也就停止。
Tomcat 中的 Acceptor(接收请求) 和 Poller (分发请求)线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求
2.线程状态
2.1 线程的5种状态
从操作系统层面划分,线程有5种状态
初始状态,仅仅是在语言层面上创建了线程对象,即Thead thread = new Thead();,还未与操作系统线程关联
- 可运行状态,也称就绪状态,指该线程已经被创建,与操作系统相关联,等待cpu给它分配时间片就可运行
- 运行状态,指线程获取了CPU时间片,正在运行
- 当CPU时间片用完,线程会转换至【可运行状态】,等待 CPU再次分配时间片,会导致我们前面讲到的上下文切换
- 阻塞状态
- 如果调用了阻塞API,如BIO读写文件,那么线程实际上不会用到CPU,不会分配CPU时间片,会导致上下文切换,进入【阻塞状态】
- 等待BIO操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
- 与【可运行状态】的区别是,只要操作系统一直不唤醒线程,调度器就一直不会考虑调度它们,CPU就一直不会分配时间片
终止状态,表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
2.2 线程的6种状态
这是从 Java API 层面来描述的
1.初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
- 2.运行(RUNNABLE):Java线程中将就绪、运行、阻塞(IO阻塞,java无法区分)三种状态笼统的称为“运行”。
- 阻塞(BLOCKED):表示线程阻塞于锁。
- 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
- 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
- 终止(TERMINATED):表示该线程已经执行完毕。
代码示例
@Slf4j(topic = "c.StateTest")
public class StateTest {
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 (StateTest.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 (StateTest.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();
}
}
13:42:08.467 [t3] DEBUG c.StateTest - running...
13:42:08.970 [main] DEBUG c.StateTest - t1 state NEW
13:42:08.973 [main] DEBUG c.StateTest - t2 state RUNNABLE
13:42:08.973 [main] DEBUG c.StateTest - t3 state TERMINATED
13:42:08.973 [main] DEBUG c.StateTest - t4 state TIMED_WAITING
13:42:08.973 [main] DEBUG c.StateTest - t5 state WAITING
13:42:08.973 [main] DEBUG c.StateTest - t6 state BLOCKED
t1是新建的一个线程,还没有开始执行;t2是正在执行的线程,由于有个死循环,所以他会一直执行;t3就执行了一次,由于主线睡了0.5秒,这个时候t3已经执行完了;t4睡的时间比主线程长;t5在等待t2执行完才能执行,且没有时间限制;t4线程加了锁,t6也对同一个加锁,等待t4释放锁。
3.结论
本章的重点在于掌握
1)线程的创建
2)线程重要的 API,如 start、run、sleep、yield、join、interrupt 等
3)线程的状态
4)原理方面,线程的运行流程,栈、栈帧、上下文切换、程序计数器等知识。
5)Thread 两种创建线程的源码
6)使用 interrupt 来编写两阶段终止