01.线程基础知识初识

1.进程与线程

进程与线程的由来:

计算机cpu工作原理,操作系统调度单核cpu时其实是在不停的切换任务,因为cpu同一时间只能做一件事,而且cpu做事速度很快,往往在完成一件任务时需要等待其他零件完成任务(比如硬盘读写i/o阻塞),此时为了最大限度的理由资源,cpu只能在不同任务间来回切换,就是我们所说的并发了。

但并发带来了一些问题,原先单任务现在变成了多个任务,任务之间的切换怎么进行,这时候就引入了进程的概念。

用进程来对应一个程序,每个进程对应一定的内存地址空间,并且只能使用它自己的内存空间,各个进程间互不干扰。并且进程保存了程序每个时刻的运行状态,这样就为进程切换提供了可能。

当进程暂时时,它会保存当前进程的状态(比如进程标识、进程的使用的资源等),在下一次重新切换回来时,便根据之前保存的状态进行恢复,然后继续执行。这就是并发,能够让操作系统从宏观上看起来同一个时间段有多个任务在执行。

换句话说,进程让操作系统的并发成为了可能。

而线程是进程的子集,线程也是一个应用程序最小的执行单元。

进程的出现解决了操作系统的并发问题,但是对于一个进程来说,它内部也需要执行多个子任务,比如读取,计算,实时显示,为了更大限度的提升效率,人们引入了线程的概念。进程让操作系统的并发性成为可能,而线程让进程的内部并发成为可能。

注意,一个进程虽然包括多个线程,但是这些线程是共同享有进程占有的资源和地址空间的。进程是操作系统进行资源分配的基本单位,而线程是操作系统进行调度的基本单位。

2.进程与线程的区别

  1. 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;

  2. 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线

  3. 进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段,数据集,堆等)及一些进程级的资源(如打开文件和信号等),某进程内的线程在其他进程不可见;

  4. 调度和切换:线程上下文切换比进程上下文切换要快得多

生活中的举例:你在上网时,挂着QQ,看着爱奇艺,并用迅雷下载着文件。这时候QQ,爱奇艺,迅雷就可以认为是一个个进程。而你在QQ中可以和张三、李四等人同时聊天,那么张三、李四等人的聊天界面就是一个个线程。

问题:是否多线程的性能一定就由于单线程呢?

不一定,要看具体的任务以及计算机的配置。比如说:对于单核CPU,如果是CPU密集型任务,如解压文件,多线程的性能反而不如单线程性能,

因为解压文件需要一直占用CPU资源,如果采用多线程,线程切换导致的开销反而会让性能下降。但是对于比如交互类型的任务,肯定是需要使用多线程的、而对于多核CPU,对于解压文件来说,多线程肯定优于单线程,因为多个线程能够更加充分利用每个核的资源。

虽然多线程能够提升程序性能,但是相对于单线程来说,它的编程要复杂地多,要考虑线程安全问题。

因此,在实际编程过程中,要根据实际情况具体选择。

3.java中的线程创建方法

前文我们已经对线程的概念有了一个初步的了解,那么怎么才能在JVM中创建一个线程呢 ?

在java中创建线程一般有两种方式:1)继承Thread类 2)实现Runnable接口

  • 1.通过继承Thread类来实现创建线程

    /**
     * 1 .继承Thread类
     *   继承Thread类的话,必须重写run方法,在run方法中定义需要执行的任务。
     **/
    class ThreadOne extends Thread{
          
          
        @Override
        public void run(){
          
          
            System.out.println(Thread.currentThread().getId()+"开始运行			了");
        }
    }
    
    /**
     *  创建好了自己的线程类之后,就可以创建线程对象了,然后通过start()方法去启动线 程。
     **/
    public static void main(String[] args) throws Exception{
          
          
        new ThreadOne().start();
        //jdk1.8下用 lambda表达式简化写法
        new Thread(()-> System.out.println(Thread.currentThread().getId()+"开始运行		了")).start();
    }	
    

创建好线程类后,此时线程只是一个不同的java对象,还不能称之为线程,只有通过调用start()方法才能启动该线程。 注意:不是调用run()方法启动线程,run方法中只是定义需要执行的任务,如果调用run方法,即相当于在主线程中执行ThreadOne对象中的run()方法,跟普通的方法调用没有任何区别,此时并不会创建一个新的线程来执行定义的任务。

  • 通过实行Runnable接口来创建线程
    Java为我们定义了一个Runnable接口,我们可以通过实现该接口并重写接口中的run()方法来实现线程的创建
//自定义一个Runnable接口的实现类,并重写其中run方法
class ThreadTwo implements Runnable{
    
    
    @Override
    public void run(){
    
    
        System.out.println(Thread.currentThread().getId()+"开始运行了");
    }
}
public static void main(String[] args) throws Exception{
    
    
    //创建Thread类并将接口实现做入参,并调用start方法
    new Thread(new ThreadTwo()).start();
}	

