zl程序教程

您现在的位置是:首页 >  后端

当前栏目

Java多线程基础(一)---线程的创建和生命周期

JAVA多线程基础线程 创建 --- 生命周期
2023-09-11 14:22:11 时间

在这里插入图片描述

1.学习内容

1、同步与异步和并发与并行概念
2、程序、进程和线程的区别
3、多线程的创建方式
4、start源码分析
5、线程的生命周期

2.具体内容

2.1 同步与异步

同步思想:所有的操作都做完,才返回给用户。这样用户在线等待的时间太长,给用户一种卡死了的感觉(就是系统迁移中,点击了迁移,界面就不动了,但是程序还在执行,卡死了的感觉)。这种情况下,用户不能关闭界面,如果关闭了,即迁移程序就中断了。
异步思想:将用户请求放入消息队列,并反馈给用户,系统迁移程序已经启动,你可以关闭浏览器了。然后程序再慢慢地去写入数据库去。这就是异步。但是用户没有卡死的感觉,会告诉你,你的请求系统已经响应了。你可以关闭界面了。

同步,是所有的操作都做完,才返回给用户结果。即写完数据库之后,在响应用户,用户体验不好,一个操作没有完成结果就不会给客户端。
异步,不用等所有操作等做完,就相应用户请求。即先响应用户请求,然后慢慢去写数据库,用户体验较好。

  • 举例:打个比方,比如我们去购物,如果你去商场实体店买一台空调,当你到了商场看中了一款空调,你就想售货员下单。售货员去仓库帮你调配物品。这天你热的实在不行了。就催着商家赶紧给你配送,于是你就等在商场里,等候着他们,直到商家把你和空调一起送回家,一次愉快的购物就结束了。这就是同步调用。
    不过,现在我们可以在网上订购一台空调。当你完成网上支付的时候,对你来说购物过程已经结束了。虽然空调还没有送到家,但是你的任务都已经完成了。商家接到你的订单后,就会加紧安排送货,当然这一切已经跟你无关了,你已经支付完成,想干什么就能去干什么了。等送货上门的时候,签收即可。这就是异步调用。

2.2 并发和并行的区别

并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的(串行),只是把时间分成若干段,使多个进程快速交替的执行。这就好像两个人用同一把铁锨,轮流挖坑,一小时后,两个人各挖一个小一点的坑,要想挖两个大一点得坑,一定会用两个小时。当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状态。

并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。就好像两个人各拿一把铁锨在挖坑,一小时后,每人一个大坑。所以无论从微观还是从宏观来看,二者都是一起执行的。当系统有一个以上CPU时,则线程的操作有可能非并发.当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行

2.3 程序、进程和线程

  • 程序:是计算机指令的集合,是一组静态的指令库,不占用系统运行的资源,不能被系统调度,也不能作为独立运行的单位,它以文件的形式存储在磁盘上。

  • 进程:是一个程序在其自身地址空间中一次执行活动。比如:打开一个记事本,就是调用了一个进程,进程是资源申请、调度和独立运行的单位,因此,它使用系统中运行的资源;而程序不能申请系统资源,一个程序可以对应多个进程。

  • 线程:线程又称为轻量级进程,是进程中一个单一的连续控制流程。它和进程一样拥有独立的执行单元,每一个线程都有自己的局部变量表、程序计数器(指向正在执行的指令指针)以及各自的生命周期,由操作系统负责调度,区别在于线程没有独立的存储空间,而是和所属进程中的其他线程共享一个存储空间,这使得线程间的通信比进程简单。
    线程和进程的关系:一个进程可以拥有多个线程,在每一个进程中至少拥有一个线程(Thread)。

线程与进程的比较

线程具有进程的许多特征,故又称轻量级进程

在引入线程的OS中,每一进程都拥有多个线程,至少一个。

  • 调度

在传统OS中,拥有资源、独立调度和分派的基本单位都是进程,在引入线程
的系统中,线程是调度和分派的基本单位,而进程是拥有资源的基本单位。

在同一个进程内线程切换不会产生进程切换,由一个进程内的线程切换到另一个进程内的线程时,将会引起进程切换。

  • 并发性

在引入线程的系统中,进程之间可并发,同一进程内的各线程之间也能并发执行。因而系统具有更好的并发性。

  • 拥有资源

