面经手册 · 第20篇《Thread 线程,状态转换、方法使用、原理分析》

2020/12/02

作者:小傅哥
博客:https://bugstack.cn

沉淀、分享、成长,让自己和他人都能有所收获!😄

一、前言

考不常用的、考你不会的、考你忽略的,才是考试!

大部分考试考的,基本都是不怎么用的。例外的咱们不说😄 就像你做程序开发,尤其在RPC+MQ+分库分表,其实很难出现让你用一个机器实例编写多线程压榨CPU性能。很多时候是扔出一个MQ,异步消费了。如果没有资源竞争,例如库表秒杀,那么其实你确实很难接触多并发编程以及锁的使用。

但!凡有例外,比如你需要开发一个数据库路由中间件,那么就肯定会出现在一台应用实例上分配数据库资源池的情况,如果出现竞争就要合理分配资源。如此,类似这样的中间件开发,就会涉及到一些更核心底层的技术的应用。

所以,有时候不是没用,而是你没有

二、面试题

谢飞机,小记! 线程我玩定了,面试也拦不住我,我说的!

谢飞机:嘿,你好哇,我是谢飞机!

面试官:好,今天电话面试,你准备好了?

谢飞机:准备好了,嘿嘿!

面试官:嗯,我看你简历里写了不少线程的东西,看来了解的不错。问你一个线程吧那就,线程之间状态是怎么转换的?

谢飞机:扒拉扒拉,扒拉扒拉!

面试官:嗯,还不错。那 yield 方法是怎么使用的。

谢飞机:嗯!好像是让出CPU。具体的没怎么用过!

面试官:做做测试,验证下,下次问你。

三、Thread 状态关系

Java 的线程状态描述在枚举类 java.lang.Thread.State 中,共包括如下五种状态:

public enum State {
    NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED;
}

这五种状态描述了一个线程的生命周期,其实这种状态码的定义在我们日常的业务开发中,也经常出现。比如:一个活动的提交、审核、拒绝、修改、通过、运行、关闭等,是类似的。那么线程的状态是通过下图的方式进行流转的,如图 20-1

图 20-1 线程状态流转

  • New:新创建的一个线程,处于等待状态。
  • Runnable:可运行状态,并不是已经运行,具体的线程调度各操作系统决定。在 Runnable 中包含了 ReadyRunning 两个状态,当线程调用了 start() 方法后,线程则处于就绪 Ready 状态,等待操作系统分配 CPU 时间片,分配后则进入 Running 运行状态。此外当调用 yield() 方法后,只是谦让的允许当前线程让出CPU,但具体让不让不一定,由操作系统决定。如果让了,那么当前线程则会处于 Ready 状态继续竞争CPU,直至执行。
  • Timed_waiting:指定时间内让出CPU资源,此时线程不会被执行,也不会被系统调度,直到等待时间到期后才会被执行。下列方法都可以触发:Thread.sleepObject.waitThread.joinLockSupport.parkNanosLockSupport.parkUntil
  • Wating:可被唤醒的等待状态,此时线程不会被执行也不会被系统调度。此状态可以通过 synchronized 获得锁,调用 wait 方法进入等待状态。最后通过 notify、notifyall 唤醒。下列方法都可以触发:Object.waitThread.joinLockSupport.park
  • Blocked:当发生锁竞争状态下,没有获得锁的线程会处于挂起状态。例如 synchronized 锁,先获得的先执行,没有获得的进入阻塞状态。
  • Terminated:这个是终止状态,从 New 到 Terminated 是不可逆的。一般是程序流程正常结束或者发生了异常。

这里参考枚举State 类的英文注释了解了每一个状态码的含义,接下来我们去尝试操作线程方法,把这些状态体现出来。

四、Thread 状态测试

1. NEW

Thread thread = new Thread(() -> {
});
System.out.println(thread.getState());

// NEW
  • 这个状态很简单,就是线程创建还没有启动时就是这个状态。

2. RUNNABLE

Thread thread = new Thread(() -> {
});
// 启动
thread.start();
System.out.println(thread.getState());

// RUNNABLE
  • 创建的线程启动后 start(),就会进入 RUNNABLE 状态。但此时并不一定在执行,而是说这个线程已经就绪,可以竞争 CPU 资源。

3. BLOCKED

