美文网首页
Java线程<第二篇>:线程API详细介绍

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

作者: NoBugException | 来源:发表于2022-04-13 07:29 被阅读0次

    (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的任务比较繁重,其它任务的执行被阻塞,导致进程假死的可能。
    

    [本章完]

    相关文章

      网友评论

          本文标题:Java线程<第二篇>:线程API详细介绍

          本文链接:https://www.haomeiwen.com/subject/vplnsrtx.html