首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >线程基本概念

线程基本概念

作者头像
Java阿呆
发布2020-11-04 15:08:35
发布2020-11-04 15:08:35
1.1K0
举报
文章被收录于专栏:Java阿呆Java阿呆

一、程序、进程与线程

首先区分一下程序、进程与线程这三个之间的概念。

程序 (Program):程序是一段静态的代码,它是应用程序执行的蓝本

进程 (Process):进程是指一种正在运行的程序,有自己的地址空间。进程是分配资源的最小单位,一个进程可以生成多个线程,这些线程拥有共享的进程资源。 进程的特点:动态性、并发性、独立性 并发和并行的区别:多个CPU同时执行多个任务是并发;一个CPU同时执行多个任务(采用时间片)是并行。

线程 (Thread):进程内部的一个执行单元,它是程序中一个单一的顺序控制流程。线程是CPU调度的基本单位。 线程又被称为轻量级进程(Lightweight Process) 如果在一个进程中同时运行了多个线程,用来完成不同的工作,则称之为多线程。 线程的特点:轻量级进程、独立调度的基本单位、可并发执行、共享进程资源。

线程和进程的区别如下图:

二、线程的创建和启动

1.线程的创建

一般来说创建线程有三种方式: 方式一:继承java.lang.Thread类,覆写run()方法 方式二:实现java.lang.Runnable接口,实现run()方法 方式三:实现java.util.current.Callable接口,实现call()方法

这三种创建方式的比较:

  • 继承Thread类方式的多线程:   优势:编写简单;   劣势:无法继承其他父类
  • 实现Runnable接口方式的多线程:   优势:可以继承其他类,多线程可共享同一个Runnable对象   劣势:编程方式稍微复杂,如果需要访问当前线程,需要调用Thread.currentThread()方法。
  • 实现Callable接口方式的多线程:   优势:功能更强大,可以有返回值,支持泛型的返回值,可以抛出异常   劣势:需要借助FutureTask,比如返回结果。   Future接口:可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。FutureTask是Future接口唯一的实现类,FutureTask同时实现了Runnable、Future接口。它可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。

实现Runnable接口方式要通用一些。

2.线程的启动

新的线程不会自动开始运行,必须通过start方法启动。 不能直接调用run()方法来启动线程,否则run()将作为一个普通方法立即执行,执行完毕前其他线程无法并发执行。 java程序启动时,会立即创建主线程,main就是在这个线程上运行。当不再产生新线程时,程序就是单线程的。

  • 继承Thread类方式的多线程: 在主线程(主函数)创建继承了Thread类的类的实例,直接调用start方法:
代码语言:javascript
复制
public class Test extends Thread{
    @Override
    public void run(){
        //线程体
        System.out.println("threadTest");
    }

    public static void main(String[] args){
        //创建子类对象
        Test test = new Test();
        //调用start方法
        test.start();
        //不要直接调用run方法
        //test.run();
    }
}
  • 实现Runnable接口方式的多线程: 创建实例,通过代理线程启动线程:
代码语言:javascript
复制
public class Test implements Runnable{
    @Override
    public void run(){
        //线程体
        System.out.println("RunnableTest");
    }

        public static void main(String[] args){
        //真实角色
        Test test = new Test();
        //代理,同时为线程重命名为Runnable
        Thread testProxy = new Thread(test,"Runnable");
        //启动线程
        test.start();
    }
}
  • 实现Callable接口方式的多线程:
代码语言:javascript
复制
public class Test implements Callable<Integer>{
    @Override
    public Integer call() throws Exception{
        int i = 1;
        System.out.println("i:"+i++);
        return i;
    }

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        //指定线程池存放线程数
        ExecutorService ser = Executors.newFixedThreadPool(2);
        //创建线程
        Test test1 = new Test();
        Test test2 = new Test();
        //获取值
        Future<Integer> result1 = ser.submit(test1);
        Future<Integer> result2 = ser.submit(test2);

        int num1 = result1.get();
        int num2 = result2.get();

        System.out.println("num1:"+num1);
        System.out.println("num2:"+num2);
        //停止服务
        ser.shutdownNow();
    }
}

三、线程的生命周期

