Java Thread类API讲解


sleep 方法、yield 方法、currentThread 方法、setPriority 方法、interrupt 相关方法、join 方法

sleep 方法

线程的 sleep 方法会使线程休眠指定的时间长度。休眠的意思是,当前逻辑执行到此不再继续执行,而是等待指定的时间。但在这段时间内,该线程持有的 monitor 锁(可以认为对共享资源的独占标志)并不会被放弃。我们可以认为线程只是工作到一半休息了一会,但它所占有的资源并不会交还。这样设计很好理解,因为线程在 sleep 的时候可能是处于同步代码块的中间位置,如果此时把锁放弃,就违背了同步的语义。所以 sleep 时并不会放弃锁,等过了 sleep 时长后,可以确保后面的逻辑还在同步执行。

sleep 方法有两个重载,分别是:

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

两者的区别只是一个支持休眠时间到毫秒级,另外一个到纳秒级。但其实第二个并不能真的精确到纳秒级别,我们来看第二个重载方法代码:

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);
}

可以看到,最终调用的还是第一个毫秒级别的 sleep 方法。而传入的纳秒会被四舍五入。如果大于 50 万,毫秒 ++,否则纳秒被省略。

yield 方法

yield 方法我们平时并不常用。yield 单词的意思是让路,在多线程中意味着本线程愿意放弃 CPU 资源,也就是可以让出 CPU 资源。不过这只是给 CPU 一个提示,当 CPU 资源并不紧张时,则会无视 yield 提醒。如果 CPU 没有无视 yield 提醒,那么当前 CPU 会从 RUNNING 变为 RUNNABLE 状态,此时其它等待 CPU 的 RUNNABLE 线程,会去竞争 CPU 资源。讲到这里有个问题,刚刚 yield 的线程同为 RUNNABLE 状态,是否也会参与竞争再次获得 CPU 资源呢?经过我大量测试,刚刚 yield 的线程是不会马上参与竞争获得 CPU 资源的。

我们看下面测试代码:

public class YieldExampleClient {

    public static void main(String[] args) {
        Thread xiaoming = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println("小明--" + i);
                if (i == 2) {
                    Thread.yield();
                }
            }
        });

        Thread jianguo = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println("建国--" + i);
            }
        });

        xiaoming.start();
        jianguo.start();
    }
}

yield 方法为了提升线程间的交互,避免某个线程长时间过渡霸占 CPU 资源。但 yield 在实际开发中用的比较少,源码的注解也提到这一点:“It is rarely appropriate to use this method."

currentThread 方法

这是一个静态方法,用于获取当前线程的实例。用法很简单,如下:

Thread.currentThread();

拿到线程的实例后,我们还可以获取 Thread 的 名称:

Thread.currentThread().getName();

我们还可以获取线程 ID :

Thread.currentThread().getId();

setPriority 方法

此方法用于设置线程的优先级。每个线程都有自己的优先级数值,当 CPU 资源紧张的时候,优先级高的线程获得 CPU 资源的概率会更大。请注意仅仅是概率会更大,并不意味着就一定能够先于优先级低的获取。这和摇车牌号一个道理,我现在中签概率是标准的 9 倍,但摇中依然摇摇无期。而身边却时不时的出现第一次摇号就中的朋友。如果在 CPU 比较空闲的时候,那么优先级就没有用了,人人都有肉吃,不需要摇号了。

优先级别高可以在大量的执行中有所体现。在大量数据的样本中,优先级高的线程会被选中执行的次数更多。

我们看下 setPriority 的源码:

public final void setPriority(int newPriority) {
    ThreadGroup g;
    checkAccess();
    if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
        throw new IllegalArgumentException();
    }
    if((g = getThreadGroup()) != null) {
        if (newPriority > g.getMaxPriority()) {
            newPriority = g.getMaxPriority();
        }
        setPriority0(priority = newPriority);
    }
}

Thread 有自己的最小和最大优先级数值,范围在 1-10。如果不在此范围内,则会报错。另外如果设置的 priority 超过了线程所在组的 priority ,那么只能被设置为组的最高 priority 。最后通过调用 native 方法 setPriority0 进行设置。

