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。

在这里插入图片描述
Java多线程基础面试总结(三)_第1张图片

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() 方法,检测是不是自己被中断了。

你可能感兴趣的:(#,Java面试总结,java,后端)