查看jdk源码我们可以发现,Runnable接口结构很简单,只是定义了一个无参、无返回值的run方法。
在这里插入图片描述

所以其实严格意义上说通过实行Runnable接口来创建线程是一种不严谨的书法,在jdk中代表线程的只有Thread这一个类,线程的执行单元就是run方法,查看Thread.run源码

在这里插入图片描述

我们可以发现,run方法只是执行了target中的run方法,而target是什么呢。查看Thread的构造函数我们可以发现:

在这里插入图片描述

这里的target就是我们传递进来的的Runnable接口的实现,Runnable的实例仅仅为线程提供了他的执行单元逻辑。

所以说创建线程有两种方式,第一种是创建一个Thread的实现类,第二种是实现Runnable接口,这种说法并不严谨。

严谨点的说法应该是:创建线程只有一种方式就是构造Thread类,而实现线程的执行单元则有两种方式:第一种是重写Thread的run方法,第二种是实现Runnable接口的run方法,并将Runnable接口的实例作为入参并构造Thread实例。

4.线程生命周期详解

线程作为程序执行的最小单位,他也有着一个完整的生命周期:

在这里插入图片描述

如上图,线程的生命周期大体分为如下5个部分:

NEW(新建) , RUNNABLE(可运行) , RUNING (运行), BLOCKED(阻塞) , TERMINATED(消亡)

下面对每个状态进行具体讲解

  • NEW 状态

    当我们创建一个线程对象,如 new Thread() 后,此时线程还没开始调用start方法启动,那么这时候的线程就处于NEW 状态。其实跟确切的说,此时线程还不存在,在没调用start() 方法前,你只是用关键字 new 创建了一个普通的Java对象。

    NEW 状态可以通过start() 进入 RUNNABLE状态。

  • RUNNABLE 状态

    线程对象创建后要想变成RUNNABLE 状态必须调用 start()方法,调用该方法后,才能算是真正的在 JVM进程中创建了一个线程。但线程一旦启动并不会马上就能得到执行,线程的运行与否与进程一样是取决于CPU的调度的,我们就把这种等待执行的状态称之 RUNNABLE(可运行)状态,表示它具备了执行资格,但是还没能真正的执行起来。

    由于此时线程并不是Running状态,所以该状态下的线程不会直接进入 BLOCKED 和 TERMINATED状态,即使在线程的执行中调用了wait、sleep或者其他block的IO操作等,也必须是线程获得了CPU的调度执行权才可以转变状态。

    RUNNABLE的线程只能意外终止 或者进入RUNNING状态。

  • RUNNING状态

    当CPU通过轮询或者其他方式从任务可执行队列中选中了线程,此时它才能真正的执行自己的内部逻辑代码,而此时线程的状态就是RUNING状态。可以说 一个正在RUNNING 状态的线程事实上也是RUNNABLE 的,但是反过来说则不成立。

    RUNNING状态下的线程状态常见转换:

    • RUNNING 变成 BLOCK 状态

      1.调用了sleep、wait方法而进入了waitSet中。

      2.进行某个阻塞的IO操作,比如网络数据的读写

      3.为获取某个锁资源,从而加入到该锁的阻塞队列中

    • RUNNING 变成 RUNNABLE 状态

      1.CPU调度器轮询使该线程放弃了执行

      2.线程主动调用了yield方法,放弃了CPU的执行权

    • RUNNING 变成 TERMINATED 状态

      1.调用stop方法

  • BLOCKED 状态

    线程由于某些原因进入了阻塞状态,详见上文,线程在BLOCKED状态可进行的状态切换:

    • 进入 RUNNABLE状态

      1.线程完成了指定时间的休眠

      2.线程阻塞的操作结束,比如读取到了想获取的数据

      3.Wait中的线程被其他线程 notify/notifyall唤醒

      4.线程获取到了某个锁资源

      5.线程在阻塞过程中被打断,比如其他线程调用了interrupt方法

    • 进入 TERMINATED 状态

      调用stop或者意外死亡(JVM Crash)

  • TERMINATED状态

    线程的最终状态,该状态的线程不会在进行状态变换,意味着整个生命周期都结束了。线程进入到TERMINATED状态的情况:

    1.线程运行正常结束,结束生命周期

    2.线程运行出错意外结束

    3.JVM Crash,导致所有线程都结束

了解线程的生命周期各状态之间的转换非常重要,每种语言具体定义的状态枚举也许不同,但总体来说都会在这5种状态的范畴之内。

猜你喜欢

转载自blog.csdn.net/weixin_43828467/article/details/110141187