interrupt 相关方法

interrupt 的意思是打断。第一感觉是让线程中断,其实,并不是这样。inerrupt 方法的作用是让可中断方法,比如让 sleep 中断。也就是说其中断的并不是线程的逻辑,中断的是线程的阻塞。这一点要彻底搞清池,否则带着错误的想法会影响学习的效果。

那么 interrupt 方法调用后,对未使用可中断方法的线程有影响吗?我们做个简单的实验,代码如下:

public class InterruptClient {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(()->{
            for(int i=0; i<100 ;i++){
                System.out.println("I'm doing my work");
                System.out.println("I'm interrupted?"+Thread.currentThread().isInterrupted());
            }
        });
        thread.start();
        Thread.sleep(1);
        thread.interrupt();
    }
}

线程 run 方法中没有调用可中断方法,只是输出 “I’m doing my work”,另外还会输出自己的中断状态。而主线程会 sleep 一毫秒,留时间给 thread 线程启动,然后调用 thread 线程的 interrupt 方法。我截取其中关键一段输出如下:

I'm doing my work
I'm interrupted?false
I'm doing my work
I'm interrupted?true
I'm doing my work
I'm interrupted?true

这段后面的输出一直到结束,都在重复 “I’m doing my work I’m interrupted?true“,这说明两个问题:

  1. 调用 interrupt 方法,并不会影响可中断方法之外的逻辑。线程不会中断,会继续执行。这里的中断概念并不是指中断线程;
  2. 一旦调用了 interrupt 方法,那么线程的 interrupted 状态会一直为 ture(没有通过调用可中断方法或者其他方式主动清除标识的情况下);

通过上面实现我们了解了 interrupt 方法中断的不是线程。它中断的其实是可中断方法,如 sleep 。可中断方法被中断后,会把 interrupted 状态归位,改回 false 。

join 方法

这个方法功能强大,也很实用。我们用它能够实现并行化处理。比如主线程需要做两件没有相互依赖的事情,那么可以起 A、B 两个线程分别去做。通过调用 A、B 的 join 方法,让主线程 block 住,直到 A、B 线程的工作全部完成,才继续走下去。我们来看下面这段代码:

public class JoinClient {
    public static void main(String[] args) throws InterruptedException {
        Thread backendDev = createWorker("backed dev", "backend coding");
        Thread frontendDev = createWorker("frontend dev", "frontend coding");
        Thread tester = createWorker("tester", "testing");

        backendDev.start();
        frontendDev.start();

//        backendDev.join();
//        frontendDev.join();

        tester.start();
    }

    public static Thread createWorker(String role, String work) {
        return new Thread(() -> {
            System.out.println("I finished " + work + " as a " + role);
        });
    }
}

这段代码中,我们把 join 方法去掉。执行结果如下:

I finished backend coding as a backed dev
I finished testing as a tester
I finished frontend coding as a frontend dev

我们期望的是前端和后端开发完成工作后,测试才开始测试。但从输出结果看并非如此。要想实现这个需求,我们只需把注释打开,让 backendDev 和 frontendDev 先做 join 操作,此时主线程会被 block 住。直到 backendDev 和 frontendDev 线程都执行结束,才会继续往下执行。输出如下:

I finished backend coding as a backed dev
I finished frontend coding as a frontend dev
I finished testing as a tester

可见调用 join 方法后 block 的并不是被调用的 backendDev 和 frontendDev 线程,而是调用方线程。

总结

讲解了 Thread 的几个常用的方法,这些方法在我们实际开发中会经常用到的,需要我们认真学习和理解。有些已经被弃用的方法没有再讲解,比如 stop 方法。关于更多的方法,可以直接阅读 Thread 源代码,Thread 类的注解写得相当详细。其实很多时候我们自己动手直接阅读源代码和注解,是更为快捷的学习方式,而且也更为权威。

发布了383 篇原创文章 · 获赞 54 · 访问量 21万+

猜你喜欢

转载自blog.csdn.net/hongxue8888/article/details/103795174