Java多线程基础面试总结(三)

线程的生命周期和状态

Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态:

  • NEW:初始状态,线程被创建出来,但是还没有调用start()方法。
  • RUNABLE:运行中状态,调用了start()方法,Java线程将操作系统中的就绪/可运行(READY)和运行(RUNNING)两种状态统称为RUNABLE(运行中)状态。
  • BLOCKED:阻塞状态,线程阻塞于锁,需要等待锁释放。
  • WATING:等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断)。
  • TIMED_WATING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
  • TERMINATED:表示当前线程已经执行完毕。

在这里插入图片描述

  • 由上图可以看出:线程创建之后它将处于 NEW(初始) 状态,调用 start() 方法后开始运行,线程这时候处于 READY(就绪/可运行) 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 RUNNING(运行) 状态。

  • 在操作系统层面,线程有 READY 和 RUNNING 状态;而在 JVM 层面,只能看到 RUNNABLE 状态,所以 Java 系统一般将这两个状态统称为 RUNNABLE(运行中) 状态 。

  • 为什么 JVM 没有区分这两种状态呢?
    java 现在的时分(time-sharing)多任务(multi-task)操作系统架构通常都是用所谓的“时间分片(time quantum or time slice)”方式进行抢占式(preemptive)轮转调度(round-robin 式)。这个时间分片通常是很小的,一个线程一次最多只能在 CPU 上运行比如 10-20ms 的时间(此时处于 running 状态),也即大概只有 0.01 秒这一量级,时间片用后就要被切换下来放入调度队列的末尾等待再次调度。(也即回到 ready 状态)。线程切换的如此之快,区分这两种状态就没什么意义了。

  • 当线程执行 wait()方法之后,线程进入 WAITING(等待) 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态。

  • TIMED_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep(long millis)方法或 wait(long millis)方法可以将线程置于 TIMED_WAITING 状态。当超时时间结束后,线程将会返回到 RUNNABLE 状态。

  • 当线程进入 synchronized 方法/块或者调用 wait 后,(被 notify)想要重新进入 synchronized 方法/块时,但是锁被其它线程占有,这个时候线程就会进入 BLOCKED(阻塞) 状态。

  • 线程在执行完了 run()方法之后将会进入到 TERMINATED(终止) 状态。

线程控制

理解了线程生命周期的基础上,可以使用Java提供的线程控制命令对线程的生命周期进行干预。

线程控制方法详解

join()方法

Thread类提供了让一个线程等待另一个线程完成的方法——join()方法(或者说让主线程等待子线程完成)。

当在某个程序执行流中调用其他线程的join()方法时,调用线程将进入等待状态(WAITING),直到被join()方法加入的join线程执行完为止。

join()方法通常由使用线程的程序调用,以将大问题划分成许多小问题,每个小问题分配一个线程。当所有的小问题都得到处理后,再调用主线程来进一步操作。

讲解案例

上面的解释可能有些枯燥,我们来看一个简单的例子直观的感受一下join()方法的作用:

public class TestE {
    public static void main(String[] args) throws ExecutionException, InterruptedException {

        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"执行");
            }
        };

        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);

        thread1.setName("子线程1");
        thread2.setName("子线程2");
        thread1.start();
        thread2.start();
        //thread1和thread2调用了join(),主线程进入等待状态
        thread1.join();
        thread2.join();

        //主线程需要等待调用了join()方法的所有子线程执行结束后才会执行
        System.out.println("主线程执行");
    }
}

运行结果:

子线程1执行
子线程2执行
主线程执行
//让thread1和thread2优先于主线程执行,主线程进入WAITING状态,知道两个子线程执行完
thread1.join();
thread2.join();

可以替换为如下代码:

while (thread1.isAlive() || thread2.isAlive()) {
    //只要两个线程中有任何一个线程还在活动,主线程就不会往下执行
}

这两种方式效果是一样的

如果不让子线程调用join()方法,主线程执行结束后子线程才能执行:

public class TestE {
    public static void main(String[] args) throws ExecutionException, InterruptedException {

        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"执行");
            }
        };

        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);

        thread1.setName("子线程1");
        thread2.setName("子线程2");
        thread1.start();
        thread2.start();
        //不让子线程调用join()方法
        //thread1.join();
        //thread2.join();

        //主线程需要等待调用了join()方法的所有子线程执行结束后才会执行
        System.out.println("主线程执行");
    }
}

运行结果:

主线程执行
子线程1执行
子线程2执行