无论是传统OS,还是引入线程的OS,进程都是拥有资源的独立单位,线程一般不拥有系统资源,但它可以访问隶属进程的资源。即一个进程的所有资源可供进程内的所有线程共享。

  • 系统开销

进程的创建和撤消的开销要远大于线程创建和撤消的开销,进程切换时,当前进程的CPU环境要保存,新进程的CPU环境要设置,线程切换时只须保存和设置少量寄存器,并不涉及存储管理方面的操作,可见,进程切换的开销远大于线程切换的开销。

同时,同一进程内的各线程由于它们拥有相同的地址空间,它们之间的同步和通信的实现也变得比较容易。

多线程实例分析:
为什么要用多线程,举一个常见的例子:假设我使用同程艺龙查询
咸阳机场-广州白云机场的航班, 我发起这个查询请求,APP中没有实时数据,需要到各大航空公司去获取信息,最后需要统一整理加工返回到同程艺龙APP。
在这里插入图片描述

  • 该例子是典型的串行任务局部并行化处理,每一个公司的接口不一样,获取的数据格式也不一样,查询速度也存在着差异,如果再跟航空公司进行串行化交互,客户端要等待很长时间,用户体验会非常差。
    解决方案:我们将每一个航空公司的查询交给一个线程去工作,然后在它们结束工作后统一对数据进行处理,这样既可以节约时间,也能够提升用户体验效果(汪文君Java高并发)。

2.4多线程的实现–Thread
例子:使用继承实现–thread

package com.kangna.cur.first;

