绕不过的并发编程--Java线程API

253 阅读20分钟

简单介绍

线程上下文切换

什么是线程上下文切换?

  • 什么是线程上下文?

    线程在执行过程中会有自己的运行条件和状态(也称上下文),比如上文所说到过的程序计数器,虚拟机栈信息等。

  • 什么时候线程会从占用 CPU 状态中退出?

    • 主动让出 CPU,比如调用了 sleep(), wait() 等。
    • 时间片用完,因为操作系统要防止一个线程或者进程长时间占用 CPU 导致其他线程或者进程饿死。
    • 调用了阻塞类型的系统中断,比如请求 IO,线程被阻塞。
    • 被终止或结束运行

这其中前三种都会发生线程切换

线程切换意味着需要保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场。并加载下一个将要占用 CPU 的线程上下文。 这就是所谓的「上下文切换」。

  • 线程上下文切换的问题

    上下文切换是现代操作系统的基本功能,因其每次需要保存信息恢复信息,这将会占用 CPU,内存等系统资源进行处理,也就意味着效率会有一定损耗,如果频繁切换就会造成整体效率低下。

OS中的阻塞和挂起

阻塞和挂起都是进程的状态

什么是阻塞?

正在执行的进程由于发生某时间(如I/O请求、申请缓冲区失败等)暂时无法继续执行。

此时引起进程调度,OS把处理机分配给另一个就绪进程,而让受阻进程处于暂停状态,一般将这种状态称为「阻塞」状态。

什么是挂起?

由于系统和用户的需要引入了「挂起」的操作,进程被挂起意味着该进程处于静止状态。

如果进程正在执行,它将暂停执行,若原本处于就绪状态,则该进程此时暂不接受调度。(被挂起进程的描述)

共同点

  • 进程都暂停执行
  • 进程都释放CPU,即两个过程都会涉及上下文切换

不同点

  • 对系统资源占用不同

    虽然都释放了CPU,但「阻塞」的进程仍处于内存中

    而「挂起」的进程通过“对换”技术被换出到外存(磁盘)中。

  • 发生时机不同

    「阻塞」一般在进程等待资源(IO资源、信号量等)时发生;

    「挂起」是由于用户和系统的需要,例如,终端用户需要暂停程序研究其执行情况或对其进行修改、OS为了提高内存利用率需要将暂时不能运行的进程(处于就绪或阻塞队列的进程)调出到磁盘

  • 恢复时机不同

    「阻塞」要在等待的资源得到满足(例如获得了锁)后,才会进入就绪状态,等待被调度而执行;

    被「挂起」的进程由将其挂起的对象(如用户、系统)在时机符合时(调试结束、被调度进程选中需要重新执行)将其主动激活

抽象思维

不妨将线程想象成小房子,我们平时实现的Runnable接口的实例相当于是任务,由线程驱动即在小房子里面才能执行。多线程情况下,我们需要给小房子的房间(代码块)加锁,确保小房子里的任务所需要的资源不会被别的房子的任务占用或者访问。

房子可能会因为任务需要进行I/O操作,申请缓存区资源失败等问题被OS撤离CPU资源。阻塞状态的房子是在申请CPU中,等待开工。

挂起状态的房子是被用户/系统搬到内存这个工作地段之外的外存中,而后面又会被主动激活被带回到内存并且分配CPU资源而开工。

我们为什么要学习「OS中的阻塞和挂起」?这是因为接下来我们要讲的sleep()方法就是一个常见的系统调用,一会的sleep过程的描述中会用到「阻塞」和「挂起」这两个概念。

Java线程相关API

本章我们会用「鱼鱼」和「摸摸」在公司摸鱼的例子来形象的解释各个API的作用。

Thread.sleep

sleep有什么用?

sleep调用后会暂停当前线程并让出CPU的执行时间,但是**sleep不会释放当前持有的对象的锁资源**,到时间后会继续执行。

sleep() 可能会抛出 InterruptedException,因为异常不能跨线程传播回 main() 中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。

sleep使用

模拟场景:

「鱼鱼」睡个5秒(sleep(time))就可以恢复精神,「摸摸」不是很相信,帮他计算时间。

