线程自动退出_【原创】Java并发编程系列11 | 线程调度

点击上方“java进阶架构师”,选择右上角“置顶公众号

20大进阶架构专题每日送达

a847ea1df7cbe13fa7fc6647eac3a26e.png

2a3a2f3e4e6734cf0c2a67c9a28cc9b4.png
Java并发编程

之前发过,但是因为之前忘记标记原创,没办法收录在【并发编程专题】里面,作为强迫症的我,必须要重发一次。本文为第 11 篇,前面几篇没看过的,可以在文末找到前几篇的跳转链接。本文介绍线程调度的如下几个操作:

  • 线程优先级
  • 守护线程
  • 线程中断
  • join
  • sleep
  • yield
  • wait & notify

1. 优先级

每个线程执行时都有一个优先级的属性,优先级高的线程可以获得较多的执行机会,而优先级低的线程则获得较少的执行机会。

操作系统采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线程会分配到若干时间片,当线程的时间片用完了就会发生线程调度,并等待着下次分配。线程分配到的时间片多少也就决定了线程使用处理器资源的多少,而线程优先级就是决定线程需要多或者少分配一些处理器资源的线程属性。

Thread 类通过一个整型成员变量 priority 来控制优先级,优先级的范围从 1 ~ 10,默认优先级是 5。

举例:如下代码,一般情况下,高级线程更先执行完毕。

public class Test1 {
public static void main(String[] args) throws InterruptedException {
new MyThread("高级", 10).start();
new MyThread("低级", 1).start();
}
}

class MyThread extends Thread {
public MyThread(String name, int pro) {
super(name);// 设置线程的名称
setPriority(pro);// 设置线程的优先级
}

@Override
public void run() {
for (int i = 0; i < 10000; i++) {
System.out.println(this.getName() + "线程第" + i + "次执行!");
}
}
}

虽然 Java 提供了 10 个优先级别,但这些优先级别需要操作系统的支持,所以需要注意:

  1. 操作系统的优先级可能不能很好的和 Java 的 10 个优先级别对应,所以最好使用 MAX_PRIORITY、MIN_PRIORITY 和 NORM_PRIORITY 三个静态常量来设定优先级,以保证程序更好的可移植性。
  2. 线程优先级不能作为程序正确性的依赖,因为操作系统可以完全不用理会 Java 线程对于优先级的设定。

2. Deamon 守护线程

守护线程是什么?

Daemon 线程是一种支持型线程,在后台守护一些系统服务,比如 JVM 的垃圾回收、内存管理等线程都是守护线程。

与之对应的就是用户线程,用户线程就是系统的工作线程,它会完成整个系统的业务操作。

用户线程结束后就意味着整个系统的任务全部结束了,因此系统就没有对象需要守护的了,守护线程自然而然就会退出。所以当一个 Java 应用只有守护线程的时候,虚拟机就会自然退出

举例:

public class Test {
public static void main(String[] args) {
Thread t1 = new MyCommon();
Thread t2 = new Thread(new MyDaemon());
t2.setDaemon(true); // 设置为守护线程

t2.start();
t1.start();
}
}