在线程的生命周期中,共有五个状态,分别是新生状态、就绪状态、运行状态、阻塞状态和死亡状态。

  • 新生状态(New)   用new关键字建立一个线程对象后,该线程对象就处于新生状态。   此时JVM为其分配内存,并初始化其成员变量的值;线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体;处于新生状态的线程有自己的内存空间,通过调用start方法进入就绪状态。
  • 就绪状态(Runnable)   当线程对象调用了start()方法之后,该线程处于就绪状态。   此时JVM会为其创建方法调用栈程序计数器;   处于就绪状态的线程具备了运行条件,但还没有分配到CPU,处于线程就绪队列(尽管是采用队列形式,事实上,把它称为可运行池而不是可运行队列。因为CPU的调度不一定是按照先进先出的顺序来调度的),等待系统为其分配CPU,线程并没有开始运行,当系统为一个线程分配CPU时间片后,它就会从就绪状态进入运行状态,该动作称之为“CPU调度”。
  • 运行状态(Running)   在运行状态的线程执行自己的run方法中代码,直到等待某资源而阻塞完成任务而死亡。   如果在给定的时间片内没有执行结束,就会被系统给换下来回到等待执行状态。   如果计算机只有一个CPU,那么在任何时刻只有一个线程处于运行状态;如果在一个多处理器的机器上,将会有多个线程并行执行,处于运行状态;当线程数大于处理器数时,依然会存在多个线程在同一个CPU上轮换的现象;   处于运行状态的线程最为复杂,它不可能一直处于运行状态(除非它的线程执行体足够短,瞬间就执行结束了),线程在运行过程中需要被中断,目的是使其他线程获得执行的机会,线程调度的细节取决于底层平台所采用的策略。线程状态可能会变为阻塞状态、就绪状态和死亡状态。比如:   对于采用抢占式策略的系统而言,系统会给每个可执行的线程分配一个时间片来处理任务;当该时间片用完后,系统就会剥夺该线程所占用的资源,让其他线程获得执行的机会。线程就会又从运行状态变为就绪状态,重新等待系统分配资源;   对于采用协作式策略的系统而言,只有当一个线程调用了它的yield()方法后才会放弃所占用的资源,也就是必须由该线程主动放弃所占用的资源,线程就会又从运行状态变为就绪状态。
  • 阻塞状态(Blocked)   当发生如下情况时,线程将会进入阻塞状态: 线程调用sleep()方法,主动放弃所占用的处理器资源,暂时进入中断状态(不会释放持有的对象锁),时间到后等待系统分配CPU继续执行; 线程调用一个阻塞式IO方法,在该方法返回之前,该线程被阻塞; 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有; 程序调用了线程的suspend方法将线程挂起;(suspend方法已经废弃) 线程调用wait,等待notify/notifyAll唤醒时(会释放持有的对象锁)   阻塞状态的分类: 等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态; 同步阻塞:线程在获取synchronized同步锁失败(因为锁被其它线程占用),它会进入到同步阻塞状态; 其他阻塞:通过调用线程的 sleep()或join()或发出I/O请求 时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态;   在阻塞状态的线程只能进入就绪状态,无法直接进入运行状态。而就绪和运行状态之间的转换通常不受程序控制,而是由系统线程调度所决定。当处于就绪状态的线程获得处理器资源时,该线程进入运行状态;当处于运行状态的线程失去处理器资源(或者yield方法自动让出资源)时,该线程进入就绪状态。
  • 死亡状态(Dead)   死亡状态是线程生命周期中的最后一个阶段。线程死亡的原因有三个。一个是正常运行的线程完成了它的全部工作;另一个是线程被强制性的终止,如通过执行stop方法来终止(不推荐),三是线程抛出未捕获的Exception或者Error。   处于死亡状态的线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生。 如果在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。

详细的线程状态转换图:

四、线程相关概念

1、锁

  当多个线程对同一个共享变量/对象进行操作,即使是最简单的操作,比如i++,在处理上实际也涉及到读取自增赋值这三个操作,也就是说,这中间可能存在时间差,导致多个线程没有按照程序编写者所期望的顺序去执行,出现错位,从而导致最终结果与预期的不一致。   java中的多线程同步是通过锁的概念来体现的,锁不是一个对象,也不是一个具体的东西,而是一种机制的名称。锁机制需要保证如下两种特性:

  • 互斥性:即在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程中的协调机制,这样在同一时间只有一个线程对需同步的代码块(复合操作)进行访问,互斥性我们也往往称之为操作的原子性。
  • 可见性:必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁时应获得最新共享变量的值),否则另一个线程可能是在本地缓存的某个副本上继续操作从而引起不一致。

2、挂起与休眠、阻塞与非阻塞

  • 挂起(Suspend):当线程被挂起的时候,其会失去CPU的使用时间,直到被其他线程(用户线程或调度线程)唤醒。
  • 休眠(Sleep):同样会失去CPU的使用时间,但是在过了指定的休眠时间之后,它会自动激活,无需唤醒(整个唤醒表面看是自动的,但实际上也得有守护线程去唤醒,只是不需编程者手动干预)。
  • 阻塞(Block):在线程执行时,所需要的资源不能得到,则线程被挂起,直到满足可操作的条件。
  • 非阻塞(UnBlock):在线程执行时,所需要的资源不能得到,但线程不是被挂起等待,而是继续执行其余事情,待条件满足了之后,收到了通知(同样是守护线程去做)再执行。   挂起和休眠是独立的操作系统的概念,而阻塞与非阻塞则是在资源不能得到时的两种处理方式,不限于操作系统,当资源申请不到时,要么挂起线程等待、要么继续执行其他操作,资源被满足后再通知该线程重新请求。显然非阻塞的效率要高于阻塞,相应的实现的复杂度也要高一些。

  在Java中显式的挂起之前是通过Thread的suspend方法来体现,现在此概念已经消失,原因是suspend/resume方法已经被废弃,它们容易产生死锁,在suspend方法的注释里有这么一段话:当suspend的线程持有某个对象锁,而resume它的线程又正好需要使用此锁的时候,死锁就产生了。   所以,现在的JDK版本中,挂起是JVM的系统行为,程序员无需干涉。休眠的过程中也不会释放锁,但它一定会在某个时间后被唤醒,所以不会死锁。现在我们所说的挂起,往往并非指编写者的程序里主动挂起,而是由操作系统的线程调度器去控制。   相应地有必要提下java.lang.Object的wait/notify,这两个方法同样是等待/通知,但它们的前提是已经获得了锁,且在wait(等待)期间会释放锁。在wait方法的注释里明确提到:线程要调用wait方法,必须先获得该对象的锁,在调用wait之后,当前线程释放该对象锁并进入休眠(这里到底是进入休眠还是挂起?文档没有细说,从该方法能指定等待时间来看,更可能是休眠,没有指定等待时间的,则可能是挂起,不管如何,在休眠/挂起之前,JVM都会从当前线程中把该对象锁释放掉),只有以下几种情况下会被唤醒:其他线程调用了该对象的notify或notifyAll、当前线程被中断、调用wait时指定的时间已到