测试代码:

 @Test
 public void testThreadSleep() throws InterruptedException {
     Thread fish=new Thread(()->{
         try {
             log.debug("我有点困,睡个5s能让我变得好一点");
             Thread.sleep(100L);
             log.debug("好的我睡了,你自便");
             Thread.sleep(5000L);
             log.debug("我睡醒了");
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
     },"鱼鱼");
 ​
     Thread mo=new Thread(()->{
         try{
             log.debug("好的,为了验证下我来给你计个时");
             Thread.sleep(100L);
             Long startTime=System.currentTimeMillis();
             fish.join();
             Long endTime=System.currentTimeMillis();
             log.debug("你睡觉花了"+(endTime-startTime)/1000+"s");
         }catch(InterruptedException e){
             e.printStackTrace();
         }
     },"摸摸");
 ​
     fish.start();
     mo.start();
     log.debug("开始");
     Thread.sleep(10000L);
 }

运行结果:

 17:10:39.955 [main] DEBUG com.dyh.TestThreadAPI - 开始
 17:10:39.955 [鱼鱼] DEBUG com.dyh.TestThreadAPI - 我有点困,睡个5s能让我变得好一点
 17:10:39.955 [摸摸] DEBUG com.dyh.TestThreadAPI - 好的,为了验证下我来给你计个时
 17:10:40.066 [鱼鱼] DEBUG com.dyh.TestThreadAPI - 好的我睡了,你自便
 17:10:45.077 [鱼鱼] DEBUG com.dyh.TestThreadAPI - 我睡醒了
 17:10:45.079 [摸摸] DEBUG com.dyh.TestThreadAPI - 你睡觉花了5s

sleep(0)的作用?

让出CPU,会触发操作系统立刻重新进行一次CPU竞争。

竞争的结果也许是当前线程仍然获得CPU控制权,也许会换成别的线程获得CPU控制权。

RocketMQ的源码中就出现了sleep(0)这段代码。详细可以看这里:没有二十年功力,写不出Thread.sleep(0)这一行“看似无用”的代码!

而且某些大厂面试的时候也是会问这个内容的,理解sleep方法还是很重要的。

sleep原理

sleep的核心是一个native方法:

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

进程、线程或任务(Linux中不区分进程与线程,都称为task)都可以sleep,这会导致它们暂停执行一段时间,直到等待的时间结束才恢复执行或在这段时间内被中断。

  • sleep在操作系统中的实现的大概流程

    • 「挂起」进程(或线程)并修改其运行状态

    • sleep()提供的参数来设置一个定时器。

    • 当时间结束,定时器会触发,内核收到中断后修改进程(或线程)的运行状态。

      例如线程会被标志为就绪而进入就绪队列等待调度。

  • Linux的sleep方法

     #include <stdio.h>
     #include <stdlib.h>
     #include <signal.h>
     #include <unistd.h>
     ///时钟编程 alarm()
     void wakeUp()
     {
           printf("please wakeup!!/n");
     }
     int main(void) 
     {
           printf("you have 4 s sleep!/n");
          signal(SIGALRM,wakeUp);
          alarm(4);
          //将进程挂起
          pause();
          printf("good morning!/n");
      
         return EXIT_SUCCESS;
      }
    

    Linux内核的sleep()函数是在挂起原语的基础上利用定时器实现的。

可变定时器(variable timer)一般在硬件层面是通过一个固定的时钟和计数器来实现的,每经过一个时钟周期将计数器递减,当计数器的值为0时产生「中断」。内核注册一个定时器后可以在一段时间后收到中断。

wait和notify

waitnotify有什么用?

调用 wait() 使得线程等待某个条件满足,线程在等待时会被挂起,当其他线程的运行使得这个条件满足时,其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程。

notifynotifyAll的区别

在讲这两个方法的区别之前我们需要先明确「等待队列」和「阻塞队列」的概念。

  • 等待队列和阻塞队列

    对于每个对象来说,都有自己的「等待队列」和「阻塞队列」。

    • 等待队列

      线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁后,加入锁对象的「等待队列」。

      「等待队列」中的线程不会去竞争该对象的锁。

    • 阻塞队列

      只有获取了对象的锁,线程才能执行对象的 synchronized 代码,wait的 线程A 被 线程B notify后会进入锁对象的「阻塞队列」。

      对象的锁每次只有一个线程可以获得,其他线程只能在「阻塞队列」中等待

notify方法随机唤醒对象的等待队列中的一个线程,进入阻塞队列;

notifyAll() 唤醒对象的等待队列中的所有线程,进入阻塞队列。

waitnotify使用

模拟场景:

「鱼鱼」和「摸摸」是好朋友,鱼鱼今天在办公室摸鱼,摸鱼(wait)前让摸摸记得老板来的时候要提醒(notify)他。

测试代码:

 @Test
 public void testThreadWait(){
     Object office=new Object();
     Thread fish=new Thread(()->{
         synchronized (office){
             log.debug("我先摸了,老板来了叫我。");
             try {
                 office.wait();
                 log.debug("老板来了,好好工作!");
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         }
     },"鱼鱼");
 ​
     Thread mo=new Thread(()->{
         log.debug("收到收到");
         synchronized (office) {
             log.debug("别摸鱼了,老板来了!");
             office.notify();
         }
     },"摸摸");
     fish.start();
     mo.start();
     log.debug("开始");
 }

运行结果:

 14:47:21.193 [main] DEBUG com.dyh.TestThreadAPI - 开始
 14:47:21.193 [鱼鱼] DEBUG com.dyh.TestThreadAPI - 我先摸了,老板来了叫我。
 14:47:21.193 [摸摸] DEBUG com.dyh.TestThreadAPI - 收到收到
 14:47:21.197 [摸摸] DEBUG com.dyh.TestThreadAPI - 别摸鱼了,老板来了!
 14:47:21.197 [鱼鱼] DEBUG com.dyh.TestThreadAPI - 老板来了,好好工作!

这里叫醒(notify)正在摸鱼(wait)的鱼鱼的条件就是「老板来了」。

  • waitnotify适用场景

    这样的机制十分适合生产者、消费者模式:

    消费者消费某个资源,而生产者生产该资源。

    当该资源缺失时,消费者调用wait()方法进行自我阻塞,等待生产者的生产;生产者生产完毕后调用notify/notifyAll()唤醒消费者进行消费。

(同样的是线程为了等待某个条件而等待,条件到了则唤醒

我们可以根据代码实际场景运用waitnotify的同步机制。

sleep()方法和wait()方法对比

  • 共同点

    • 两者都可以暂停线程的执行。让线程进入WAITING状态。
  • 区别

    • sleep() 方法没有释放锁,而 wait() 方法释放了锁

      为什么wait()需要释放锁?

      使用 wait() 挂起期间,线程会释放锁。

      这是因为,如果没有释放锁,那么其它线程就无法进入对象的同步方法或者同步控制块中,那么就无法执行 notify() 或者 notifyAll() 来唤醒挂起的线程,造成死锁。

    • wait() 通常被用于线程间交互/通信sleep()通常被用于暂停执行。

      (前一句话可以参考上面「摸摸」和「鱼鱼」的故事)

    • wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify()或者 notifyAll() 方法。

      sleep()方法执行完成后,线程会自动苏醒,或者也可以使用 wait(long timeout) 超时后线程会自动苏醒。

    • sleep()Thread 类的静态本地方法,wait() 则是 Object 类的本地方法

      • 为什么wait()方法不定义在Thread中?

        wait() 是让获得对象锁的线程实现等待,会自动释放当前线程占有的对象锁。

        每个对象(Object)都拥有对象锁,既然要释放当前线程占有的对象锁并让其进入 WAITING 状态,自然是要操作对应的对象(Object)而非当前的线程(Thread)。

      • 为什么sleep()方法定义在Thread中?

        因为 sleep() 是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁。

为什么wait/notify需要和synchronized一起使用?

  • 从程序执行来说:

    无论是 wait 还是 notify,如果不配合 synchronized 一起使用,在程序运行时就会报 IllegalMonitorStateException 非法的监视器状态异常,而且 notify 也不能实现程序的唤醒功能了。

  • 从逻辑上来说:

    wait是让获得对象锁的线程实现等待,在synchronized块中才能满足充分条件,notify同理。

Thread.join

什么是join方法?

joinThread类的方法,官方的说明是:Waits for this thread to die.

等待调用join的线程的任务执行结束。

join有什么用?

join方法可以控制线程的执行顺序。

主要作用就是同步,在线程A中调用了线程B的join方法时,表示只有当线程B执行完毕时,线程A才能继续执行。

join使用

模拟场景:

「鱼鱼」想要叫「摸摸」一起去吃饭,但是摸摸还要搬砖,鱼鱼等待摸摸搬完砖(join)才能一起去吃饭。

测试代码:

 @Test
 public void testThreadJoin() throws InterruptedException {
     Thread mo=new Thread(()->{
         try {
             Thread.sleep(100L);
             log.debug("等我一下,我把这个砖搬完");
             Thread.sleep(100L);
             log.debug("开始搬砖!");
             Thread.sleep(5000L);
             log.debug("搬完了,去吃饭吧!");
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
     },"摸摸");
 ​
     Thread fish=new Thread(()->{
         try {
             Thread.sleep(100L);
             log.debug("什么时候去吃饭?");
             Thread.sleep(100L);
             log.debug("好的,等你");
             mo.join(); // 等待摸摸搬完砖
             log.debug("行,去吃饭吧");
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
     },"鱼鱼");
 ​
     fish.start();
     mo.start();
     log.debug("开始");
     Thread.sleep(10000L);
 }

运行结果:

 6:28:59.170 [main] DEBUG com.dyh.TestThreadAPI - 开始
 16:28:59.276 [鱼鱼] DEBUG com.dyh.TestThreadAPI - 什么时候去吃饭?
 16:28:59.276 [摸摸] DEBUG com.dyh.TestThreadAPI - 等我一下,我把这个砖搬完
 16:28:59.388 [鱼鱼] DEBUG com.dyh.TestThreadAPI - 好的,等你
 16:28:59.388 [摸摸] DEBUG com.dyh.TestThreadAPI - 开始搬砖!
 16:29:04.402 [摸摸] DEBUG com.dyh.TestThreadAPI - 搬完了,去吃饭吧!
 16:29:04.402 [鱼鱼] DEBUG com.dyh.TestThreadAPI - 行,去吃饭吧

join原理

join核心源码:

 public final synchronized void join(long millis)
     throws InterruptedException {
     long base = System.currentTimeMillis();
     long now = 0;
 ​
     if (millis < 0) {
         throw new IllegalArgumentException("timeout value is negative");
     }
 ​
     if (millis == 0) {
         while (isAlive()) {
             wait(0);
         }
     } else {
         while (isAlive()) {
             long delay = millis - now;
             if (delay <= 0) {
                 break;
             }
             wait(delay);
             now = System.currentTimeMillis() - base;
         }
     }
 }

阅读源码我们知道join实际是利用wait方法来实现的

用上面「鱼鱼」和「摸摸」的例子来解释:

当线程fish调用线程mojoin方法的时候,fish获取到了mo的对象锁。

mo调用自身wait方法进行阻塞,只要当mo结束或者到时间后才会退出,接着唤醒线程fish继续执行。

millis为线程fish等待线程mo最长执行多久(0为永久),join源码中调用wait(0),所以fish等待直到线程mo执行结束。

Thread.yield

什么是yield

很多人翻译成「线程让步」,算是很形象了。

对静态方法 Thread.yield() 的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。

该方法只是对线程调度器的一个建议,而且也只是建议具有相同优先级的其它线程可以运行。

简单来说,调用后当前线程从运行状态变成就绪状态。CPU会在就绪状态的线程中重新选举下一个执行的线程。

yield使用

模拟场景:

有一个需求,但是「鱼鱼」和「摸摸」都不想揽活,于是他们互相推让(yield)。其中「鱼鱼」态度更坚决一些(优先级为5),于是在辩论阶段会争取到更多的话语权。

测试代码:

 @Test
 public void testThreadYield() throws InterruptedException {
     Thread fish=new Thread(()-> {
         try {
             Thread.sleep(100L);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         for (int i = 0; i < 30; i++) {
             log.debug("这个需求鱼鱼干不了,摸摸来吧");
             Thread.yield();
         }
     },"鱼鱼");
     fish.setPriority(5);
 ​
     Thread mo=new Thread(()->{
         try {
             Thread.sleep(100L);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         for (int i = 0; i < 30; i++) {
             log.debug("这个需求摸摸干不了,鱼鱼来吧");
             Thread.yield();
         }
     },"摸摸");
     fish.start();
     mo.start();
     mo.setPriority(10);
 ​
     log.debug("开始");
     Thread.sleep(10000L);
 }

运行结果:

 11:12:30.606 [main] DEBUG com.dyh.TestThreadAPI - 开始
 11:12:30.718 [摸摸] DEBUG com.dyh.TestThreadAPI - 这个需求摸摸干不了,鱼鱼来吧
 11:12:30.718 [鱼鱼] DEBUG com.dyh.TestThreadAPI - 这个需求鱼鱼干不了,摸摸来吧
 11:12:30.718 [鱼鱼] DEBUG com.dyh.TestThreadAPI - 这个需求鱼鱼干不了,摸摸来吧
 11:12:30.718 [鱼鱼] DEBUG com.dyh.TestThreadAPI - 这个需求鱼鱼干不了,摸摸来吧
 11:12:30.718 [鱼鱼] DEBUG com.dyh.TestThreadAPI - 这个需求鱼鱼干不了,摸摸来吧
 11:12:30.718 [鱼鱼] DEBUG com.dyh.TestThreadAPI - 这个需求鱼鱼干不了,摸摸来吧
 11:12:30.718 [鱼鱼] DEBUG com.dyh.TestThreadAPI - 这个需求鱼鱼干不了,摸摸来吧
 11:12:30.718 [鱼鱼] DEBUG com.dyh.TestThreadAPI - 这个需求鱼鱼干不了,摸摸来吧
 11:12:30.718 [鱼鱼] DEBUG com.dyh.TestThreadAPI - 这个需求鱼鱼干不了,摸摸来吧
 11:12:30.718 [鱼鱼] DEBUG com.dyh.TestThreadAPI - 这个需求鱼鱼干不了,摸摸来吧
 11:12:30.718 [鱼鱼] DEBUG com.dyh.TestThreadAPI - 这个需求鱼鱼干不了,摸摸来吧
 11:12:30.718 [摸摸] DEBUG com.dyh.TestThreadAPI - 这个需求摸摸干不了,鱼鱼来吧
 11:12:30.719 [摸摸] DEBUG com.dyh.TestThreadAPI - 这个需求摸摸干不了,鱼鱼来吧
 11:12:30.719 [摸摸] DEBUG com.dyh.TestThreadAPI - 这个需求摸摸干不了,鱼鱼来吧
 11:12:30.719 [摸摸] DEBUG com.dyh.TestThreadAPI - 这个需求摸摸干不了,鱼鱼来吧
 11:12:30.719 [摸摸] DEBUG com.dyh.TestThreadAPI - 这个需求摸摸干不了,鱼鱼来吧
 11:12:30.720 [摸摸] DEBUG com.dyh.TestThreadAPI - 这个需求摸摸干不了,鱼鱼来吧
 11:12:30.720 [摸摸] DEBUG com.dyh.TestThreadAPI - 这个需求摸摸干不了,鱼鱼来吧
 11:12:30.720 [摸摸] DEBUG com.dyh.TestThreadAPI - 这个需求摸摸干不了,鱼鱼来吧
 11:12:30.721 [摸摸] DEBUG com.dyh.TestThreadAPI - 这个需求摸摸干不了,鱼鱼来吧

由于鱼鱼的优先级更高,所以在推辞(yield)后还能争抢下一次CPU的时间片分配。最后鱼鱼已经先于摸摸讲完了10次推辞,摸摸只能默默的一个人无力的推辞。

Thread.interrupt

本章其实需要讲的是interruptinterrupted

一个线程在正常执行完成后会自动结束,如果在运行过程中发生异常也会提前结束。

什么是InterruptedException

通过调用一个线程的 interrupt 可以通过让线程抛出异常的方式中断该线程。

如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。

但是不能中断 I/O 阻塞和 synchronized锁阻塞。

interrupt使用

模拟场景:

今天「摸摸」休假,「鱼鱼」一个人在公司也想摸鱼睡觉,结果老板来了直接打断(interrupt)了他,导致他不能正常的从睡觉状态切换成工作状态,露出了马脚。

测试代码:

 @Test
 public void testThreadInterrupt() throws InterruptedException {
     Thread fish=new Thread(()->{
         try {
             Thread.sleep(200L);
             log.debug("白天睡大觉!");
             Thread.sleep(5000L);
             log.debug("好好工作!");// 不被打断鱼鱼就会正常工作
         }catch (InterruptedException e){
             e.printStackTrace();
         }
     },"鱼鱼");
 ​
     fish.start();
     log.debug("开始");
     Thread.sleep(200L);
     log.debug("老板来了");
     fish.interrupt();
     Thread.sleep(200L);
     log.debug("老板:都异常了,别装了,一会来我办公室");
 }

运行结果:

 16:07:20.010 [main] DEBUG com.dyh.TestThreadAPI - 开始
 16:07:20.219 [鱼鱼] DEBUG com.dyh.TestThreadAPI - 白天睡大觉!
 16:07:20.219 [main] DEBUG com.dyh.TestThreadAPI - 老板来了
 java.lang.InterruptedException: sleep interrupted
     at java.lang.Thread.sleep(Native Method)
     at com.dyh.TestThreadAPI.lambda$testThreadInterrupt$8(TestThreadAPI.java:148)
     at java.lang.Thread.run(Thread.java:748)
 16:07:20.426 [main] DEBUG com.dyh.TestThreadAPI - 老板:都异常了,别装了,一会来我办公室

什么是interrupted

只有interrupt的局限性:

如果一个线程的 run() 方法执行一个无限循环,并且没有执行 sleep() 等会抛出 InterruptedException 的操作,那么调用线程的 interrupt() 方法就无法使线程提前结束。

interruptinterrupted一般是配合的:

但是调用 interrupt() 方法会设置线程的「中断标记」,此时调用 interrupted() 方法会返回 true

因此可以在循环体中使用 interrupted() 方法来判断线程是否处于中断状态,从而提前结束线程。

interrupted使用

模拟场景:

有了上次睡觉被打断(interrupt)的经验,「鱼鱼」决定通过看一些媒体(不会抛出InterruptedException)的方式摸鱼,但是一旦注意到老板来了(interrupted()==true)就要快速切换成工作状态。

测试代码:

 @Test
 public void testThreadInterrupted() throws InterruptedException {
     Thread fish=new Thread(()->{
         String[] media={"B站","知乎","微博","小红书","爱奇艺"};
         Random random=new Random();
         int count=0;
         while(!Thread.interrupted()){// 只要没有被打断就摸鱼
             log.debug(media[random.nextInt(media.length)]
                       +"真好看,嘿嘿...");
             count++;
             if(count%19==0)// 防止一直占用CPU
                 Thread.yield();
         }
         log.debug("老板来了,好好工作!");
     },"鱼鱼");
 ​
     fish.start();
     log.debug("开始");
     Thread.sleep(50L);
     log.debug("老板来了");
     fish.interrupt();
     Thread.sleep(50L);
     log.debug("老板:这次不错嘛");
 }
 ​

运行结果:

 16:47:40.213 [main] DEBUG com.dyh.TestThreadAPI - 开始
 16:47:40.279 [鱼鱼] DEBUG com.dyh.TestThreadAPI - 爱奇艺真好看,嘿嘿...
 16:47:40.279 [鱼鱼] DEBUG com.dyh.TestThreadAPI - B站真好看,嘿嘿...
 16:47:40.279 [鱼鱼] DEBUG com.dyh.TestThreadAPI - 小红书真好看,嘿嘿...
 16:47:40.279 [鱼鱼] DEBUG com.dyh.TestThreadAPI - 微博真好看,嘿嘿...
 16:47:40.279 [鱼鱼] DEBUG com.dyh.TestThreadAPI - 小红书真好看,嘿嘿...
 16:47:40.279 [鱼鱼] DEBUG com.dyh.TestThreadAPI - 小红书真好看,嘿嘿...
 16:47:40.280 [main] DEBUG com.dyh.TestThreadAPI - 老板来了
 16:47:40.282 [鱼鱼] DEBUG com.dyh.TestThreadAPI - 老板来了,好好工作!
 16:47:40.342 [main] DEBUG com.dyh.TestThreadAPI - 老板:这次不错嘛

Thread.setDeamon

什么是Deamon

守护线程是程序运行时在后台提供服务的线程,不属于程序中不可或缺的部分。

当所有非守护线程结束时,程序也就终止,同时会杀死所有守护线程。

main() 属于非守护线程。而垃圾回收器线程就是一种守护线程。)

在线程启动之前使用 setDaemon() 方法可以将一个线程设置为守护线程。

setDeamon使用

模拟场景:

「摸摸」回来上班了,「鱼鱼」改掉了上班睡觉的习惯而变成了刷各媒体平台了,这一次鱼鱼看的出了神,摸摸害怕鱼鱼翻车,决定自发的提醒他。鱼鱼听到了提醒也决定还是好好工作,刚好就给老板撞上了。多亏了摸摸默默的守护(setDeamon(true))。

测试代码:

 @Test
 public void testSetThreadDaemon() throws InterruptedException {
     Thread fish=new Thread(()->{
         String[] media={"B站","知乎","微博","小红书","爱奇艺"};
         Random random=new Random();
         while(!Thread.interrupted()){// 只要没有被打断就摸鱼
             log.debug(media[random.nextInt(media.length)]
                       +"真好看,嘿嘿...");
         }
         log.debug("老板来了,好好工作!");
     },"鱼鱼");
 ​
     Thread mo=new Thread(()->{
         try {
             Thread.sleep(6L);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         log.debug("小心点,今天查的严");
         fish.interrupt();
     },"摸摸");
     mo.setDaemon(true);
 ​
     fish.start();
     mo.start();
 ​
     log.debug("开始");
 ​
     Thread.sleep(50L);
 ​
     log.debug("老板:这次不错嘛");
 }

运行结果:

 17:16:38.421 [main] DEBUG com.dyh.TestThreadAPI - 开始
 17:16:38.421 [鱼鱼] DEBUG com.dyh.TestThreadAPI - 爱奇艺真好看,嘿嘿...
 17:16:38.425 [鱼鱼] DEBUG com.dyh.TestThreadAPI - 微博真好看,嘿嘿...
 17:16:38.425 [鱼鱼] DEBUG com.dyh.TestThreadAPI - 知乎真好看,嘿嘿...
 17:16:38.425 [鱼鱼] DEBUG com.dyh.TestThreadAPI - 小红书真好看,嘿嘿...
 17:16:38.425 [鱼鱼] DEBUG com.dyh.TestThreadAPI - 知乎真好看,嘿嘿...
 17:16:38.426 [鱼鱼] DEBUG com.dyh.TestThreadAPI - 爱奇艺真好看,嘿嘿...
 17:16:38.426 [摸摸] DEBUG com.dyh.TestThreadAPI - 小心点,今天查的严
 17:16:38.426 [鱼鱼] DEBUG com.dyh.TestThreadAPI - 知乎真好看,嘿嘿...
 17:16:38.426 [鱼鱼] DEBUG com.dyh.TestThreadAPI - 老板来了,好好工作!
 17:16:38.476 [main] DEBUG com.dyh.TestThreadAPI - 老板:这次不错嘛

解释:

这里「摸摸」看见了「鱼鱼」摸鱼了6ms就去提醒了鱼鱼,而且过程中鱼鱼并没有释放掉CPU时间片,摸摸也能正常的运行,这是因为摸摸成了守护线程执行在后台(CPU),为用户线程鱼鱼提供服务,不和用户线程争抢时间片,两者算是并行的。

Thread.run

可以直接调用run方法吗?

这是另一个非常经典的 Java 多线程面试问题,而且在面试中会经常被问到。

new 一个 Thread,线程进入了新建状态。

调用 start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作

但是,直接执行 run() 方法,会把 run() 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

  • 测试

     @Test
     public void testThreadRun(){
         Thread fish=new Thread(()->{
             log.debug("你好我是鱼鱼");
         },"鱼鱼");
         fish.run();
     }
    

    运行结果:

     14:35:35.089 [main] DEBUG com.dyh.TestThreadAPI - 你好我是鱼鱼
    

    发现执行任务的线程并不是「鱼鱼」而是main线程。

总结: 调用 start() 方法方可启动线程并使线程进入就绪状态,直接执行 run() 方法的话不会以多线程的方式执行。

小结

本篇文章我们结合「鱼鱼」和「摸摸」上班摸鱼的例子讲解了Java线程相关 API。

相信通过本章的学习大家可以对线程 API 形成足够的印象。

本章参考: