Java 中线程的启动原理分析

前言

作为一名Java程序员,对线程的掌握,是必不可少的,那么,线程是如何发起的?又是如何执行的?通过这篇文章,来进行详细阐述。

1. 多线程概念

        首先,简单来说一下概念,有很多小伙伴还分不清【进程和线程】【并行与并发】的区别。

1.1 进程 & 线程 & 管程(monitor 监视器)

        我们平时使用Java语言写出来的程序都是以 .java 结尾的文件组成的,这些文件是存储硬盘上的静态文件,通过Java虚拟机编译成和平台无关的字节码,也就是变成了.class 结尾的文件。当我们通过main()方法运行这个程序后,这些.class文件会被加载到内存中等待被执行,接着 CPU开始执行这些程序的每一行指令,然后基于这些指令产生相应的结果,我们把这个运行中的程序称为进程。一台正在运行的计算机,可以同时启动多个进程,干不同的事,互不影响。

        假设在这个程序中,有一段逻辑是从磁盘上解析一个文件进行持久化操作,当CPU执行到从磁盘读取数据这个指令时,由于磁盘的IO速度相比 CPU的运算速度来说要慢很多,所以CPU 在等待磁盘I/O 返回的过程中一直处于闲置状态。CPU作为计算机的核心资源,被闲置显然是不合理的。
        分时系统的出现解决了这个问题,分时系统是计算机对资源的一种共享方式,它利用多道程序和CPU时间片调度的方式使得多个用户可以同时使用一台计算机。

        什么是多道程序呢?由于单个程序无法让CPU和I/O设备始终处于忙碌状态,所以操作系统允许同时加载多个程序到内存,也就是说可以同时启动多个进程,系统给这些进程分配独立的地址空间,以保证每个进程的地址不会相互干扰。

        当CPU在执行某个进程中的指令出现I/O或其他阻塞时,为了提高CPU的利用率,操作系统会采用CPU调度算法把闲置的CPU时间片分配给第二个进程,当前进程运行结束后又会把CPU时间片分配给之前阻塞的进程来执行,从而保证CPU一直处于忙碌状态,整个调度过程如下图所示。

        在多核CPU架构中运行多个进程,从而实现多个进程的并行执行,一切看起来很美好,那么为什么又要有线程这种设计呢?

        原因是进程本身是一个比较重的设计。首先,每个进程需要有自己的地址空间,并且每次涉及进程切换时,需要保存当前CPU指令上下文,使得资源的消耗及性能的损耗比较大。其次,对一个独立的进程来说,该进程内同一时刻只能做一件事,如果在这个进程中想实现同时执行多个任务并行执行,很显然是做不到的。最重要的是,当进程中某个代码出现阻塞时,会导致整个进程挂起,即便有些逻辑不依赖于该阻塞的任务也会无法执行,为了解决这个问题,人们把进程的资源分配和进程中任务调度的执行分开处理,因此形成了线程的概念。

        引入线程的设计后,CPU的最小调度和分配单元就变成了线程,在一个进程中可以创建多个线程。因此,当出现上面描述的情况,即一个进程中存在多个任务时,我们可以针对每个任务分配独立的线程来执行,当其中一个任务因为阻塞无法执行时,其他任务不会受到影响。   

        除此之外,线程的好处还有很多。

  • 由于线程不需要分配操作系统资源,所以它相对进程来说是比较轻的。

  • 线程的切换只需要保存少量的寄存器内容,相比进程来说,资源耗费更小,因此效率更高。

  • 一个进程中可以创建多个线程,同一个进程中多个线程的CPU时间片的切换并不会导致
    进程切换,而且还能实现单进程中多个任务的并行执行。

总结一下,进程和线程的主要区别是:

操作系统的资源管理方式不同,进程有独立的地址空间,当一个进程崩溃后,不会影响其他的进程。而线程是一个进程中不同的执行路径,它有自己的堆栈和 局部变量,但是没有单独的地址空间。

管程:Monitor(监视器),也就是我们平时所说的锁。这个需要从 JVM 层面进行说明 ,也就是 JVM 对“对象头(Mark Word)”中锁的监控,此篇文章不做扩展,后面在 synchronized 锁的内容篇幅会进行详细说明。

① Monitor其实是一种同步机制,它的义务是保证(在同一时间)只有一个线程可以访问被保护的数据和代码;
② JVM中同步时基于进入和退出的监视器对象(Monitor,管程);每个对象实例都有一个Monitor对象;
③ Monitor对象和JVM对象一起销毁,底层由C来实现;

1.2 并行 & 并发

        从操作系统层面来说,并发和并行可以表示 CPU 执行多个任务的方式。

  • 并发:并发是指两个或者多个任务在同一时间间隔内发生,我们生活中常见的案例,比如 12306 抢票、电商网站的秒杀等;
  • 并行:当有多个 CPU 核心是,在同一个时刻可以同时运行多个任务,这种方式叫做并行。比如打开某个 Word 文档的时候,同时也打开另外一个 Excel 文档;

       总的来说,并行和并发的区别就是,多个人做多件事和一个人做多件事的区别。

2. Java 中创建线程的方式