class MyCommon extends Thread {
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("线程1第" + i + "次执行!");
try {
Thread.sleep(7);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

class MyDaemon implements Runnable {
public void run() {
for (long i = 0; i < 9999999L; i++) {
System.out.println("后台线程第" + i + "次执行!");
try {
Thread.sleep(7);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

执行结果:

后台线程第0次执行!
线程1第0次执行!
线程1第1次执行!
后台线程第1次执行!
后台线程第2次执行!
线程1第2次执行!
线程1第3次执行!
后台线程第3次执行!
线程1第4次执行!
后台线程第4次执行!
后台线程第5次执行!
后台线程第6次执行!
后台线程第7次执行!

通过结果可以看到,用户线程 MyCommon 执行完毕之后,程序中只有守护线程 MyDaemon,虚拟机退出,守护线程 MyDaemon 也就结束了。

如何设置守护线程?

Thread 类 boolean 类型的 daemon 属性标志守护线程,通过 setDaemon(boolean on)方法设置守护线程。

  • 调用 setDaemon(boolean on)设置守护线程要在线程启动前,否则会抛出异常。
  • 守护线程在退出的时候并不会执行 finnaly 块中的代码,所以将释放资源等操作不要放在 finnaly 块中执行,这种操作是不安全的。
public class Daemon {
public static void main(String[] args) {
Thread thread = new Thread(new DaemonRunner(), "DaemonRunner");
thread.setDaemon(true);
thread.start();
System.out.println("主线程执行完毕");
}

static class DaemonRunner implements Runnable {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("DaemonThread finally run.");
}
}
}
}

运行 Daemon 程序,只会输出"主线程执行完毕"。

main 线程在启动了线程 DaemonRunner 之后随着 main 方法执行完毕而终止,而此时 Java 虚拟机中已经没有非 Daemon 线程,虚拟机需要退出。JDaemon 线程 DaemonRunner 立即终止,DaemonRunner 中的 finally 块并没有执行

3. 中断

中断代表线程状态,每个线程都关联了一个中断状态,用 boolean 值表示,初始值为 false。中断一个线程,其实就是设置了这个线程的中断状态 boolean 值为 true。

注意区分字面意思,中断只是一个状态,处于中断状态的线程不一定要停止运行。

Thread 类线程中断的方法:

// 设置一个线程的中断状态为true
public void interrupt() {}

// 检测线程中断状态,处于中断状态返回true
public boolean isInterrupted() {}

// 静态方法,检测调用这个方法的线程是否已经中断,处于中断状态返回true
// 注意:这个方法返回中断状态的同时,会将此线程的中断状态重置为false
public static boolean interrupted() {}
自动感知中断

以下方法会自动感知中断:

Object 类的 wait()、wait(long)、wait(long, int)
Thread 类的 join()、join(long)、join(long, int)、sleep(long)、sleep(long, int)

当一个线程处于 sleep、wait、join 这三种状态之一时,如果此时线程中断状态为 true,那么就会抛出一个 InterruptedException 的异常,并将中断状态重新设置为 false。

举例:利用中断结束线程

public class Test1 {
public static void main(String[] args) throws InterruptedException {
MyThread thread = new MyThread();
thread.start();
Thread.sleep(3000);
thread.interrupt();
}
}

class MyThread extends Thread {
int i = 0;

@Override
public void run() {
while (true) {
System.out.println(i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("中断异常被捕获了");
return;
}
i++;
}
}
}

执行结果:

0
1
2
中断异常被捕获了

MyThread 线程一直循环打印数字,3s 之后主线程将 MyThread 线程中断,MyThread 线程处于 sleep 状态会自动感应中断,抛出 InterruptedException 异常,线程结束执行。

4. join

当一个线程必须等待另一个线程执行时,就用到 join。

Thread 类中的三个 join 方法:

// 当前线程加入该线程后面,等待该线程终止。
void join()// 当前线程等待该线程终止的时间最长为 millis 毫秒。如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度void join(long millis)// 等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度void join(long millis,int nanos)

使用举例:将主线程加入到子线程后面,不过如果子线程在 1 毫秒时间内没执行完,则主线程便不再等待它执行完,进入就绪状态,等待 cpu 调度。

public class Test1 {
public static void main(String[] args) throws InterruptedException {
MyThread t = new MyThread();
t.start();
t.join(1);// 将主线程加入到子线程后面,不过如果子线程在1毫秒时间内没执行完,则主线程便不再等待它执行完,进入就绪状态,等待cpu调度
for (int i = 0; i < 30; i++) {
System.out.println("main线程第" + i + "次执行!");
}
}
}

class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("MyThread线程第" + i + "次执行!");
}
}
}

join 实现:三个 join 方法都调用同一个 join(long millis)方法,join 其实就是通过将主线程 wait 相应时间来实现的。

public final synchronized void join(long millis) throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;

if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}

if (millis == 0) {
// 只要子线程MyThread isAlve,主线程就一直挂起
while (isAlive()) {
wait(0);
}
} else {
// 1.delay时间>0,主线程wait delay时间
// 2.主线程自动唤醒之后,再次检查如果子线程MyThread isAlive且delay时间还没到就就继续将主线程wait
// 3.循环1 2 ,直到子线程MyThread执行完或者主线程wait时间超过millis
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}

5. sleep

sleep 方法是 Thread 的静态方法,sleep 让线程进入到阻塞状态,交出 CPU,让 CPU 去执行其他的任务。

sleep 方法不会释放锁。

6. yield

yield 方法是 Thread 的静态方法,yield 方法让当前正在执行的线程进入到就绪状态,让出 CPU 资源给其他的线程。

注意:

yield 方法只是让当前线程暂停一下,重新进入就绪线程池中,让系统的线程调度器重新调度器重新调度一次,完全可能出现这样的情况:当某个线程调用 yield()方法之后,线程调度器又将其调度出来重新进入到运行状态执行。

7. wait & notify/notifyAll

先来复习一下 synchronized 监视器锁 monitor 的实现原理。