Object obj = new Object();
new Thread(() -> {
    synchronized (obj) {
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}).start();

Thread thread = new Thread(() -> {
    synchronized (obj) {
        try {
            obj.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
});

thread.start();
while (true) {
    Thread.sleep(1000);
    System.out.println(thread.getState());
}

// BLOCKED
// BLOCKED
// BLOCKED
  • 这段代码稍微有点长,主要是为了让两个线程发生锁竞争。
  • 第一个线程,synchronized 获取锁后休眠,不释放锁。
  • 第二个线程,synchronized 获取不到锁,会被挂起。
  • 那么最后的输出结果就会是,BLOCKED

4. WAITING

Object obj = new Object();
Thread thread = new Thread(() -> {
    synchronized (obj) {
        try {
            obj.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
});

thread.start();

while (true) {
    Thread.sleep(1000);
    System.out.println(thread.getState());
}

// WAITING
// WAITING
// WAITING
  • 只要在 synchronized 代码块或者修饰的方法中,调用 wait 方法,又没有被 notify 就会进入 WAITING 状态。
  • 另外 Thread.join 源码中也是调用的 wait 方法,所以也会让线程进入等待状态。

5. TIMED_WAITING

Object obj = new Object();
Thread thread = new Thread(() -> {
    synchronized (obj) {
        try {
            Thread.sleep(100000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
});
thread.start();

while (true) {
    Thread.sleep(1000);
    System.out.println(thread.getState());
}

// TIMED_WAITING
// TIMED_WAITING
// TIMED_WAITING
  • 有了上面状态获取的对比,这个状态的获取就没什么难度了。只要改成 Thread.sleep(100000); 就可以了。

6. TERMINATED

Thread thread = new Thread(() -> {
});
thread.start();

System.out.println(thread.getState());
System.out.println(thread.getState());
System.out.println(thread.getState());

// RUNNABLE
// TERMINATED
// TERMINATED
  • 这个就比较简单了,只要一个线程运行完,它的生命周期结束了,就进入了 TERMINATED 状态。

五、Thread 方法使用

一般情况下 Thread 中最常用的方法就是 start 启动,除此之外一些其他方法可能在平常的开发中用的不多,但这些方法在一些框架中却经常出现。因此只了解它们的概念,但是却缺少一些实例来参考! 接下来我们就来做一些案例来验证这些方法,包括:yield、wait、notify、join。

1. yield

yield 方法让出CPU,但不一定,一定让出!。这种可能会用在一些同时启动的线程中,按照优先级保证重要线程的执行,也可以是其他一些特殊的业务场景(例如这个线程内容很耗时,又不那么重要,可以放在后面)。

为了验证这个方法,我们做一个例子:启动50个线程进行,每个线程都进行1000次的加和计算。其中10个线程会执行让出CPU操作。那么,如果让出CPU那10个线程的计算加和时间都比较长,说明确实在进行让出操作。

案例代码

private static volatile Map<String, AtomicInteger> count = new ConcurrentHashMap<>();
static class Y implements Runnable {
    private String name;
    private boolean isYield;
    public Y(String name, boolean isYield) {
        this.name = name;
        this.isYield = isYield;
    }
    @Override
    public void run() {
        long l = System.currentTimeMillis();
        for (int i = 0; i < 1000; i++) {
            if (isYield) Thread.yield();
            AtomicInteger atomicInteger = count.get(name);
            if (null == atomicInteger) {
                count.put(name, new AtomicInteger(1));
                continue;
            }
            atomicInteger.addAndGet(1);
            count.put(name, atomicInteger);
        }
        System.out.println("线程编号:" + name + " 执行完成耗时:" + (System.currentTimeMillis() - l) + " (毫秒)" + (isYield ? "让出CPU----------------------" : "不让CPU"));
    }
}

public static void main(String[] args) {
    for (int i = 0; i < 50; i++) {
        if (i < 10) {
            new Thread(new Y(String.valueOf(i), true)).start();
            continue;
        }
        new Thread(new Y(String.valueOf(i), false)).start();
    }
}

测试结果

线程编号10 执行完成耗时2 (毫秒)不让CPU
线程编号11 执行完成耗时2 (毫秒)不让CPU
线程编号15 执行完成耗时1 (毫秒)不让CPU
线程编号14 执行完成耗时1 (毫秒)不让CPU
线程编号19 执行完成耗时1 (毫秒)不让CPU
线程编号18 执行完成耗时1 (毫秒)不让CPU
线程编号22 执行完成耗时0 (毫秒)不让CPU
线程编号26 执行完成耗时0 (毫秒)不让CPU
线程编号27 执行完成耗时1 (毫秒)不让CPU
线程编号30 执行完成耗时0 (毫秒)不让CPU
线程编号31 执行完成耗时0 (毫秒)不让CPU
线程编号34 执行完成耗时1 (毫秒)不让CPU
线程编号12 执行完成耗时1 (毫秒)不让CPU
线程编号16 执行完成耗时1 (毫秒)不让CPU
线程编号13 执行完成耗时1 (毫秒)不让CPU
线程编号17 执行完成耗时1 (毫秒)不让CPU
线程编号20 执行完成耗时0 (毫秒)不让CPU
线程编号23 执行完成耗时0 (毫秒)不让CPU
线程编号21 执行完成耗时0 (毫秒)不让CPU
线程编号25 执行完成耗时1 (毫秒)不让CPU
线程编号24 执行完成耗时1 (毫秒)不让CPU
线程编号28 执行完成耗时0 (毫秒)不让CPU
线程编号38 执行完成耗时0 (毫秒)不让CPU
线程编号39 执行完成耗时0 (毫秒)不让CPU
线程编号37 执行完成耗时1 (毫秒)不让CPU
线程编号40 执行完成耗时0 (毫秒)不让CPU
线程编号44 执行完成耗时0 (毫秒)不让CPU
线程编号36 执行完成耗时1 (毫秒)不让CPU
线程编号42 执行完成耗时1 (毫秒)不让CPU
线程编号45 执行完成耗时1 (毫秒)不让CPU
线程编号43 执行完成耗时1 (毫秒)不让CPU
线程编号46 执行完成耗时0 (毫秒)不让CPU
线程编号47 执行完成耗时0 (毫秒)不让CPU
线程编号35 执行完成耗时0 (毫秒)不让CPU
线程编号33 执行完成耗时0 (毫秒)不让CPU
线程编号32 执行完成耗时0 (毫秒)不让CPU
线程编号41 执行完成耗时0 (毫秒)不让CPU
线程编号48 执行完成耗时1 (毫秒)不让CPU
线程编号6 执行完成耗时15 (毫秒)让出CPU----------------------
线程编号7 执行完成耗时15 (毫秒)让出CPU----------------------
线程编号49 执行完成耗时2 (毫秒)不让CPU
线程编号29 执行完成耗时1 (毫秒)不让CPU
线程编号2 执行完成耗时17 (毫秒)让出CPU----------------------
线程编号1 执行完成耗时11 (毫秒)让出CPU----------------------
线程编号4 执行完成耗时15 (毫秒)让出CPU----------------------
线程编号8 执行完成耗时12 (毫秒)让出CPU----------------------
线程编号5 执行完成耗时12 (毫秒)让出CPU----------------------
线程编号9 执行完成耗时12 (毫秒)让出CPU----------------------
线程编号0 执行完成耗时21 (毫秒)让出CPU----------------------
线程编号3 执行完成耗时21 (毫秒)让出CPU----------------------
  • 从测试结果可以看到,那些让出 CPU 的,执行完计算已经在10毫秒以上,说明我们的测试是效果的。

2. wait & notify

wait 和 notify/nofityall,是一对方法,有一个等待,就会有一个叫醒,否则程序就夯在那不动了。关于这部分会使用到的 synchronized 在之前小傅哥有深入的源码分析,讲到它是怎么加锁在对象头的,如果你忘记了可以翻翻看 《synchronized 解毒,剖析源码深度分析!》

接下来我们模拟鹿鼎记·丽春院,清倌喝茶吟诗聊风月日常。当有达官贵人来时,需要分配清倌给大老爷。中间会有一些等待、叫醒操作。只为让你更好的记住这样的案例,不要想歪喽。清倌人即是只卖艺欢场人,喊麦的。

案例代码

public class 丽春院 {

    public static void main(String[] args) {
        老鸨 鸨子 = new 老鸨();

        清倌 miss = new 清倌(鸨子);
        客官 guest = new 客官(鸨子);

        Thread t_miss = new Thread(miss);
        Thread t_guest = new Thread(guest);

        t_miss.start();
        t_guest.start();
    }

}

class 清倌 implements Runnable {

    老鸨 鸨子;

    public 清倌(老鸨 鸨子) {
        this.鸨子 = 鸨子;
    }

    @Override
    public void run() {
        int i = 1;
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e1) {
                e1.printStackTrace();
            }
            if (i == 1) {
                try {
                    鸨子.在岗清倌("苍田野子", "500 日元");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } else {
                try {
                    鸨子.在岗清倌("花田岗子", "800 日元");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            i = (i + 1) % 2;
        }
    }

}

class 客官 implements Runnable {

    老鸨 鸨子;

    public 客官(老鸨 鸨子) {
        this.鸨子 = 鸨子;
    }

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e1) {
                e1.printStackTrace();
            }
            try {
                鸨子.喝茶吟诗聊风月();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

class 老鸨 {

    private String 清倌 = null;
    private String price = null;
    private boolean 工作状态 = true;

    public synchronized void 在岗清倌(String 清倌, String price) throws InterruptedException {
        if (!工作状态)
            wait();//等待
        this.清倌 = 清倌;
        this.price = price;
        工作状态 = false;
        notify();//叫醒
    }

    public synchronized void 喝茶吟诗聊风月() throws InterruptedException {
        if (工作状态)
            wait();//等待
        System.out.println("聊风月:" + 清倌);
        System.out.println("茶水费:" + price);
        System.out.println("  " + "  " + "  " + "  " + "  " + "  " + "  " + "  " + "  " + "  " + 清倌 + "完事" + "准备 ... ...");
        System.out.println("****************************************");
        工作状态 = true;
        notify();//叫醒
    }

}

测试结果

聊风月苍田野子
茶水费500 日元
                    苍田野子完事准备 ... ...
****************************************
聊风月花田岗子
茶水费800 日元
                    花田岗子完事准备 ... ...
****************************************
聊风月苍田野子
茶水费500 日元
                    苍田野子完事准备 ... ...
****************************************

...
  • 效果效果主要体现 wait、notify,这两个方法的使用。我相信你一定能记住这个例子!

3. join

join 是两个线程的合并吗?不是的!

join 是让线程进入 wait ,当线程执行完毕后,会在JVM源码中找到,它执行完毕后,其实执行notify,也就是 等待叫醒 操作。

源码jdk8u_hotspot/blob/master/src/share/vm/runtime/thread.cpp

void JavaThread::exit(bool destroy_vm, ExitType exit_type) {
	// Notify waiters on thread object. This has to be done after exit() is called
	// on the thread (if the thread is the last thread in a daemon ThreadGroup the
	// group should have the destroyed bit set before waiters are notified).
	ensure_join(this);
}
static void ensure_join(JavaThread* thread) {
  // 叫醒
  java_lang_Thread::set_thread(threadObj(), NULL);
  lock.notify_all(thread);
}

好的,就是这里!lock.notify_all(thread),执行到这,就对上了。

案例代码

Thread thread = new Thread(() -> {
    System.out.println("thread before");
    try {
        Thread.sleep(3000);
    } catch (Exception e) {
        e.printStackTrace();
    }
    System.out.println("thread after");
});
thread.start();
System.out.println("main begin!");
thread.join();
System.out.println("main end!");

测试结果

main begin
thread before
thread after
main end

Process finished with exit code 0

首先join() 是一个synchronized方法, 里面调用了wait(),这个过程的目的是让持有这个同步锁的线程进入等待,那么谁持有了这个同步锁呢?答案是主线程,因为主线程调用了threadA.join()方法,相当于在threadA.join()代码这块写了一个同步代码块,谁去执行了这段代码呢,是主线程,所以主线程被wait()了。然后在子线程threadA执行完毕之后,JVM会调用lock.notify_all(thread);唤醒持有threadA这个对象锁的线程,也就是主线程,会继续执行。

  • 这部分验证的主要体现就是加了 thread.join() 后,会影响到输出结果。如果不加,main end! 会优先 thread after 提前打印出来。
  • join() 是一个 synchronized 方法,里面调用了 wait() 方法,让持有当前同步锁的线程进入等待状态,也就是主线程。当子线程执行完毕后,我们从源码中可以看到 JVM 调用了 lock.notify_all(thread) 所以唤醒了主线程继续执行。

六、总结

  • 线程状态和状态的转换也是面试中必问的问题,但除了面试是我们自己在开发中,如果真的使用线程,是非常有必要了解线程状态是如何转换的。模模糊糊的使用,总会觉得担心,那么你是个好程序员!
  • 线程的一些深入学习都是在调用本地方法,也就是需要了解到JVM层面,才能更加深刻的见到c++代码是如何实现这部分逻辑的。
  • 在使用线程的时候一定要让自己有一个类似多核的脑子,线程一起、生死由你!本章节就扯到这了,很多的知识都是为了整套内容体系的全面,为后续介绍其他知识打下根基。感谢!

七、系列推荐

(转载本站文章请注明作者和出处 微信公众号:bugstack虫洞栈 | 作者:小傅哥

Show Disqus Comments

Post Directory

扫码或搜索:bugstack虫洞栈
发送 290992
即可立即永久解锁本站全部文章