3、内核态与用户态

  有一些系统级的调用,比如:清除时钟、创建进程等这些系统指令,如果这些底层系统级指令能够被应用程序任意访问的话,那么后果是危险的,系统随时可能崩溃,所以 CPU将所执行的指令设置为多个特权级别,在硬件执行每条指令时都会校验指令的特权,比如:Intel x86架构的CPU将特权分为0-3四个特权级,0级的权限最高,3权限最低。   而操作系统根据系统调用的安全性分为两种:内核态和用户态。内核态执行的指令的特权是0,用户态执行的指令的特权是3。   当一个任务(进程)执行系统调用而进入内核指令执行时,进程处于内核运行态(或简称为内核态);   当任务(进程)执行自己的代码时,进程就处于用户态。   在执行系统级调用时,需要将变量传递进去、可能要拷贝、计数、保存一些上下文信息,然后内核态执行完成之后需要再将参数传递到用户进程中去,这个切换的代价相对来说是比较大的,所以应该是尽量避免频繁地在内核态和用户态之间切换。   Java并没有自己的线程模型,而是使用了操作系统的原生线程!线程方面的事在操作系统来说属于系统级的调用,需要在内核态完成,所以如果频繁地执行线程挂起、调度,就会频繁造成在内核态和用户态之间切换,影响效率。   JDK5之前的synchronized效率低下,是因为在阻塞时线程就会被挂起、然后等待重新调度,而线程操作属于内核态,这频繁的挂起、调度使得操作系统频繁处于内核态和用户态的转换,造成频繁的变量传递、上下文保存等,从而性能较低。

4、Main线程

main线程是个非守护线程,不能设置为守护线程。   这是因为,Main线程是由Java虚拟机在启动的时候创建的。main方法开始执行的时候,主线程已经创建好并在运行了。对于运行中的线程,调用Thread.setDaemon()会抛出异常Exception in thread “main” java.lang.IllegalThreadStateException。 Main线程结束,其他线程一样可以正常运行   主线程,只是个普通的非守护线程,用来启动应用程序,不能设置成守护线程;除此之外,它跟其他非守护线程没有什么不同。主线程执行结束,其他线程一样可以正常执行。线程其实并不存在互相依赖的关系,一个线程的死亡从理论上来说,不会对其他线程有什么影响。 Main线程结束,其他线程也可以立刻结束,当且仅当这些子线程都是守护线程   Java虚拟机(相当于进程)退出的时机是:虚拟机中所有存活的线程都是守护线程。只要还有存活的非守护线程虚拟机就不会退出,而是等待非守护线程执行完毕;反之,如果虚拟机中的线程都是守护线程,那么不管这些线程的死活java虚拟机都会退出。

5、并发与并行

  并发和并行的区别就是:一个处理器同时处理多个任务和多个处理器或者是多核的处理器同时处理多个不同的任务。前者是逻辑上的同时发生(simultaneous),而后者是物理上的同时发生。   并发性(concurrency),又称共行性,是指能处理多个同时性活动的能力,并发事件之间不一定要同一时刻发生。   并行(parallelism)是指同时发生的两个并发事件,具有并发的含义,而并发则不一定并行。   来个比喻:并发和并行的区别就是一个人同时吃三个馒头和三个人同时吃三个馒头。

单就一个CPU而言两个线程可以解决线程阻塞造成的不流畅问题,其本身运行效率并没有提高,多CPU的并行运算才真正解决了运行效率问题,这也正是并发和并行的区别。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2019-05-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、程序、进程与线程
  • 二、线程的创建和启动
    • 1.线程的创建
    • 2.线程的启动
  • 三、线程的生命周期
  • 四、线程相关概念
    • 1、锁
    • 2、挂起与休眠、阻塞与非阻塞
    • 3、内核态与用户态
    • 4、Main线程
    • 5、并发与并行
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档