专栏首页Java进阶架构师【原创】Java并发编程系列11 | 线程调度

【原创】Java并发编程系列11 | 线程调度

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

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 中重新去竞争锁。

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》

本文分享自微信公众号 - java进阶架构师(java_jiagoushi),作者:何适

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-05-21

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 论如何优雅的使用和理解线程池

    平时接触过多线程开发的童鞋应该都或多或少了解过线程池,之前发布的《阿里巴巴 Java 手册》里也有一条:

    java进阶架构师
  • 【原创】Java并发编程系列15 | 重入锁ReentrantLock

    AQS是java.util.concurrent包的核心基础组件,是实现Lock的基础。那么AQS是如何实现Lock的呢?

    java进阶架构师
  • 【原创】Java并发编程系列16 | 公平锁与非公平锁

    上一篇提到重入锁 ReentrantLock 支持两种锁,公平锁与非公平锁。那么这篇文章就来介绍一下公平锁与非公平锁。

    java进阶架构师
  • Android多线程:你必须要了解的多线程基础知识汇总

    版权声明:本文为博主原创文章,未经博主允许不得转载,更多请继续关注Carson_Ho htt...

    Carson.Ho
  • 线程的生命周期

    线程的六种状态: NEW、RUNNABLE、BIOCKED、WAITING、TIME_WAITING、TERMINATED。

    用户7386338
  • 没想到,这么简单的线程池用法,深藏这么多坑!

    生产有个对账系统,每天需要从渠道端下载对账文件,然后开始日终对账。这个系统已经运行了很久,前两天突然收到短信预警,没有获取渠道端对账文件。

    andyxh
  • Java 线程池中的线程复用是如何实现的?

    那么就来和大家探讨下这个问题,在线程池中,线程会从 workQueue 中读取任务来执行,最小的执行单位就是 Worker,Worker 实现了 Runnabl...

    用户1516716
  • Java多线程:线程属性

    喜欢天文的pony站长
  • JUC线程池ThreadPoolExecutor源码分析

    很早之前就打算看一次JUC线程池ThreadPoolExecutor的源码实现,由于近段时间比较忙,一直没有时间整理出源码分析的文章。之前在分析扩展线程池实现可...

    Throwable
  • ReentrantLock 源码解读

    | 傻瓜源码-内容简介 |

    傻瓜源码

扫码关注云+社区

领取腾讯云代金券