JAVA进阶系列 - 并发编程 - 第5篇 Thread API

在上一篇中介绍了 Thread 类的构造方法,可是光有构造方法也不够,我们还得再学习多一些该类常用的 API 才行,这样才能对该类有更深刻的了解,同时也能让我们有更多的选择。

Thread类提供的API有几十个,由于篇幅问题,本篇文章仅选择几个有代表性的来进行讲解。剩余的API小伙伴们感兴趣的可以通过源码进行查看,也可以给我留言,我们共同探讨共同学习。

目标

  1. currentThread
  2. setPriority
  3. yield
  4. sleep
  5. interrupt
  6. interrupted
  7. join

内容

1. currentThread

该方法用于返回当前执行线程的引用,我们可以在代码块中通过它来获取当前的线程对象,虽然看起来很简单,但是使用非常广泛,在后续的内容中都会大量使用到该方法。

方法:

public static native Thread currentThread();

代码:

/**
 * 这个例子我们可以看到 Thread.currentThread() 这里拿到的都是各自的执行线程引用对象。
 */
public class CurrentThreadDemo {
    public static void main(String[] args) {
        // 打印结果为:true
        Thread t = new Thread(() -> {
            System.out.println("t".equals(Thread.currentThread().getName()));
        }, "t");
        t.start();
        // 打印结果为:true
        System.out.println("main".equals(Thread.currentThread().getName()));
    }
}

2. setPriority

进程有进程的优先级,线程同样也有优先级,理论上是优先级比较高的线程会优先被CPU进行调度,但事实上往往并不会如你所愿。

如果CPU比较忙,设置优先级可能会获得更多的CPU时间片,但是在CPU空闲的情况下,设置优先级几乎不会有任何作用。所以,我们不要试图在程序设计中使用优先级来绑定某些业务或者让业务依赖于优先级,这产生的结果可能会与你所期望的结果不一致。

方法:

public final void setPriority(int newPriority); // 设置线程优先级
public final int getPriority(); // 获取线程优先级

案例:

/**
 * t1 线程的优先级比 t2 线程的低,正常来说应该统计数量会比 t2 线程的少
 * 但是我这里随机测试统计到的次数为:
 *  t1: 59573
 *  t2: 34321
 * 不同的CPU资源情况会有不同的运行结果,小伙伴们可以多测试几次看看
 */
public class PriorityDemo {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (true) {
                System.out.println("t1");
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            while (true) {
                System.out.println("t2");
            }
        }, "t2");
        // 最小值为:1,中间值为:5,最大值为:10
        t1.setPriority(9);
        t2.setPriority(10);

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

3. yield

yield方法属于一种启发式的方法,它给调度程序的提示是当前线程愿意放弃对处理器的当前使用。调度程序可以随意忽略此提示。应将其使用与详细的性能分析和基准测试结合起来,以确保它实际上具有所需的效果。

此方法很少被使用。对于调试或测试目的,它可能很有用,因为它可能有助于重现由于竞争条件而产生的错误。

方法:

public static native void yield();

案例:

/**
 * 将注释部分放开的话可以看到控制台输出的结果有时候是 0 在前面,有时候是 1 在前面
 * 而将该部分注释之后,你会发现一只都是 0 在前面
 */
public class YieldDemo {
    public static void main(String[] args) {
        IntStream.range(0, 2).mapToObj(YieldDemo::test).forEach(Thread::start);
    }

    private static Thread test(int index){
        return new Thread(() -> {
            // if (index == 0){
            //     Thread.yield();
            // }
            System.out.println(index);
        });
    }
}

4. sleep

sleep是一个静态方法,根据系统计时器和调度程序的精度和准确性,使当前正在执行的线程进入休眠状态(暂时停止执行)达指定的毫秒数。该线程不会失去任何监视器的所有权(例如monitor锁,关于monitor锁在后续的文章中会进行详细讲解)。

方法:

public static native void sleep(long millis);   // 休眠的毫秒数
public static void sleep(long millis, int nanos);   // 休眠的毫秒数与纳秒数

案例:

/**
 * 在这个例子中,我们分别在自定义线程喝主线程中进行了休眠,每个线程的休眠互不影响
 * Thread.sleep() 只会导致当前线程休眠指定的时间
 */
public class SleepDemo {
    public static void main(String[] args) {
        new Thread(() -> {
            long startTime = System.currentTimeMillis();
            sleep(2000);
            System.out.printf("%s线程耗时:%d%s", Thread.currentThread().getName(), System.currentTimeMillis() - startTime, "ms");
            System.out.println("");
        }, "t").start();

        long startTime = System.currentTimeMillis();
        sleep(3000);
        System.out.printf("%s线程耗时:%d%s", Thread.currentThread().getName(), System.currentTimeMillis() - startTime, "ms");
    }

