Java线程<第二篇>:线程API详细介绍

2022-04-13  本文已影响0人  NoBugException

(1)休眠 sleep

sleep 是一个静态方法,它有两个重要的重载,分别是:

public static native void sleep(long millis);
public static void sleep(long millis, int nanos);

第一个重载方法是本地方法,形参是一个以毫秒为单位的时间整数;
第二个重载方法源码中有具体实现:

public static void sleep(long millis, int nanos)
throws InterruptedException {
    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (nanos < 0 || nanos > 999999) {
        throw new IllegalArgumentException(
                            "nanosecond timeout value out of range");
    }

    if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
        millis++;
    }

    sleep(millis);
}

它有两个形参,分别是以毫秒为单位的时间,以及以纳秒为单位的时间整数。
从源码中分析,当满足 nanos >= 500000 或者 nanos != 0 && millis == 0 条件之后,毫秒数自增1,最终依然调用了 sleep 的本地方法。

下面是具体使用方法:

Thread.sleep(3000); // 当前线程休眠 3000 毫秒
Thread.sleep(3000, 500000); // 当前线程休眠 3001 毫秒
Thread.sleep(3000, 800000); // 当前线程休眠 3001 毫秒
Thread.sleep(3000, 400000); // 当前线程休眠 3000 毫秒
Thread.sleep(0, 400000); // 当前线程休眠 3001 毫秒
Thread.sleep(0, 800000); // 当前线程休眠 3001 毫秒

从逻辑上,具体的休眠时间根据源码可以计算出,但是,需要知道的是,这个时间精度要以系统的定时器和调度器的精度为准。

(2)TimeUnit 替代 Thread.sleep

实际上,我们几乎不会使用 Thread.sleep,TimeUnit 同样可以实现一样的效果:

代码的具体使用如下:

    try {
        TimeUnit.HOURS.sleep(3); // 时
        TimeUnit.MINUTES.sleep(3); // 分
        TimeUnit.SECONDS.sleep(3); // 秒
        TimeUnit.MILLISECONDS.sleep(3000); // 毫秒
        TimeUnit.MICROSECONDS.sleep(3001); // 微妙
        TimeUnit.NANOSECONDS.sleep(3001); // 纳秒
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

TimeUnit 可以直接指定时间单位,比 Thread.sleep 的可读性更高,源码如下:

public void sleep(long timeout) throws InterruptedException {
    if (timeout > 0) {
        long ms = toMillis(timeout);
        int ns = excessNanos(timeout, ms);
        Thread.sleep(ms, ns);
    }
}

从源码上分析,TimeUnit 本质上使用的是 Thread.sleep, 两者对比,TimeUnit 的优点如下:

(1)可读性更高:TimeUnit 指定了时间单位,传入的时间参数更加易懂;
(2)扩展性更好:TimeUnit 可以指定的时间单位有:日、时、分、秒、微妙、纳秒;

(3)线程 yield

yield 方法属于启发式的方法,其会提醒调度器我愿意放弃当前的 CPU 资源,如果 CPU 的资源不紧张,则会忽略这种提醒。

调用 yield 方法会使当前线程从 RUNNING 状态切换到 RUNNABLE 状态,一般这个方法不太常用。

先看下以下代码:

private static Thread createThread(int index) {
    return new Thread(new Runnable() {
        @Override
        public void run() {
            if (index == 1) {
                System.out.println("放弃当前CUP资源");
                Thread.yield();
            }
            System.out.println(index);
        }
    });
}

public static void main(String[] args) {
    for (int index = 1; index < 3; index++) {
        createThread(index).start();
    }
}

假设CPU是单核CPU,那么CPU的调度结果见下图:

单核CPU,使用yield调度流程.jpg

(4)线程优先级

thread.setPriority(7);
threadGroup.setMaxPriority(10);

一般情况下,不会对线程设定优先级,更不会让某些业务严重地依赖线程的优先级别。

(5)获取线程ID

thread.getId();

调用 getId 可以获取线程唯一 ID,线程的 ID 在整个 JVM进程中都会是唯一的,并且是从 0 开始逐次递增。

(6)获取当前线程

Thread.currentThread();

(7)线程上下文类加载器

thread.getContextClassLoader();

获取线程上下文加载器,简单来说就是这个线程是由哪个类加载器加载的,如果是在没有修改线程上下文加载器的情况下,则保持与父线程同样的类加载器。

thread.setContextClassLoader(ClassLoader classLoader);

设置该线程的类加载器,这个方法可以打破 JAVA 类加载器的父委托机制,有时候该方法也被称为 JAVA 类加载器的后门。

(8)线程 interrupt

interrupt 是中断的意思,当线程进入阻塞状态时,执行 interrupt 会让线程退出阻塞状态,interrupt 被称之为中断方法

能够让线程阻塞的方法有:

(1)Object 的 wait 方法;
(2)Thread 的 sleep 方法;
(3)Thread 的 join 方法;
(4)io 操作;
(5)其它方法;

一旦线程在阻塞的情况下被打断,都会抛出一个称为 InterrupttedException 异常,这个异常就像一个 signal(信号)一样通知当前线程被打断了。

演示代码如下:

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("thread线程开始");
                try {
                    TimeUnit.MINUTES.sleep(1); // 休眠 1 分钟
                } catch (InterruptedException e) {
                    System.out.println("捕获了InterruptedException异常");
                }
                System.out.println("thread线程结束");
            }
        });
        thread.start();
        TimeUnit.SECONDS.sleep(3); // 休眠 3 秒
        thread.interrupt(); // 中断
        TimeUnit.SECONDS.sleep(3); // 休眠 3 秒
        System.out.println("main线程结束");
    }

执行结果如下:

thread线程开始
捕获了InterruptedException异常
thread线程结束
main线程结束