Monitor 中有几个关键属性:

_owner:指向持有ObjectMonitor对象的线程
_WaitSet:存放处于wait状态的线程队列
_EntryList:存放处于等待锁block状态的线程队列
_recursions:锁的重入次数
_count:用来记录该线程获取锁的次数
  • 线程 T 中锁对象调用 wait():_owner 置为 null,计数器_count 减 1,_WaitSet 中加入 T 等待被唤醒。
  • 锁对象调用 notify():从_存放处于 wait 状态的线程队列 _WaitSet 中随意选择一个线程 T,将线程 T 从_WaitSet 中移到_EntryList 中重新去竞争锁。
bffe1b14cbdff151362a09d493f65e58.png
Monitor

同步队列(锁池/_EntryList):由于线程没有竞争到锁,只能等待锁释放之后再去竞争,此时线程就处于该对象的同步队列(锁池)中,线程状态为 BLOCKED。

等待队列(等待池/_WaitSet):线程调用了 wait 方法后被挂起,等待 notify 唤醒或者挂起时间到自动唤醒,此时线程就处于该对象的等待队列(等待池)中,线程状态为 WAITING 或者 TIMED_WAITING。

wait 方法:释放持有的对象锁,线程状态由 RUNNING 变为 WAITING,并将当前线程放置到对象的等待队列;

notify 方法:在目标对象的等待集合中随意选择一个线程 T,将线程 T 从等待队列移到同步队列重新竞争锁,线程状态由 WAITING 变为 BLOCKED。

  • 当等待集合中存在多个线程时,并没有机制保证哪个线程会被选择到。
  • 调用 notify 的线程释放锁,线程 T 竞争锁,如果竞争到锁,线程 T 从之前 wait 的点开始继续执行。

notifyAll 方法:notifyAll 方法与 notify 方法的运行机制是一样的,只是将等待队列中所有的线程全部移到同步队列。

  • wait & notify/notifyAll 这三个都是 Object 类的方法。
  • 使用 wait,notify 和 notifyAll 前提是先获得对象的锁。

总结

通过设置线程优先级属性可以改变线程被 CPU 调度的机会,需要注意线程优先级不能作为程序正确性的依赖。

Daemon 线程是一种支持型线程,在后台守护一些系统服务,当只有守护线程的时候,程序就会自然退出。

线程中断代表线程状态,每个线程都关联了一个用 boolean 值表示中断状态。当一个线程处于 sleep、wait、join 这三种状态之一时,线程中断会抛出一个 InterruptedException 的异常。

线程调度还有 Thread 类的 join、sleep、yield 方法,Object 的 wait、notify/notifyAll 方法。

参考资料

  1. 《Java 并发编程之美》
  2. 《Java 并发编程实战》
  3. 《Java 并发编程的艺术》
  4. 技术和媒体实验室-Java 并发和多线程教程: http://tutorials.jenkov.com/java-concurrency/index.html
  5. The Java® Virtual Machine Specification: https://docs.oracle.com/javase/specs/jvms/se8/html

并发系列文章汇总

【原创】01 | 开篇获奖感言
【原创】02 | 并发编程三大核心问题
【原创】03 | 重排序-可见性和有序性问题根源
【原创】04 | Java 内存模型详解
【原创】05 | 深入理解 volatile
【原创】06 | 你不知道的 final
【原创】07 | synchronized原理
【原创】08 | synchronized锁优化
【原创】09 | 基础干货
【原创】10 | 线程状态

————  e n d ————

快年底了,师长为大家准备了三份面试宝典:

《java面试宝典5.0》

《350道Java面试题:整理自100+公司》

《资深java面试宝典-视频版》

分别适用于初中级,中高级,以及资深级工程师的面试复习。

内容包含java基础、javaweb、各个性能优化、JVM、锁、高并发、反射、Spring原理、微服务、Zookeeper、数据库、数据结构、限流熔断降级等等。

d068ca926476ccb643f28720c57a0abf.png

获取方式:点“在看”,V信关注师长的小号:编程最前线并回复 面试 领取,更多精彩陆续奉上。

一、初中级《java面试宝典5.0》,对标8-13K

30203eb65a500f7e40e9d249d76649e3.png

二、中高级《350道Java面试题:整理自100+公司》,对标12-20K

7adc350036c8728e21f1d19fa2a79404.png

三、资深《java面试突击-视频版》,对标20K+

22ffe7ef47daf729dd5c58cb614a5f55.png

4ddaae850a90755c4de6fbcada231ddc.gif

在看好不好,喵~ 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值