public class PrimeThread extends Thread{
	public void run(){
		for( int i = 0; i < 50; i++){
			System.out.println("当前线程的id:" + Thread.currentThread().getId() + "--i=" + i);
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

多线程的实现–Runnable

package com.kangna.cur.first;

import java.util.concurrent.TimeUnit;

public class SecondThread implements Runnable {
	@Override
	public void run() {
		for(int i = 0; i < 50; i++){
			System.out.println("当前线程:" + Thread.currentThread().getId() + "--s=" + i);
			try {
				TimeUnit.MILLISECONDS.sleep(200);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

测试类

package com.kangna.cur.first;

public class TestThread {
	public static void main(String args[]){
		PrimeThread prime = new PrimeThread();
		prime.start();
		
		new Thread(new SecondThread()).start();
		for(int k = 0; k < 100; k++){
			System.out.println("当前线程的id:" + Thread.currentThread().getId() + "--k=" + k);
			try {
				Thread.sleep(200);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

测试类中启动了三个线程,测试的结果中三个线程交替执行且每次执行的结果是不一样的,因为每次CPU的调度情况是不一样的,程序运行结果如下:
在这里插入图片描述

  • 创建多线程的其他方式如:匿名内部类和callable方式不介绍,后面会介绍callable方式,现在只学习最一般的创建方式。

面试题:Thread继承和Runnable接口
实际开发中一般使用Runnable接口的方式比较多,因为:

  1. 通过继承Thread类的方式,可以完成多线程的建立。但是这种方式有一个局限性,如果一个类已经有了自己的父类,就不可以继承Thread类而实现Runnable接口可以避免单继承的局限性;

  2. 使用Runnable将线程的控制和业务逻辑的运行彻底分离开来;

  3. Runnable和Thread的run最大的不同就是Thread的run方法是不能共享的,也就是说A线城不能把B线程的run方法当做自己的执行单元,而使用Runnable接口则很容易就能实现,使用用一个Runnable的实例可以构造不同的Thread实例。

2.4 start方法源码分析

    public synchronized void start() {
      
        if (threadStatus != 0)
            throw new IllegalThreadStateException();
        group.add(this);

        boolean started = false;
        try {
            start0();  //JNI
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }
    private native void start0();
    @Override
    public void run() {
    //如果构造Thread时传递了Runnable,则会执行runnable的run方法
        if (target != null) {
            target.run();
        }
     //否则需要重写Thread类的run方法
}

我们看到start方法的重点在于start0这个本地方法,就是JNI(Java native
interface)方法,其实看看方法注释和JDK文档,我们知道start方法中调用了start0方法,在开始执行这个线程的时候,JVM将会调用该线程的run方法,也就是说,run方法是被JNI方法start0()调用的。

介绍一下JNI:native方法是通过java中的JNI实现的,java与本地已编译的代码交互,通常会丧失平台可移植性。但是,有些情况下这样做是可以接受的,甚至是必须的,比如,使用一些旧的库,与硬件、操作系统进行交互,或者为了提高程序的性能。JNI标准至少保证本地代码能工作在任何Java 虚拟机实现下。

阅读源码总结如下几点:

  • Thread被构造后的new状态,事实上threadstatus内部属性为0,此时它并处于执行状态,和普通的Java对象没有什么区别,在没有start之前该线程还不存在;
  • 不能两次启动Thread,否则会抛异常;
  • 线程启动后被添加到一个ThreadGroup中;
  • 一个线程的生命周期结束,也就是到了Terminated状态;

2.5 线程的生命周期

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Terminated)5种状态。尤其是当线程启动以后,它不可能一直"霸占"着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换,这些线程状态的切换需要用到线程定义的数据结构(局部变量表、程序计数器,以及生命周期)

  1. 新建状态,当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时仅由JVM为其分配内存,并初始化其成员变量的值

  2. 就绪状态,当线程对象调用了start()方法之后,该线程处于就绪状态。Java虚拟机会为其创建方法调用栈和程序计数器,等待调度运行

  3. 运行状态,如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态

  4. 阻塞状态,当处于运行状态的线程失去所占用资源之后,便进入阻塞状态

  5. 死亡状态,在线程的生命周期当中,线程生命周期的终点

线程生命周期状态图
线程生命周期状态图如上,下面来具体解释一下每个状态:
线程状态详解

  1. 线程的New状态

当我们使用一个new创建一个Thread对象时,此时它并不处于执行状态,因为没有调用start方法启动该线程,那么线程的状态为new状态,准确的说,它仅仅是Thread对象状态,因为在没有start之前,该线程是根本不存在的,与我们使用new关键字创建的普通对象是没什么区别的,

  1. 线程的Runnable状态

线程对象进入Runnable状态必须调用start方法,那么此时才是真正的在JVM中创建了一个线程,但是并不是一启动就可以立刻执行,线程的运行与否和进程一样要由CPU调度,我们称为可执行状态(Runnable),也就是说它具备执行的资格(预备役),然而并没有真正的执行起来,而是等待CPU的调度。
由于存在Running状态,所以不会直接进入Block状态 和Terminated,即使是线程的执行中调用wait、sleep或者其他block的IO操作,这些都必须先获得CPU的执行权才可以,Runnable状态只能意外终止或者进入Running状态。

  1. 线程的Running状态
    一旦CPU的轮询或者其他方式从任务的可执行队列中选中了线程,此时该线程才可以执行自己的逻辑代码,一个Running状态的线程是Runnable的,反之不成立。
    在Running状态下可以发生如下状态的转换:

    • 直接进入Terminated,比如调用JDK中不推荐使用的stop方法;

    • 进入block,比如调用了sleep,或者wait方法进入waitSet中;

    • 进行某IO操作,如进行网络数据的读写进入Block状态;

    • 获取某个锁资源,从而进入到该锁阻塞队列中而进入到Block状态;

    • 由于CPU的轮询调度使该线程放弃执行,进入Runnable状态;

    • 线程主动调用yield方法,放弃CPU执行权,进入Runnable状态。

  2. 线程的Blocked状态
    线程进行IO操作或获取某个锁资源就会进入Blocked状态,线程的在Blocked状态中可以切换到如下的状态:

    • 直接进入Terminated状态,如调用stop方法;
    • 线程阻塞操作的结束,如读取了想要的数据字节进入到Runnable状态;
    • 线程完成了指定休眠时间,进入到Runnable状态;
    • wait线程被notify或notifyall唤醒,进入Runnable状态;
    • 线程获取到了某个锁资源,进入Runnable;
    • 线程在阻塞中被打断,如线程调用interrupt方法,进入到Runnable状态。
  3. 线程的Terminated状态
    线程的最终状态,意味着线程的整个生命周期结束了,且不会切换到任何其它状态,下列情况会进入到Terminated状态:

    • 线程的正常结束;

    • 线程运行出错意外结束;

    • JVM Crash,导致所有线程都结束了

3总结

  1. 概括介绍了关于多线程的有关概念(罗列了别人的概念)
  2. 线程的创建方式继承Thread和实现Runnable接口(其它方式没有实现)
  3. 详细介绍了线程的生命周期
  4. 分析了start()方法源码;
    读汪文君《java高并发编程详解》