Java多线程 -- 线程的生命周期和状态什么是上下文切换线程死锁避免死锁 sleep() 方法和 wait() 方法的区别和共同点为什么调用 start() 方法会执行 run() 方法

Posted CodeJiao

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java多线程 -- 线程的生命周期和状态什么是上下文切换线程死锁避免死锁 sleep() 方法和 wait() 方法的区别和共同点为什么调用 start() 方法会执行 run() 方法相关的知识,希望对你有一定的参考价值。

1. 线程的生命周期和状态

Java 线程在运⾏的⽣命周期中的指定时刻只可能处于下⾯ 6 种不同状态的其中⼀个状态(图源《Java并发编程艺术》4.1.4 节)。

线程在⽣命周期中并不是固定处于某⼀个状态⽽是随着代码的执⾏在不同状态之间切换。Java 线程状态变迁如下图所示(图源《Java 并发编程艺术》4.1.4 节):

由上图可以看出:

线程创建之后它将处于 NEW(新建) 状态,调用 start() 方法后开始运行,线程这时候处于 READY(可运行) 状态。可运行状态的线程获得了 cpu 时间片(timeslice)后就处于 RUNNING(运行) 状态。

操作系统隐藏 Java 虚拟机(JVM)中的 READY 和 RUNNING 状态,它只能看到 RUNNABLE 状态(图源:HowToDoInJava:Java Thread Life Cycle and Thread States),所以 Java 系统⼀般将这两个状态统称为 RUNNABLE(运行中) 状态 。

当线程执行 wait() 方法之后,线程进入 WAITING(等待)状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 TIME_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep(long millis)方法或 wait(long millis)方法可以将 Java 线程置于 TIMED WAITING状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED(阻塞) 状态。线程在执行 Runnablerun()方法之后将会进入到 TERMINATED(终止) 状态。


2. 什么是上下文切换

多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。

概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。

上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。

Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。


3. 线程死锁


3.1 死锁的概念

可以认为是两个线程或进程在请求对方占有的资源

线程死锁描述的是这样⼀种情况:多个线程同时被阻塞,它们中的⼀个或者全部都在等待某个资源被释放。由于线程被⽆限期地阻塞,因此程序不可能正常终⽌。

如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对⽅的资源,所以这两个线程就会互相等待⽽进⼊死锁状态。

3.2 出现以下四种情况会产生死锁

  • 情况一:相互排斥。一个线程或进程永远占有共享资源,比如,独占该资源。
  • 情况二:循环等待。例如,进程A在等待进程B,进程B在等待进程C,而进程C又在等待进程A。
  • 情况三:部分分配。资源被部分分配,例如,进程A和B都需要访问一个文件,同时需要用到打印机,进程A得到了这个文件资源,进程B得到了打印机资源,但两个进程都不能获得全部的资源了。
  • 情况四:缺少优先权。一个进程获得了该资源但是一直不释放该资源,即使该进程处于阻塞状态。

3.3 死锁的具体案例

线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对⽅的资源,所以这两个线程就会互相等待⽽进⼊死锁状态。

public class DeadLockDemo 
    private static final Object resource1 = new Object();//资源 1
    private static final Object resource2 = new Object();//资源 2

    public static void main(String[] args) 
        new Thread(() -> 
            synchronized (resource1) 
                System.out.println(Thread.currentThread() + "get resource1");
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) 
                    System.out.println(Thread.currentThread() + "get resource2");
                
            
        , "线程 1").start();

        new Thread(() -> 
            synchronized (resource2) 
                System.out.println(Thread.currentThread() + "get resource2");
                System.out.println(Thread.currentThread() + "waiting get resource1");
                synchronized (resource1) 
                    System.out.println(Thread.currentThread() + "get resource1");
                
            
        , "线程 2").start();
    

运行结果:


4. 避免死锁

避免死锁就需要从产生死锁的四个必要条件入手。

  • 破坏互斥条件:这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。

  • 破坏请求与保持条件:一次性申请所有的资源。

  • 破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。

  • 破坏循环等待条件:靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

我们对线程 2 的代码修改成下面这样就不会产生死锁了(破坏循环等待条件):

        new Thread(() -> 
            synchronized (resource1) 
                System.out.println(Thread.currentThread() + "get resource2");
                System.out.println(Thread.currentThread() + "waiting get resource1");
                synchronized (resource2) 
                    System.out.println(Thread.currentThread() + "get resource1");
                
            
        , "线程 2").start();

运行结果:


5. sleep() 方法和 wait() 方法的区别和共同点

  • 两者最主要的区别在于sleep 方法没有释放锁,而 wait 方法释放了锁 。
  • 两者都可以暂停线程的执行。
  • Wait 通常被用于线程间 交互 / 通信,sleep 通常被用于暂停执行。
  • wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。
  • sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout)超时后线程会自动苏醒。

5. 为什么调用 start() 方法会执行 run() 方法

为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?

new 一个 Thread,线程进入了 NEW(新建)状态;调用 start() 方法,会启动一个线程并使线程进入了 READY (就绪)状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 而直接执行 run() 方法,会把 run()方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。



以上是关于Java多线程 -- 线程的生命周期和状态什么是上下文切换线程死锁避免死锁 sleep() 方法和 wait() 方法的区别和共同点为什么调用 start() 方法会执行 run() 方法的主要内容,如果未能解决你的问题,请参考以下文章

Java多线程 线程池的生命周期及运行状态

java多线程的生命周期

Java多线程并发02——线程的生命周期与常用方法

Java 多线程系列2——多线程的生命周期及生产消费者模型

多线程基础之线程的生命周期

java线程的生命周期及五种基本状态