源码分析

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()) {
    /**
     * 关键代码,让子线程调用join()方法,意味着参数 millis=0,所以会进入这个if语句中(不明白的可以看下面的图)
     * wait(0):让线程一直等待,特别注意的是,这个wait()方法是Object的native wait方法,所以他实际生效的是当前线程,即会让主线程一直等待
     * 如下图所示,这个wait()方法是Object的方法,而不是被调用join()方法的子线程对象的
     * join()方法是用wait()方法实现,但为什么没有通过notify()系列方法唤醒呀,如果不唤醒,那不就一直等待下去了吗?
     * 原因是:在java中,Thread类线程执行完run()方法后,一定会自动执行notifyAll()方法
     */
                wait(0); 
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

调用某个线程的 join 方法,实际调用的是这个方法,然后才会调用内部的 join 方法,也就是上面的代码,同时传参 millis = 0。

在这里插入图片描述
在这里插入图片描述

sleep()方法

sleep()方法可以使线程进入WAITING状态,而且不会占用CPU资源(让出CPU给其他线程),但也不会释放锁,直到过了规定的时间后再执行后续代码,休眠期间如果被中断,会抛出异常,并会清空中断状态标记。

sleep()方法的特点

  1. sleep()方法可以使线程进入WAITING状态
    这个和wait()方法一致,都会使线程进入等待状态。

  2. 不会占用CPU资源
    不会浪费系统资源,可以放心使用。

  3. 不释放锁
    我记忆的口诀分享:sleep()方法是抱着锁睡觉。
    线程休眠期间是不会释放锁的,线程会一直保持在等待状态。

  4. 响应中断
    遇到中断请求时,支持当前线程中断,并抛出sleep interrupted异常。

sleep()方法的两种代码写法

  1. Thread.sleep(timeout)

    此方式参数只能是毫秒,如果参数是负值,则会抛出异常。虽然常见,但不推荐使用。

  2. TimeUnit.SECONDS.sleep(timeout)

    此种方式可以接收负数参数,当参数为负数,阅读源码会发现,它会跳过执行,所以不会抛出异常。此种方式的可读性高,可以指定小时、分钟、秒、毫秒、微秒等参数,所以更加优秀,推荐使用这种写法。
    例如:
    TimeUnit.MICROSECONDS.sleep(1000);
    TimeUnit.SECONDS.sleep(1000);
    TimeUnit.DAYS.sleep(1000);

源码分析:

public void sleep(long timeout) throws InterruptedException {
        //如果参数是负数,会跳过执行,所以不会抛出异常
        if (timeout > 0) {
            //自动进行时间格式转换
            long ms = toMillis(timeout);
            int ns = excessNanos(timeout, ms);
            //底层仍然是通过Thread.sleep()方法实现的,只是对Thread.sleep()方法进行了封装
            Thread.sleep(ms, ns);
        }
    }

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

共同点:

  • 两者都可以暂停线程的执行。
  • 两者都可以响应中断。

不同点:

  • sleep()方法没有释放锁,而wait()方法释放了锁。
  • sleep()方法通常用于暂停线程的执行,wait()方法通常用于线程间交互/通信。
  • sleep() 方法执行完成后,线程会自动苏醒;wait() 方法被调用后,线程不会自动苏醒,需要其他线程调用同一个对象上的 notify()或者 notifyAll() 方法。或者也可以使用 wait(long timeout) 超时后线程会自动苏醒。
  • sleep()方法是Thread 类的静态本地方法,wait() 则是 Object 类的本地方法。
  • wait()、notify()方法必须写在同步方法/同步代码块中,是为了防止死锁和永久等待,使线程更安全,而sleep()方法没有这个限制。

yield()方法

暂停当前正在执行的线程对象(即放弃当前拥有的cup资源),并执行其他线程。
yield()做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。
但是,实际中无法保证yield()一定能达到让步目的,因为让步的线程还有可能被线程调度程序再次选中,即有可能刚刚放弃但是马上又获得cpu时间片。

yield()方法详解

  1. yield()方法只是提出申请释放CPU资源,至于能否成功释放由JVM决定。由于这个特性,一般编程中用不到此方法,但在很多并发工具包中,yield()方法被使用,如AQS、ConcurrentHashMap、FutureTask等。

  2. 调用了yield()方法后,线程依然处于RUNNABLE状态,线程不会进入堵塞状态。

什么是堵塞状态?

线程状态是处于BLOCKED或WAITING或TIME_WAITING这三种统称为堵塞状态,堵塞状态下cpu是不会分配时间片的。

  1. 调用了yield()方法后,线程处于RUNNABLE状态时,线程就保留了随时被调度的权利。

yield()方法和sleep()方法有什么区别

yield()方法调用后线程处于RUNNABLE状态,而sleep()方法调用后线程处于TIME_WAITING状态,所以yield()方法调用后线程只是暂时的将调度权让给别人,但立刻可以回到竞争线程锁的状态;而sleep()方法调用后线程处于阻塞状态。

setDaemon()方法

JAVA线程分为即实线程与守护线程,守护线程是优先级低,存活与否不影响JVM退出的线程,实现守护线程的方法是在线程start()之前setDaemon(true)。

其他非守护线程关闭后无需手动关闭守护线程,守护线程会自动关闭,避免了麻烦,Java垃圾回收线程就是一个典型的守护线程。

守护线程的特点当非守护线程执行结束时,守护线程跟着销毁。当运行的唯一线程是守护线程时,Java虚拟机将退出。

案例详解

public class TestF {

    public static void main(String[] args) {

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                int i = 0;
                while(i++ < 1000){
                    System.out.println("子线程执行:"+i);
                }
            }
        });
        
        /**
         * 如果添加了这段代码:thread.setDaemon(true),证明将thread线程设置为守护线程
         * 
         * 由thread线程run()方法可知,thread线程会循环执行1000条输出语句,而主线程只会循环执行10条输出语句
         * 
         * 如果将thread线程设置为守护线程,当主线程的输出语句执行完毕时,程序就会终止,无论thread线程是否循环执行完1000条输出语句
         * 
         * 如果没有将thread线程设置为守护线程,即使主线程的输出语句已经执行完毕,程序仍然不会终止,直到thread线程循环执行完1000条输出语                               句,程序才会终止
         */
        thread.setDaemon(true);
        
        thread.start();

        for(int i =0; i < 10; i++){
            System.out.println("主线程执行:"+i);
        }

    }
}