(9)线程 isInterrupted

isInterrupted 是 Thread 的一个成员方法,注意和 Thread 的静态方法 interrupted 之间的区别。

isInterrupted 方法是判断线程是否是中断状态,下面举两个例子:

举例一:

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                }
            }
        });
        thread.start();
        TimeUnit.SECONDS.sleep(3); // 休眠 3 秒
        System.out.println("是否是中断状态1:" + thread.isInterrupted());
        thread.interrupt(); // 中断
        TimeUnit.SECONDS.sleep(3); // 休眠 3 秒
        System.out.println("是否是中断状态2:" + thread.isInterrupted());
    }

在线程中,执行了一个无限执行的 while 语句,当执行到 thread.interrupt() 时,interrupter 标识不会被擦除,因此,在执行 thread.interrupt() 之前中断状态是 false,在执行 thread.interrupt() 之后中断状态是 true。

是否是中断状态1:false
是否是中断状态2:true

虽然中断状态被置为 true,但是不代表线程被中断,实际上,线程中的 while 仍然在执行。

一般情况下,interrupt 方法需要和阻塞方法一起使用,比如下面一段代码实现:

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    TimeUnit.MINUTES.sleep(1); // 休眠 1 分钟
                } catch (InterruptedException e) {
                    System.out.println("捕获了InterruptedException异常");
                }
            }
        });
        thread.start();
        TimeUnit.SECONDS.sleep(3); // 休眠 3 秒
        System.out.println("是否是中断状态1:" + thread.isInterrupted());
        thread.interrupt(); // 中断
        TimeUnit.SECONDS.sleep(3); // 休眠 3 秒
        System.out.println("是否是中断状态2:" + thread.isInterrupted());
    }

当执行 thread.interrupt() 后,InterruptedException 异常被捕获,与此同时,interrupt 标志被擦除,输出结果是:

是否是中断状态1:false
捕获了InterruptedException异常
是否是中断状态2:false

执行 thread.interrupt() 之后,isInterrupted 的返回值依然是 false,原因是:线程的阻塞方法被中断之后,interrupt 标志会被擦除。

(10)静态方法 interrupted

interrupted 是一个静态方法,它的作用是获取是否是 interrupt 状态,它的作用和 thread.interrupt() 是一样的,但是它们却有本质的区别:

(1)thread.interrupt():只有使用阻塞方法并捕获到 InterruptedException 异常之后才会擦除 interrupt 状态;如果没有阻塞方法,那么 interrupt 状态不会被擦除;
(2)Thread.interrupted():Thread.interrupted() 第一次执行返回的是真实的 interrupt 状态,执行之后 interrupt 状态被擦除;当第二次执行 Thread.interrupted() 后, interrupt 状态已经被擦除,所以第二次执行的返回值是false;

(10)线程 join

join 和 sleep 一样,都是阻塞方法,都可以捕获到中断信号,并且擦除线程的 interrupt 标识,它有3个重载方法,分别是:

thread.join();
thread.join(3000);
thread.join(3000, 1);

演示代码如下:

    /**
     * 创建一个线程,名称为 index
     * @param index
     * @return
     */
    private static Thread create(int index) {
        return new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程 " +index + " 开始");
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程 " +index + " 结束");
            }
        }, String.valueOf(index));
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println("线程 main " + "开始");
        for (int index = 1; index < 3; index++) {
            Thread thread = create(index);
            thread.start();
            thread.join();
        }
        System.out.println("线程 main " + "结束");
    }

在 main 方法(main 线程)中,创建了两个线程,先 start,再 join,join 方法会使 main 方法暂时阻塞;
名称为 1 的线程是首先 join,名称为 2 的线程是第二个 join,这两个线程的执行是顺序的(串行的),当执行完名称为 1 的线程之后才会执行名称为 2 的线程;
当执行 join方法 的线程全部执行完毕之后,才继续执行 main 方法;
以上代码的打印结果如下:

线程 main 开始
线程 1 开始
线程 1 结束
线程 2 开始
线程 2 结束
线程 main 结束

join 方法还可以传递具体的时间,比如,将 thread.join() 改成 thread.join(3000);
即线程1执行3秒之后,开始执行线程2,但是线程1会继续执行;
线程2执行3秒之后,main 线程 和 线程 1 都继续执行;

最终的打印结果如下:

线程 main 开始
线程 1 开始
线程 2 开始
线程 1 结束
线程 main 结束
线程 2 结束

(11)如何关闭一个线程

JDK有一个被弃用的 stop 方法,该方法在关闭线程的时候可能不会释放掉 monitor 的锁,这里不建议使用该方法结束线程。

那么,应该如何关闭呢?

(1)等待线程正常结束,完成线程的生命周期;
(2)如果含有阻塞方法,那么可以捕获中断异常,执行线程的 interrupt 方法;
(3)使用一个变量控制是否需要停止线程的开关,为了在多线程的情况下线程安全,那么需要添加volatile关键字,代码如下:

public class MyThread extends Thread {

    private volatile boolean isClosed = false;

    @Override
    public void run() {
        while (!isClosed && !isInterrupted()) {

        }
        System.out.println("线程执行结束");
    }

    /**
     * 关闭线程
     */
    public void closed () {
        isClosed = true; // 关闭标识
        interrupt(); // 中断
    }
}

public static void main(String[] args) throws InterruptedException {
    MyThread myThread = new MyThread();
    myThread.start();
    TimeUnit.SECONDS.sleep(5);
    myThread.closed(); // 关闭线程
}

(4)程序异常退出
(5)进程假死

有时候JVM的任务比较繁重,其它任务的执行被阻塞,导致进程假死的可能。

[本章完]

上一篇下一篇

猜你喜欢

热点阅读