1、进程与线程
1.1、进程
进程可以看作是程序的执行过程。一个程序的运行需要CPU时间、内存空间、文件以及I/O等资源。操作系统就是以进程为单位来分配这些资源的,所以说进程是分配资源的基本单位。
(1)、进程是动态的,程序是静态的
程序是静态的,它本身作为一种软件资源可以长期保存在磁盘(常说的硬盘)中。比如QQ,QQ作为一个程序,其本身保存在计算机的磁盘上。此时,它并没有得到CPU、内存、I/O等资源。因此当前的QQ程序只是一个静态的程序并不能给我们实现视频、语音等功能。
但当QQ程序开始执行时,操作系统就会将QQ程序从磁盘中装入内存,同时也会在操作系统中创建属于QQ的进程,这些新创建的QQ进程会得到操作系统分配的CPU、内存、I/O等资源,得到这些资源后,创建出来的QQ进程就可以实现视频和语音的功能。而当我们点击退出QQ后,这些进程就会立刻消亡,分配得到的资源也会被释放。
由此可以看出,QQ进程是QQ程序的执行过程,它是动态的,有一定的生命周期,会动态的产生和消亡。进程是资源分配的单位。
(2)、程序与进程并不是一 一对应的关系
虽然进程可以看作是程序的执行过程,但并非一个程序对应一个进程,即二者并不是一 一对应的关系。程序与进程的关系可能有以下几种:
①一个程序产生一个进程:比如Win10的记事本程序(notepad.exe),每打开一个txt文本文件,就只会启动一个记事本进程。
②一个程序产生多个进程:比如浏览器启动时,一般都会产生多个进程,这些进程相互配合,互相影响,共同实现浏览器的功能。
③一个程序可以被多个进程共用:比如一个记事本程序在执行时,就只会产生一个进程。但当我们再用记事本程序打开一个文件时,此时就会再次在操作系统中创建一个新的进程,这个新的进程同样也会调用记事本程序。即在此刻,计算机磁盘中只有一个记事本程序,但是操作系统中却有两个记事本进程在共用这个程序,且这两个进程互不影响。
④一个进程又可能要用到多个程序:比如,用C语言写了一个helloword.c的程序。此时,输入命令gcc helloword.c
。那么操作系统会创建一个进程,它调用c编译程序,对helloword.c文件进行编译。这个进程在执行编译的过程中,除了调用c编译程序和我们编写的helloword程序外,还会用到c预处理程序、连接程序、结果输出程序等。
1.2、线程
线程从属于进程,只能在进程的内部活动,多个线程共享进程所拥有的的资源。如果把进程看作是完成许多功能的任务的集合,那么线程就是集合中的一个任务元素,负责具体的功能。虽然CPU、内存、I/O等资源分配给了进程,但实际上真正利用这些资源并在CPU上执行的却是线程,即真正完成程序功能的是线程。
因为进程作为这些资源的拥有者,它的负载很重,在进程的创建、切换、删除过程中的时间和空间开销都很大。所以目前主流的操作系统都只将进程作为资源的拥有者,而把CPU调度和运行的属性赋予了线程。
比如打开浏览器程序,会产生相应的进程,浏览器进程中包含有许多线程,如HTTP请求线程,I/O线程,渲染线程,事件响应线程等。浏览器进程拥有着内存和I/O资源等,但当我们在浏览器中输入文字时,真正使用I/O资源接收我们输入的文字,并在CPU处理文字的却是浏览器进程中的I/O线程。即真正完成浏览器文字输入功能的是线程。
现代很多操作系统支持让一个进程包含多个线程,从而提高程序的并行程度和资源的利用率。
1.3、线程与进程的关系
①一个进程可以有多个线程,但至少要有一个线程,并且一个线程只能在一个进程的地址空间内活动。
②资源分配给进程,而一个进程内的所有线程共享该进程的所有资源。
③CPU分配给的是线程,即真正在CPU上运行的是线程。
④进程间通信较为复杂,同一台计算机的进程通信称为 IPC(Inter-process communication)。
而不同计算机之间的进程通信,则需要通过网络,并遵守共同的协议,例如 HTTP等。
⑤线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量。
⑥线程更轻量,线程上下文切换成本一般上要比进程上下文切换低。
2、Java中的进程与线程
2.1、JVM进程
我们知道Java语言是需要运行在JVM上的。实际上,JVM也是一个软件程序,这就意味着它执行起来也会在操作系统中创建进程,即JVM进程,通常又叫JVM实例。而我们所写的main方法,实际上就是JVM进程中主线程的所在。
从操作系统的角度来看,我们常说的Java程序,应该包括JVM和我们编写的Java代码。
当我们写完Java代码,并编译成class文件后,使用Java命令执行main方法;或者直接在IDE启动main方法时,JVM程序就会执行,操作系统会将其从磁盘中装入内存,并创建一个JVM进程,随后启动主线程,主线程会去调用某个类的 main 方法,因此这个主线程就是我们写的main方法所在。
实际上,JVM本身就是一个多线程应用,即使我们在代码中并没有手动的创建线程,JVM进程也并不是只有一个主线程,而是也会有其他线程。这些线程完成着JVM的功能,如GC线程负责回收JVM使用过程中的垃圾对象。JVM进程启动完成后,必然会有的线程如下:
至此,我们知道了,启动一个Java程序,本质上就是启动JVM程序,并在操作系统中创建一个JVM进程。这个JVM进程会由操作系统分配许多资源,如内存、I/O等。JVM进程中包含有许多线程,这些线程共享JVM进程分配到的资源,同时这些线程也是CPU核心上执行的实体,它们完成着JVM所具有的功能。
那么如果我们启动两个Java程序,会生成多少个JVM进程呢?
我们编写两个Java程序,其有代码如下:
processTest01程序
public class processTest01 {
public static void main(String[] args) throws InterruptedException {
System.out.println("我是测试01");
byte[] a = new byte[1024*1024*50]; //在堆中占50MB
processTest02 test02 = new processTest02();
Thread.sleep(1000*60*30); //休眠三十分钟
}
}
processTest02程序
public class processTest02 {
public static void main(String[] args) throws InterruptedException {
System.out.println("我是测试02");
byte[] a = new byte[1024*1024*900]; //在堆中占900MB
Thread.sleep(1000*60*30); //休眠三十分钟
}
}
我们将编译这两个程序,并分别用Java命令启动它们。看看两个Java程序会在操作系统中创建了多少个进程。
打开JDK自带的jvisualvm.exe
,这是JDK提供的查看Java进程和线程相关信息的工具程序,在自己电脑上的JDK目录下(Win10):Java\jdk1.8.0_131\bin
。如图所示:
可以继续使用jvisualvm
,查看进程中线程的相关情况:
pid(进程号)为2146的进程:
pid(进程号)为4196的进程:
可以看出,启动多少个Java程序,就会创建多少个JVM进程,也称之为JVM实例。而每一个JVM实例都是独立的,它们互不影响。这也是前面所说的一个程序可以被多个进程共用的情况。
一个JVM进程就是一个JVM的实例。JVM的实例在执行Java程序的过程中会把它管理的内存划分为不同区域,称之为运行时数据区,如下所示:
2.2、Java的线程
我们知道,线程从属于进程,是CPU调度执行的单位,各个线程共享进程内的资源。目前主流的操作系统都支持了线程。在实现了线程的操作系统中,一个进程中必然有至少一个操作系统的线程,这种属于操作系统的线程被称为内核线程(kernel-Level Thread,KLT)。
而各个应用程序实现多线程的方式主要有三种:
①内核线程1:1实现
内核线程即操作系统本身的线程,1:1意味着程序中的线程与操作系统中的内核线程是直接对应的。这种线程的创建是由操作系统来完成的,同时也是由操作系统来负责调度的。这种内核线程的切换需要硬件支持,切换所需的时间也较长,但其优点是一个线程阻塞了,其他线程也可以执行,则进程就能继续工作。但一般来说,程序中的线程不会直接使用内核线程,而是使用它提供的高级接口,称之为轻量级进程(Light Weight Process,LWP)。虽然名称变了,但其本质上还是操作系统的内核线程。每个轻量级进程,都由一个内核线程支持,所以他们都可以独立调度,由操作系统的调度器(Scheduler)负责调度。总的来说就是,程序中的线程是操作系统的内核线程。
可以看出,使用内核线程1:1实现的程序可以同时在多个CPU核心上跑。也就是说,程序执行产生的一个进程中的多个线程在同一时刻可能会在不同的CPU核心上运行。这对于一个程序来说,大大加快了运行效率。
②用户线程1:N实现
用户线程指的是由用户程序自主实现,不需要操作系统来实现的线程,一个线程不是内核线程,就可以认为是用户线程。用户线程虽然不需要操作系统来实现,但在实现了线程的操作系统中,一个进程中必然要有一个内核线程来支持运行。1:N中的1就是一个内核线程的意思。而N指的是用户程序自主实现的多用户线程,操作系统无法得知这些用户线程的存在,因为这些用户线程都是在用户程序内部建立、切换和销毁的。由于用户线程不需要操作系统的帮助,所以对于用户线程的操作可以非常快,消耗低,且不需要硬件的支持。同时,用户线程的的数量不受操作系统的限制。在没有实现多线程的操作系统中也可以实现多线程程序。但由于用户线程需要映射为内核线程才能执行,所以如果一个线程阻塞,那么所有的线程都将阻塞,进程也无法继续工作。线程的调度也是由用户程序自主实现。总的来说,用户线程就是用户的程序自主实现的线程,多个用户线程对应着一个操作系统的内核线程。
可以看出,用户线程1:N实现的程序,一个进程中的多用户线程在同一时刻只能在一个CPU核心上运行,因为只有一个内核线程支持着这个进程。从操作系统角度来看,这就是一个单线程的进程。当然,如果一个实现了用户线程的程序执行产生了多个进程,那么实际上这个程序也可能在多个CPU核心上跑。目前很少有程序实现这种用户线程了。
③混合N:M实现
混合实现即用户线程和内核线程一起使用的实现方式。在这种混合实现下,即存在用户线程,又存在轻量级进程(内核线程)。用户线程还是由用户程序自主实现,这样用户线程的创建、切换、销毁依然快速且消耗低。而一个用户线程的集合(包含一个或多个用户线程)又与一个内核线程映射。多个用户线程的集合,就是N:M实现中的N,而M自然指的是多个内核线程。这样的情况下,也可以继续使用操作系统的调度功能,而且由于一个内核线程支持着一个用户线程的集合,所以一个用户线程阻塞,并不会阻塞其他用户线程,进程也能继续工佐。总的来说,混合实现就是一个用户线程的集合对应着一个内核线程,一个进程中会存在多个用户线程集合,则会有多个内核线程来支持运行。
可以看出,由于用户线程集合映射到了一个内核线程上,而一个进程又有多个用户线程集合。所以使用混合实现多线程的程序,进程中也可能存在多个用户线程在不同CPU核心上执行的情况。
Java线程的实现
介绍完程序实现多线程的三种方式,那么Java是如何实现多线程的呢?
首先,Java虚拟机规范并未规定要如何实现多线程,所以Java的线程都是由虚拟机来具体实现,不同的虚拟机实现线程的方式可能都不相同。不过,在JDK1.2之前,早期的虚拟机都采用的是用户线程1:N的实现方式。而在JDK1.3之后,大部分的虚拟机都采用了内核线程1:1实现的方式,包括我们最常用的HostSpot虚拟机。
这就意味着,我们在平常的开发中,不论是JVM程序自己创建的线程,还是我们手动编码创建的线程,实际上都是直接1:1映射到了操作系统的内核线程。 这一内核线程由操作系统来创建,且虚拟机不会去干涉线程调度。Java的线程何时交给CPU核心去执行,交给哪个CPU核心,线程有多少CPU核心的执行时间,线程何时冻结、唤醒等等,都交给操作系统去完成,也都是操作系统全权决定(不过Java虚拟机也可以设置线程优先级来给操作系统的线程调度提供建议)。
3、多线程与并行、并发
两个或两个以上的线程在同一时刻发生就称为并行,如两个线程在同一时刻在两个不同的CPU核心上执行,则可以说这两个线程是并行执行。
两个或两个以上的线程在同一时间段内发生则称为并发,比如两个线程在一个极短的时间段上分别在同一个CPU核心上执行,则可以说这两个线程是并发执行。
并行与并发的关键就在于是否为同一时刻执行,并行是在同一时刻执行,而并发则是在极短的时间内执行。
在一个CPU核心中,线程实际是并发执行的,操作系统中有一个组件叫做任务调度器,将cpu核心的时间片(windows下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于cpu核心在线程间(时间片很短)的切换非常快,给人的感觉是同时运行的,但实际上一个CPU核心同一时刻只能支持一个线程运行。即如果是在单个CPU核心的操作系统中,Java程序(包含JVM)本身虽然是多线程的,但实际上,同一时刻只能有一个Java线程在执行。
但目前的计算机已经很少有单个核心的CPU了,目前即使是个人使用的计算机都是多个核心的CPU了,每个核心都可以独立调度运行线程,这就意味着线程之间可以并行执行。
可以看出,在多核心的CPU下,线程之间是可以并行执行的。但即使是拥有多个CPU核心的计算机,CPU核心的数量始终是有限的,而一个操作系统中的线程数远远多于CPU核心数,所以线程之间大部分情况下是属于并发状态的。即线程之间是在极短时间下交替在CPU核心上执行的。
需要注意的是,在单个CPU核心下,多线程其实是没有太大意义的,因为始终只能有一个线程在CPU核心上执行,而线程间的切换是需要耗费时间和资源的。但多核CPU可以同时执行线程,如果在多核CPU中还是使用单线程,无疑是对CPU的巨大浪费。并发的最主要的目的就是最大限度利用CPU资源。
但并发并不是线程特有的,进程之间也可以并发。有些语言实现并发就是使用进程来进行并发,如PHP。不过Java的并发依然是依赖于多线程,即多线程是Java实现并发的一种方式。