    private static void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

yield 和 sleep 的区别:

  • yield 只是对 CPU 调度器的一个提示,如果生效了,会导致线程上下文的切换;
  • sleep 会导致当前线程暂停指定的时间,没有 CPU 时间片的消耗;
  • yield 会使线程由 RUNNING 状态进入 RUNNABLE 状态;
  • sleep 会使线程短暂 block,之后在给定的时间内释放 CPU 资源;
  • yield 不能保证一定生效,而 sleep 几乎百分百的按照指定时间进行休眠(最终休眠时间要以系统的定时器和调度器的精度为准)
  • yield 无法捕获中断信号,而 sleep 被另一个线程打断则能捕获到中断信号

5. interrupt

线程interrupt是一个非常重要的API,也是经常使用的方法,如果在调用:

  1. Object类的 wait(),wait(long)或wait(long,int)方法或join(),join(long),join(long,int)的方法时阻塞了此线程,此类的sleep(long)或sleep(long,int)方法,则其中断状态将被清除,并将收到InterruptedException

  2. InterruptibleChannel 的 I/O 操作,则该通道将被关闭,该线程的中断状态将被设置,并且该线程将收到java.nio.channels.ClosedByInterruptException

  3. Selector 的 wakeup 方法,则将设置该线程的中断状态,并且它将立即从选择操作中返回(可能具有非零值),就像调用选择器的唤醒方法一样。

与之相关的API还有几个。

方法:

public void interrupt();    // 中断阻塞
public static boolean interrupted();    // 判断当前线程是否被中断,该方法会直接擦除掉线程的标识
public boolean isInterrupted(); // 判断当前线程是否被中断,仅做判断不影响标识
// interrupted 和 isInterrupted 方法都是调用本地方法 isInterrupted() 来实现,该方法中的参数 ClearInterrupted 主要用来控制是否擦除线程 interrupt 的标识,该标识被擦除后,后续的所有判断都将会是 false
private native boolean isInterrupted(boolean ClearInterrupted);

案例:

/**
 * 新建一个线程 t,休眠 1分钟
 * 主线程休眠 2秒钟之后,对线程 t进行打断,控制台输出:interrupt,程序结束
 */
public class InterruptDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            try {
                Thread.sleep(60 * 1000);
            } catch (InterruptedException e) {
                System.out.println("interrupt");
            }
        }, "t");
        t.start();

        Thread.sleep(2000);
        t.interrupt();
    }
}

在线程内部中存在一个名为 interrupt flag 的标识,如果一个线程被 interrupt,那么它的flag将被设置,在源码中我们也可以看到有对应的描述。

public void interrupt() {
    if (this != Thread.currentThread())
        checkAccess();

    synchronized (blockerLock) {
        Interruptible b = blocker;
        if (b != null) {
            interrupt0();           // Just to set the interrupt flag
            b.interrupt(this);
            return;
        }
    }
    interrupt0();
}

但是如果当前线程正在执行可中断方法被阻塞时,调用interrupt方法将其中断,反而会导致flag被清楚,关于这点在后面的文章中还会详细介绍。

7. join

Thread 的 join 方法同样也是一个非常重要的方法,使用它的特性可以实现很多比较强的功能,在 Thread 类中给我们提供了三个不同的 方法,具体如下。

方法:

public final void join();   // 永久等待该线程生命周期结束
public final synchronized void join(long millis);   // 设置最长等待毫秒值,为 0 则永久等待
public final synchronized void join(long millis, int nanos); // 设置最长等待毫秒值与纳秒值,为 0 则永久等待

案例:

/**
 * 创建两个线程 1 和 2 并分别启动,在控制台进行输出。
 * 同时main线程调用这两个线程的方法,这时你会发现线程 1 和 2 会交替的输出直到两个线程都运行完毕
 * 此时main线程才开始循环输出,如果将 join 方法注释掉,则三个线程会同时进行输出
 */
public class JoinDemo {
    public static void main(String[] args) throws InterruptedException {
        List<Thread> list = IntStream.range(1, 3).mapToObj(JoinDemo::getThread).collect(Collectors.toList());

        list.forEach(Thread::start);

        for (Thread thread : list) {
            thread.join();
        }

        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "-" + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private static Thread getThread(int name){
        return new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + "-" + i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, String.valueOf(name));
    }
}

总结

在本篇文章中,我们学习了 Thread 的一些较常见的 API,Thread 的 API 是掌握高并发编程的基础,非常有必要熟练掌握!

今天的文章到这里就结束了,小伙伴们有什么建议或者意见请联系我改进哦,你们的支持是我最大的动力!!!
本文由博客群发一文多发等运营工具平台 OpenWrite 发布

推荐阅读更多精彩内容