1.线程安全
一般来说,同时提到进程和线程时,会以这样的定义来对比进程和线程:进程是资源分配的基本单位,线程是CPU调度的基本单位。
上述内容是一个总的结论,但是为什么会得到这样一个结论?我们得从进程是如何出现的,线程是如何出现的来解释。
首先我们需要理解程序的执行本质上是CPU指令的执行,我们的程序是由一堆静态的代码文件构成的,这些静态文件(如Java文件)经过一系列编译器、解释器的工作变成了Class文件、JVM指令到最后变成CPU指令。一行代码的执行最后可能变成多行的CPU指令的执行。
在最早期的计算机中,不包括操作系统,计算机从头到尾只运行一个程序,以串行的方式执行指令,并且这个程序能访问计算机中的所有资源(计算资源+存储资源)。如果想要运行多个程序,只能以串行的方式执行多个程序。一次执行完当前的程序后才能执行下一个程序。
但是问题出现了,假设一个程序需要等待外界的输入操作,由于CPU执行的速率特别快,这段等待的时间对于CPU来说无限漫长,这个时候CPU的计算资源就被浪费了,如果在等待的时候,能够运行其他的程序,那么便大大的提高了资源的利用率!
我们希望能够同时运行多个程序,在还是单核处理器的时代,操作系统出现了。操作系统可以让这个程序执行一段时间,然后切换到另一个程序再执行一段时间。使得计算机能够运行多个程序,并且让每个程序在单独的进程中运行。然后操作系统可以为每个进程分配各种资源,如内存、文件句柄等,这就是进程是资源分配的基本单位的由来。
为了让每个进程都能公平使用到计算机的各种资源,一种比较高效的方式出现了,即把CPU执行的时间进行粗粒度的分片——时间片,即进程A运行一个时间片,时间片用完,切换到进程B运行一个时间片。在线程还没有出现的年代,进程是资源分配的基本单位也是CPU调度的基本单位。在这里我们抛开资源分配和CPU调度来为进程做一个简单的定义:运行时的进程就是一个计算机,它拥有计算机资源(存储资源),在运行的这段时间内,CPU执行着这段程序对应的CPU指令(计算资源)。时间片用完,进程切换,下一个进程运行。整个操作可以简化成加载CPU上下文+CPU执行+保存CPU上下文,这里的CPU上下文我们可以简单理解成CPU执行所需要的环境即计算机的相关资源。这个时候我们进入了多进程时代!
同样的,一个问题出现了,假设有两个进程需要访问同一份计算机资源,但是由于进程切换(这里为什么访问同一份计算机资源如内存,进程还需要切换是因为每个进程拥有独立的虚拟内存地址空间,但是同样的虚拟内存空间对应的物理内存地址是不同的,导致了进程之间无法利用直接的内存映射进行进程间通信)的原因,下一个进程仍然需要重新加载一次计算机的资源。CPU执行的时间很快,但是进程切换时的保存上下文和加载CPU上下文的时间却很长,这时候CPU的计算资源就又被浪费了,如果访问同一份资源的时候,能够不需要进程的切换,那么便大大提高了资源的利用率!
这个时候线程出现了,当访问同一份资源时,不需要进行进程的切换,直接就能够得到CPU的执行时间。进程内有两个线程,这两个线程就能直接共享该进程的资源!减少了进程保存和加载上下文的时间。
为了让每个线程都能公平使用到进程的资源,与进程同理,我们需要将CPU执行的时间进行一次更细粒度的分片,即线程A运行一个时间片,时间片用完,切换到线程B运行一个时间片。由于我们是将进程的时间片再进行的一次细分,于是进程便将CPU调度的基本单位这个头衔让给了线程,于是线程就变成了CPU调度的基本单位,因为它的CPU执行时间的粒度更细。这就是线程是CPU调度的基本单位的由来。这个时候我们进入多线程时代!并发由此出现。
于此我们再给出进程与线程的联系和区别,理解起来就更为方便:
联系:
1.一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。
2.资源分配给进程,同一进程的所有线程共享该进程的所有资源。
3.处理器分配给线程,即真正在处理器上运行的是线程。
4.线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。
区别:
对比 | 进程 | 线程 |
---|---|---|
定义 | 进程是程序运行的一个实体的运行过程,是系统进行资源分配和调配的一个独立单位。 | 线程是进程运行和执行的最小调度单位。 |
系统开销 | 创建撤销切换开销大,资源要重新分配和收回。 | 仅保存少量寄存器的内容,开销小,在进程的地址空间执行代码。 |
拥有资源 | 资源拥有的基本单位。 | 基本上不占资源,仅有不可少的资源(程序计数器,一组寄存器和栈)。 |
调度 | 资源分配的基本单位。 | 独立调度分配的单位。 |
安全性 | 进程间相互独立,互不影响。 | 线程共享一个进程下面的资源,可以互相通信和影响。 |
地址空间 | 系统赋予的独立的内存地址空间。 | 由相关堆栈寄存器和和线程控制表TCB组成,寄存器可被用来存储线程内的局部变量。 |
2. 并发与并行
由进程与线程我们引出了并发的概念。接下来我们引出并行的概念,单处理器只能一次运行一个线程,随着处理器的发展,多处理器出现了,每个处理器都能单独运行一个线程,这个时候并行的概念的出现了。
先看一个解释:并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
并发强调的是一个宏观概念,指的是一段时间间隔,可以理解为在一段时间线内多个线程在运行,但是在某个具体的时间点上,只有一个线程在运行;并行是一个微观概念,指的是一个时刻,可以理解为在一个时间点上多个线程在运行,宏观上,在一段时间内有多个线程在运行。所以多线程不一定就是并发,他俩并没有直接的关系。不要把多线程就理解成并发。在这种解释里,多线程并发实际上这些线程通过分时地交替执行(时间片轮转)。而多线程并行只能通过多处理器来实现,单处理器无法实现并行。
但是如果两个线程同时运行,各干各的,操作的是不同的资源,这样很容易就能理解成并行;但是两个线程同时运行,操作的是同一个资源,这样就涉及到临界区资源的竞争,其中一个线程正在操作共享资源,另一个线程则需要排队等待(虽然排队等待,但是线程还是在运行的!不会让出时间片),这还是并行吗?看样子理解成并发可能更好是不是?
这就引出并发与并行的以下第二个解释:并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。不同实体就对应上述操作的不同资源,同一实体就对应上述操作的同一个资源。这个解释不以时间为参考对象,而是以操作的资源为参考对象。
所以并发与并行并不是割裂的,独立的。而是有一定的联系,在上述举例中可以看出并行同时具有并发的含义。并发可以并行,并发也可以不并行。所以对并发与并行可以这么理解:多个线程对资源的操作在时间窗口(时间片)上是否有重叠,也即是否同时运行?有重叠就是并行,不会重叠就是并发。所以按多线程执行的生命周期来看,多线程可能是并发的,也有可能是并行的。
在Java多线程中,线程可能是并发执行,也可能是并行执行。
3.线程安全的定义
由于并发与并行概念的提出,多个线程间对共享资源的操作产生了风险,程序可能会出现不可预测的结果,至此便引出了线程安全。
首先引用Java并发实战作者Brain Goetz对线程安全的定义:“当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么这个类就是线程安全的”。
再看深入理解Java虚拟机作者周志明引用的上述定义:“有多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度方式和交替执行,也不需要额外的同步,调用这个对象的行为都可以获得正确的结果,那么这个对象是线程安全的”。
虽然定义稍有差别,但是目标只有一个:正确的行为或者其行为获得正确的结果。这里正确我们不以纯概念来理解,可以简单的认为是应当出现的结果。举例来说,i=1,两个线程同时执行i++,执行完毕后应当出现的结果或者说正确的结果是3,如果不是3,那就不正确。简化来看:当多线程访问某个类时,这个类都能表现出正确的行为,那么就称这个类是线程安全的;或者说当多线程访问某个类生成的同一个对象时,调用这个对象的行为都可以获得正确的结果,那么可以说这个对象所属的类是线程安全的。这里要声明一下同一个对象,只有操作同一个对象,才会有线程安全和不安全的说法,如果操作的不是同一个对象,则不会有线程不安全的情况。即单例模式存在线程安全的单例模式和线程不安全的单例模式。