interrupt()方法

interrupt()的作用其实也不是中断线程,而是通知线程应该中断了,具体来说,当对一个线程调用interrupt() 时:

  • 如果线程处于被阻塞状态(如果线程在调用 Object 类的 wait()、wait(long) 或 wait(long, int) 方法,或者该Thread类的 join()、join(long)、join(long, int)、sleep(long) 或 sleep(long, int) 方法后处于阻塞状态),那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。
  • 如果线程处于正常活动状态,那么会将该线程的中断标志设置为true,仅此而已。被设置中断标志的线程将继续正常运行,不受影响。

interrupt() 并不能真正的中断线程,需要被调用的线程自己进行配合才行,在正常运行任务时,经常检查本线程的中断标志位,如果被设置了中断标志就自行停止线程。具体到底中断还是继续运行,应该由被通知的线程自己处理。

interrupted()方法

检查当前中断标识(即查看当前中断信号是true还是false),并清除中断信号,一般处理过中断以后使用此方法。

换句话说,如果这个方法连续被调用两次,那么第二次调用将返回false(除非当前线程在第一次调用清除其中断状态之后、第二次调用检查它之前再次被中断)。

当一个线程调用另一个线程的interrupt()方法时,会中断该线程并设置线程的中断状态标志。如果一个线程被阻塞在一个等待状态(如sleep()、wait()、join()等)中,则该线程会立即抛出InterruptedException异常并清除中断状态标志。但如果一个线程没有处于等待状态,那么该线程就需要自己检查中断状态并进行相应的处理。
interrupted()方法可以用于在不抛出异常的情况下检查线程的中断状态,并清除中断状态标志。如果中断状态标志被设置为true,则表示当前线程已被中断,可以根据需要进行相应的处理。同时,interrupted()方法会清除中断状态标志,以便后续的代码不会检测到一个已经被处理过的中断标志。

isInterrupted()

检查当前中断标识(即查看当前中断信号是true还是false)

如果想详细了解这个问题,可以参考这篇文章——interrupt()、interrupted()和isInterrupted()你真的懂了吗

stop()方法

强制线程停止执行。

stop() 方法会真的杀死线程,不给线程喘息的机会,如果线程持有 ReentrantLock 锁,被 stop() 的线程并不会自动调用 ReentrantLock 的 unlock() 去释放锁,那其他线程就再也没机会获得 ReentrantLock 锁,这实在是太危险了。所以该方法就不建议使用了,类似的方法还有 suspend() 和 resume() 方法,这两个方法同样也都不建议使用了,所以这里也就不多介绍了。

而 interrupt() 方法就温柔多了,interrupt() 方法仅仅是通知线程,线程有机会执行一些后续操作,同时也可以无视这个通知。被 interrupt 的线程,是怎么收到通知的呢?一种是异常,另一种是主动检测。

当线程 A 处于 WAITING、TIMED_WAITING 状态时,如果其他线程调用线程 A 的 interrupt() 方法,会使线程 A 返回到 RUNNABLE 状态,同时线程 A 的代码会触发 InterruptedException 异常。上面我们提到转换到 WAITING、TIMED_WAITING 状态的触发条件,都是调用了类似 wait()、join()、sleep() 这样的方法,我们看这些方法的签名,发现都会 throws InterruptedException 这个异常。这个异常的触发条件就是:其他线程调用了该线程的 interrupt() 方法。

当线程 A 处于 RUNNABLE 状态时,并且阻塞在 java.nio.channels.InterruptibleChannel 上时,如果其他线程调用线程 A 的 interrupt() 方法,线程 A 会触发 java.nio.channels.ClosedByInterruptException 这个异常;而阻塞在 java.nio.channels.Selector 上时,如果其他线程调用线程 A 的 interrupt() 方法,线程 A 的 java.nio.channels.Selector 会立即返回。

上面这两种情况属于被中断的线程通过异常的方式获得了通知。还有一种是主动检测,如果线程处于 RUNNABLE 状态,并且没有阻塞在某个 I/O 操作上,例如中断计算圆周率的线程 A,这时就得依赖线程 A 主动检测中断状态了。如果其他线程调用线程 A 的 interrupt() 方法,那么线程 A 可以通过 isInterrupted() 方法,检测是不是自己被中断了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/12573.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

什么是 AUTOSAR C++14?

总目录链接>> AutoSAR入门和实战系列总目录 总目录链接>> AutoSAR BSW高阶配置系列总目录 文章目录 什么是 AUTOSAR C14&#xff1f;AUTOSAR C14 规则和偏差静态分析工具可以完全支持自动 什么是 AUTOSAR C14&#xff1f; 它是 C 版本 14 (ISO/IEC 14882:2014…

layui框架学习(23:代码文本修饰模块)

Layui中的代码文本修饰模块layui.code主要用于修饰代码区域或文本行&#xff0c;其基本用法是使用预设类layui-code标识包含代码或文本的元素&#xff0c;然后调用layui.code函数渲染样式。Layui官网教程及示例中主要使用pre元素包含带修饰的代码或文本&#xff08;pre元素可定…

Qt在安卓手机输出‘hello,world‘

我也想实现这样的功能。 最开始的参考文章&#xff1a; (2条消息) Qt android 开发环境搭建_逝水流年丶轻染尘的博客-CSDN博客 方案1&#xff1a;(失败) 我之前已经下载过 Qt5.14.2了&#xff0c;所以我想直接添加组件 中间过程参考&#xff1a; (2条消息) Qt更新组件出现&…

Kubernetes 多集群管理工具Rancher 使用介绍

目录 一、Rancher 简介 2.1 Run Kubernetes Everywhere 2.2 满足 IT 需求规范 2.3 赋能 DevOps 开发团队 2.4 Rancher API Server 的功能 2.4.1授权和角色权限控制 2.4.2使用 Kubernetes 的功能 2.4.3 配置云端基础信息 2.4.4 查看集群信息 2.5 编辑下游集群 二、Ra…

在线免费把Markdown格式文件转换为PDF格式

用CSDN的MarkDown编辑器在线转换 CSDN的MarkDown编辑器说实话还是挺好用的。 导出PDF操作步骤&#xff0c;图文配合看&#xff1a; 在MD编辑模式下写好MarkDown文章或者直接把要转换的MarkDown贴进来&#xff1b; 使用预览模式&#xff0c;然后在预览文件上右键选择打印&…

部分面试题记录

Spring相关&#xff1a; 1. spring框架中的单例bean是线程安全的嘛&#xff1f; 1.1 bean单例多例配置&#xff1a; bean可以手动设置单例或者多例&#xff1a; Service Scope("singleton") public class UserServicelmpl implements UserService { }singleton:b…

溯源取证-Linux内存取证 中难度篇

原谅我这么晚才出来文章&#xff0c;因为最近忙着录课&#xff0c;至于为啥没有基础篇&#xff0c;是因为靶场里没看见&#xff0c;哈哈 这个也是研究了好几个晚上才出来的东西&#xff0c;此处场景为linux环境下的rootkit病毒&#xff0c;我们通过这篇文章可以通过内存取证发…

基于广泛数据增强的新型白质束ONE-SHOT分割

文章目录 One-Shot Segmentation of Novel White Matter Tracts via Extensive Data Augmentation摘要方法One-Shot分割的广泛数据增强 实验结果 One-Shot Segmentation of Novel White Matter Tracts via Extensive Data Augmentation 摘要 探索了新WM束的One-Shot分割问题由…

揭秘阿里新大招:大模型只是前菜

技术的代际变革往往对商业格局产生深远影响。眼下&#xff0c;随着产业各界对大模型的投入布局加深&#xff0c;一个业界共识逐渐诞生&#xff1a;大模型正在改变云生态&#xff0c;将为云计算行业带来新的奇点。 在过去的一段时间里&#xff0c;市场研究机构IDC在2022年底的预…

瑞吉外卖项目——读写分离

读写分离 读和写所有压力都由一台数据库承担&#xff0c;压力大数据库服务器磁盘损坏则数据丢失&#xff0c;单点故障 Mysql主从复制 介绍 MySQL主从复制是一个异步的复制过程&#xff0c;底层是基于Nysql数据库自带的二进制日志功能。 就是一台或多台MysQL数据库&#xf…

偶数科技发布实时湖仓数据平台 Skylab 5.0

2023 年 4 月 11 日&#xff0c; 偶数发布了最新的实时湖仓数据平台 Skylab 5.0&#xff0c;平台各个组件进行了全面升级。新版 Skylab 的发布标志着偶数科技具有从数据存储、计算、管理到分析、应用和 AI 能力的完整的数据管理生态闭环&#xff0c;帮助用户实现批流一体、实时…

LeetCode LCP 04. 覆盖【二分图最大匹配,匈牙利算法】困难

本文属于「征服LeetCode」系列文章之一&#xff0c;这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁&#xff0c;本系列将至少持续到刷完所有无锁题之日为止&#xff1b;由于LeetCode还在不断地创建新题&#xff0c;本系列的终止日期可能是永远。在这一系列刷题文章…

[Java]Session机制

什么是Session Session是另一种记录客户状态的机制&#xff0c;不同的是Cookie保存在客户端浏览器中&#xff0c;而Session保存在服务器上。客户端浏览器访问服务器的时候&#xff0c;服务器把客户端信息以某种形式记录在服务器上。这就是Session。客户端浏览器再次访问时只需…

springboot整合redis

一、总体概述 1、redis配置文件 redis.conf配置文件&#xff0c;改完后确保生效&#xff0c;记得重启&#xff0c;记得重启 默认daemonize no 改为 daemonize yes 默认protected-mode yes 改为 protected-mode no 默认bind 127.0.0.1 改为 直接注释掉(默认bind 127.0.0.1只能…

ApplicationContextAware接口

一、ApplicationContextAware接口的基本介绍 public interface ApplicationContextAware extends Aware {void setApplicationContext(ApplicationContext applicationContext) throws BeansException;}在Spring/SpringMVC中&#xff0c;我们拿到IOC容器无非有三种方式&#x…

HCIE-Cloud Computing LAB常见问题收集谱

第一题&#xff1a;FusionCompute 扩容CNA与对接共享存储 FusionCompute&#xff1a;关联存储资源失败 物理阵列里面太多没清理的了。然后去排查问题&#xff0c;存储地址也正确&#xff0c;管理接口也互联&#xff0c;IQN号也修改了&#xff0c;结果是启动器快满了 排查网…

Javaweb基础配置模板(mybatis+javaweb)

1.大纲规划图 本配置涉及的技术:mybatis,javaweb,json转换&#xff0c;分页查询等 2.导入相关的配置文件pom.xml 2.1 依赖文件 <dependencies> <!-- 测试依赖--><dependency><groupId>junit</groupId><artifactId>junit</artifact…

如何把视频里的声音提取出来,4种有效方法学起来

在我们日常生活中&#xff0c;可能会有需要从视频文件中提取音频的情况&#xff0c;比如想要将视频中的歌曲或语音内容提取出来&#xff0c;或者电脑上看视频时&#xff0c;总有一些很有意思的BGM&#xff0c;想录下来或者提取出来单独使用&#xff0c;不过有些小伙伴可能不知道…

[abc复盘] abc297 20230409

[atc复盘] abc297 20230409 一、本周周赛总结A - Double Click1. 题目描述2. 思路分析3. 代码实现 B - chess9601. 题目描述2. 思路分析3. 代码实现 C - PC on the Table1. 题目描述2. 思路分析3. 代码实现 D - Count Subtractions1. 题目描述2. 思路分析3. 代码实现 E - Kth …

AOP通知中获取数据

AOP通知中获取数据 之前我们写AOP仅仅是在原始方法前后追加一些操作&#xff0c;接下来我们要说说AOP中数据相关的内容&#xff0c;我们将从获取参数、获取返回值和获取异常三个方面来研究切入点的相关信息。 获取切入点方法的参数&#xff1a;所有的通知类型都可以获取参数 …