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

news2024/10/4 23:32:17

线程的生命周期和状态

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.coloradmin.cn/o/416629.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java设计模式 11-代理模式

代理模式 一、 代理模式(Proxy) 1、代理模式的基本介绍 代理模式&#xff1a;为一个对象提供一个替身&#xff0c;以控制对这个对象的访问。即通过代理对象访问目标对象.这样做的好处是: 可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能。被代理的对象…

时间序列的平稳性

如何检查时间序列是否平稳&#xff0c;如果它是非平稳的&#xff0c;我们可以怎么处理 当未来的数据与现在相似时&#xff0c;它更容易建模。平稳性描述了时间序列的统计特征不随时间变化的概念。因此一些时间序列预测模型&#xff0c;如自回归模型&#xff0c;依赖于时间序列…

IoC 之 Spring 统一资源加载策略【Spring源码】

《JavaPub源码》 文末是系列文章 IoC 之 Spring 统一资源加载策略 Spring 框架的核心是 IoC&#xff08;Inversion of Control&#xff09;&#xff0c;它将应用程序的对象依赖关系管理和控制反转到容器中。在 Spring IoC 容器中&#xff0c;组件的创建和配置是通过外部配置…

IDEA 运行启动 pulsar-manager项目

IDEA 运行 pulsar-manager项目&#xff08;gradle&#xff09; 1、下载pulsar-manager源码 giithub地址 smn-manager 2、将项目导入IDEA并初始化项目 问题&#xff1a;SSL peer shut down incorrectly 将https改成http之后又会出现 Server returned HTTP response code: …

服务(第四篇)Apache的网页优化

一、网页压缩 ①首先检查是否安装 mod_deflate 模块 ②如果没有安装mod_deflate 模块&#xff0c;重新编译安装 Apache 添加 mod_deflate 模块 yum -y install gcc gcc-c pcre pcre-devel zlib-devel cd /opt/httpd-2.4.29/ ./configure \ --prefix/usr/local/httpd \ --enabl…

HDFS学习笔记 【Namenode/DN管理】

说明 DN管理管理了什么&#xff1f; NN上如何表示DN DN存储和块的关系 梳理DatanodeManager存储类 DatanodeDescriptor DN的抽象&#xff0c;依次继承。每一层增加一点额外的信息。 DatanodeId 基本的DN信息&#xff0c;hostname&#xff0c;数据传输接口&#xff0c;info服…

QTableView 设置selection-background-color和border不同时生效问题记录

问题&#xff1a; qtableview在使用过程种设置qss样式&#xff0c;设置选中时的背景色&#xff0c;以及边框颜色&#xff0c;不能同时生效。 解决&#xff1a; 在qss中设置QTableView的样式时&#xff0c;对于item项&#xff0c;selection-background-color的参数设置应该分…

在将公司“一拆六”后,阿里巴巴未来将释放出哪些投资价值?

来源&#xff1a;猛兽财经 作者&#xff1a;猛兽财经 阿里巴巴为何要将公司拆分为六大业务集团 3月28日&#xff0c;阿里巴巴集团董事会主席兼首席执行官张勇发布全员信&#xff0c;宣布启动“16N”组织变革&#xff0c;将公司拆分为六大业务集团和多家业务分公司。 在阿里巴巴…

关于FPGA(Vivado)后仿真相关问题的探讨

FPGA后仿真时&#xff0c;相比于功能仿真增加了门延时和布线延时&#xff0c;相对于门级仿真增加了布线延时&#xff0c;因此后仿真相比于功能仿真具有不同的特点。 下面所示的代码在功能仿真时是正确的的&#xff0c;但在后仿真时&#xff0c;似乎是有问题的。功能很简…

大数据项目组-月度考核汇报0102

目录 01-2023年02月-月度考核汇报 2月份完成项目情况 2月份学习情况 3月份学习计划 老师点评 02-2023年03月-月度考核汇报 项目完成情况 本月学习内容 下月学习计划 老师点评 01-2023年02月-月度考核汇报 2月份完成项目情况 MySQL数据同步到ElasticSearch任务进展&a…

linux shell 操作二进制文件(xxd、dd)

1&#xff0c;生成一个二进制文件 dd 工具 # 全零 二进制文件 dd if/dev/zero of1.bin bs1 count1024#全0xff tr \000 \377 < /dev/zero | dd ofall_0xff.bin bs1 count1024 > /dev/null xxd 工具&#xff1a;只能改单个地址 # echo "0x123" | xxd -r – xx…

【华为od2023-C++版】字符串重新排列、字符串重新排序

华为od机试题C2023个人练习使用 字符串重新排列、字符串重新排序 题目描述 给定一个字符串s&#xff0c;s包括以空格分隔的若干个单词&#xff0c;请对s进行如下处理后输出:1、单词内部调整:对每个单词字母重新按字典序排序Q 2、单词间顺序调整: 1)统计每个单词出现的次数&a…

算法刷题日志

今天是星期几就加上多少天在最后 public class Main {public static void main(String[] args) {System.out.println(Math.pow(20, 22) % 7 6);} }这题是判断左右回文&#xff0c;且要保持单调性&#xff0c;因为回文数左右对称所以只需要判断左边是否单调递增。 public class…

模拟信号隔离放大器变送器 导轨安装DIN11 IPO EM系列

概述&#xff1a; 导轨安装DIN11 IPO EM系列模拟信号隔离放大器是一种将输入信号隔离放大、转换成按比例输出的直流信号混合集成电路。产品广泛应用在电力、远程监控、仪器仪表、医疗设备、工业自控等需要电量隔离测控的行业。该模块内部嵌入了一个高效微功率的电源&#xff0…

yolov5详解与改进

https://github.com/z1069614715/objectdetection_script YOLOV5改进-Optimal Transport Assignment Optimal Transport Assignment&#xff08;OTA&#xff09;是YOLOv5中的一个改进&#xff0c;它是一种更优的目标检测框架&#xff0c;可以在保证检测精度的同时&#xff0c…

研究的艺术 (The craft of research) 读书笔记

前言 如果你对这篇文章感兴趣&#xff0c;可以点击「【访客必读 - 指引页】一文囊括主页内所有高质量博客」&#xff0c;查看完整博客分类与对应链接。 对于研究者而言&#xff0c;写作是一件很重要的事&#xff0c;好的写作不仅能让更多人愿意读&#xff0c;获得更大影响力&…

k8s学习-CKS真题-K8S安全策略PodSecurityPolicy

目录题目环境搭建解题参考题目 Task 使用 Trivy 开源容器扫描器检测 namespace kamino 中 Pod 使用的具有严重漏洞的镜像。 查找具有 High 或 Critical 严重性漏洞的镜像&#xff0c;并删除使用这些镜像的 Pod。 注意&#xff1a;Trivy 仅安装在 cluster 的 master 节点上&…

开源的全文搜索引擎总结,chatbot和chatgpt项目必备资源

全文搜索引擎就是通过从互联网上提取的各个网站的信息&#xff08;以网页文字为主&#xff09;而建立的数据库中&#xff0c;检索与用户查询条件匹配的相关记录&#xff0c;然后按一定的排列顺序将结果返回给用户。 1、Apache Lucene Java 全文搜索框架 许可证&#xff1a;Apa…

中文英文翻译-英翻中文在线翻译

如果您需要将大量的英文文件或网页翻译成中文&#xff0c;那么我们的批量翻译英文翻译中文的软件将是您最佳的选择。我们的软件是一款高效、准确、节省时间的翻译工具&#xff0c;可以在最短时间内完成大量英文文本的翻译&#xff0c;让您在工作生活中处处流畅地处理英语文本。…

【云原生进阶之容器】第六章容器网络6.4.3--Flannel网络模式

《云原生进阶之容器》专题索引: 第一章Docker核心技术1.1节——Docker综述第一章Docker核心技术1.2节——Linux容器LXC第一章Docker核心技术1.3节——命名空间Namespace第一章Docker核心技术1.4节——chroot技术第一章Docker核心技术1.5.1节——cgroup综述