2.1 继承 Thread类


	public class ThreadDemo{
	    public static void main(String[] args) {
	        //4.创建Thread类的子类对象
	        MyThread myThread=new MyThread();
	        //5.调用start()方法开启线程
	        //[ 会自动调用run方法这是JVM做的事情,源码看不到 ]
	        myThread.start();
	        for (int i = 0; i < 100; i++) {
	            System.out.println("我是主线程"+i);
	        }
	    }
	}
	class MyThread extends Thread{
	    //2.重写run方法
	    public void run(){
	        //3.将要执行的代码写在run方法中
	       for(int i=0;i<100;i++){
	           System.out.println("我是线程"+i);
	       }
	    }
	}

2.2 实现 Runnable 接口

public class RunnableDemo {
    public static void main(String[] args) {
        //4.创建Runnable的子类对象
        MyRunnale mr=new MyRunnale(); 
        //5.将子类对象当做参数传递给Thread的构造函数,并开启线程
        //MyRunnale taget=mr; 多态
        new Thread(mr).start();
        for (int i = 0; i < 1000; i++) {
            System.out.println("我是主线程"+i);
        }
    }
}

//1.定义一个类实现Runnable
class MyRunnale implements Runnable{
    //2.重写run方法
    @Override
    public void run() {
        //3.将要执行的代码写在run方法中
        for (int i = 0; i < 1000; i++) {
            System.out.println("我是线程"+i);
        }
    }
}

2.3 实现 Callable 接口(带返回值)

	/*
	创建线程的方式三: 实现callable接口 ---JDK 5.0 新增
	1.创建一个实现Callable接口的实现类
	2.实现call方法,将此线程需要执行的操作声明在call()中
	3.创建callable接口实现类的对象
	4.将此callable的对象作为参数传入到FutureTask构造器中,创建FutureTask的对象
	5.将FutureTask对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用star
	6.获取callable接口中call方法的返回值
	* */
	public class ThreadNew {
	    public static void main(String[] args) {
	        //3.创建callable接口实现类的对象
	        NumThead m=new NumThead();
	        //4.将此callable的对象作为参数传入到FutureTask构造器中,创建FutureTask的对象
	        
	        FutureTask futureTask = new FutureTask(m);
	        //5.将FutureTask对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()方法
	        //FutureTask类继承了Runnable接口
	        //new Runnable = futrueTask;
	        new Thread(futureTask).start();
	
	        //6.获取callable接口中call方法的返回值
	        try {
	            //get()方法返回值即为FutureTask构造器参数callable实现类重写的call方法的返回值
	            Object sum = futureTask.get();
	            System.out.println("总和是:"+sum);
	        } catch (Exception e) {
	            e.printStackTrace();
	        }
	    }
	
	}
	//1.创建一个实现Callable接口的实现类
	class  NumThead implements Callable{
	   // class  NumThead implements Callable<Integer>{
	    //2.实现call方法,将此线程需要执行的操作声明在call()中
	    @Override
	    public Object call() throws Exception {
	    //public Integer call() throws Exception {
	        int sum=0;
	        for(int i=1;i<=100;i++){
	            System.out.println(i);
	            sum+=i;
	        }
	        return sum;
	    }
	}

2.4 通过线程池创建

        后面会针对线程池的设计进行单独说明。

3. 线程启动的原理分析

        通过上面我们得知,在 Java中,创建线程的方式有四种,创建完之后,最后都是通过 start()方法来进行线程的启动,那它是如何启动的?我们通过查看源码来分析一下。

        先从我们写的代码来看:

new Thread(() -> System.out.println("===== 线程启动 ====="),"t1").start();

可以看到,在 Java 中,跟到最后,发现是调用了底层的 native 修饰的 start0() 方法。

        navite()方法是一个本地方法,它是非 Java语言实现的一个接口,使用 C/C++语言在其他文件中定义实现。简单来说,native()方法就是在 Java 中声明的可以调用非 Java 语言的方法。

       既然 Thread.java 文件中找不到实现,那么肯定是在 JVM 层面中做了一些处理,于是在openjdk的源码中,我们找到了与其对应的Thread.c 文件,惊奇的发现了下面的内容:

 附上 openjdk 中 Thread.c 源码的地址:jdk8/jdk8/jdk: 00cd9dc3c2b5 src/share/native/java/lang/Thread.c

继续分析,需要下载JVM 源码,下载地址:https://hg.openjdk.java.net/jdk8/jdk8/hotspot/archive/tip.zip

之后,找到对应的 jvm.cpp、thread.cpp 两个文件,具体路径: 

openjdk8\hotspot\src\share\vm\prims\jvm.cpp

openjdk8\hotspot\src\share\vm\runtime\thread.cpp

简单说明:.cpp 文件是 源代码;.hpp 文件是头文件,类似于 Java 中的 import

 打开 jvm.cpp文件,搜索在 Thread.c 文件中对应的方法 JVM_StartThread

 可以看到,JVM 调用了 Thread::start(native_thread) 方法启动,将一个本地线程传入。接着打开 Thread.cpp 文件,搜索 Thread::start,找到了如下内容:

 最终是通过调用了操作系统来完成了线程的启动任务。

至此,Java 中线程的启动过程分析完毕,也清楚的知道了,其实是由JVM 负责调度工作,告知操作系统需要启动线程。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值