什么是进程和线程
进程
进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。
线程
线程是比进程更小的执行单位。一个进程在执行过程中可以产生多个线程。同类的线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈、本地方法栈。所以系统在产生一个线程,或者在线程之间切换工作时,负担要比进程小得多。因此,线程也被称为“轻量级进程”。
程序计数器为什么是私有的
程序计数器有两个作用:
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制。
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程切换回来的时候能够知道该线程上次运行到哪了。
所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。
虚拟机栈和本地方法栈为什么是私有的
- 每个线程运行时需要的内存称为虚拟机栈,每执行一个方法,就创建一个对应的虚拟机栈帧,栈帧中存放局部变量表、操作数栈、常量池引用等信息。
- 本地方法栈和虚拟机栈很类似,区别在于本地方法栈为虚拟机使用到的native方法服务。在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。
堆和方法区
堆和方法区都是所有线程共享的内存空间。堆事进程中最大的一块内存,用于存放新的对象,方法区用于存放已被加载的类的信息、常量、静态变量、即时编译器编译后的代码等数据。方法区是一个概念,堆中的永久代或者元空间是方法区的具体实现。
并发与并行
- 并发:两个及两个以上的作业在同一 时间段 内执行。
- 并行:两个及两个以上的作业在同一 时刻 执行。
同步和异步
- 同步 : 发出一个调用之后,在没有得到结果之前, 该调用就不可以返回,一直等待。
- 异步 :调用在发出之后,不用等待返回结果,该调用直接返回。
为什么使用多线程
先从总体上来说:
- 从计算机底层来说: 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。
- 从当代互联网发展趋势来说: 现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。
再深入到计算机底层来探讨:
- 单核时代: 在单核时代多线程主要是为了提高单进程利用 CPU 和 IO 系统的效率。 假设只运行了一个 Java 进程的情况,当我们请求 IO 的时候,如果 Java 进程中只有一个线程,此线程被 IO 阻塞则整个进程被阻塞。CPU 和 IO 设备只有一个在运行,那么可以简单地说系统整体效率只有 50%。当使用多线程的时候,一个线程被 IO 阻塞,其他线程还可以继续使用 CPU。从而提高了 Java 进程利用系统资源的整体效率。
- 多核时代: 多核时代多线程主要是为了提高进程利用多核 CPU 的能力。举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,不论系统有几个 CPU 核心,都只会有一个 CPU 核心被利用到。而创建多个线程,这些线程可以被映射到底层多个 CPU 上执行,在任务中的多个线程没有资源竞争的情况下,任务执行的效率会有显著性的提高,约等于(单核时执行时间/CPU 核心数)。
多线程可能的问题
内存泄漏、死锁、线程不安全等等
线程的生命周期和状态
Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态:
- New:初始状态,线程被创建出来但没有被调用start()
- Runnable:运行状态,线程被调用了start()等待运行的状态
- Blocked:阻塞状态,需要等待锁释放
- Waiting:等待状态,该线程需要等待其他线程做出一些特定动作(通知或中断)
- Timed_Waiting:超时等待状态,可以在指定时间后自行返回而不是像Waiting一样一直等待
- Terminated:终止状态,表示该线程已经运行完毕
线程创建之后它将处于 NEW(新建) 状态,调用 start()
方法后开始运行,线程这时候处于 READY(可运行) 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 RUNNING(运行) 状态。
在操作系统层面,线程有 READY 和 RUNNING 状态;而在 JVM 层面,只能看到 RUNNABLE 状态,所以 Java 系统一般将这两个状态统称为 RUNNABLE(运行中) 状态 。为什么 JVM 没有区分这两种状态呢?
现在的时分(time-sharing)多任务(multi-task)操作系统架构通常都是用所谓的“时间分片(time quantum or time slice)”方式进行抢占式(preemptive)轮转调度(round-robin 式)。这个时间分片通常是很小的,一个线程一次最多只能在 CPU 上运行比如 10-20ms 的时间(此时处于 running 状态),也即大概只有 0.01 秒这一量级,时间片用后就要被切换下来放入调度队列的末尾等待再次调度。(也即回到 ready 状态)。线程切换的如此之快,区分这两种状态就没什么意义了。
- 当线程执行
wait()
方法之后,线程进入 WAITING(等待) 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态。 - TIMED_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过
sleep(long millis)
方法或wait(long millis)
方法可以将线程置于 TIMED_WAITING 状态。当超时时间结束后,线程将会返回到 RUNNABLE 状态。 - 当线程进入
synchronized
方法/块或者调用wait
后(被notify
)重新进入synchronized
方法/块,但是锁被其它线程占有,这个时候线程就会进入 BLOCKED(阻塞) 状态。 - 线程在执行完了
run()
方法之后将会进入到 TERMINATED(终止) 状态。
什么是上下文切换?
线程在执行过程中会有自己的运行条件和状态(也称上下文),比如程序计数器、栈信息等。当出现如下情况,线程会从占用CPU状态中退出:
- 主动让出CPU,比如调用wait()、sleep()方法
- 时间片用完,因为操作系统要防止一个线程或者进程长时间占用CPU导致其他线程或者进程饿死
- 调用了阻塞类型的系统中断,比如请求IO,线程被阻塞
- 被终止或结束运行
前三种都会发生线程切换,线程切换意味着要保存当前线程的上下文,留待线程下次占用CPU的时候恢复现场,并加载下一个将要占用CPU的线程上下文。这就是所谓的 上下文切换。
上下文切换是现代操作系统的基本功能,因其每次需要保存信息恢复信息,这将会占用 CPU,内存等系统资源进行处理,也就意味着效率会有一定损耗,如果频繁切换就会造成整体效率低下。
什么是线程死锁?
多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
产生死锁的四个必要条件:
- 互斥条件:该资源任意一个时刻只由一个线程占用。
- 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
- 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系
如何预防和避免死锁?
预防死锁
破坏死锁的产生的必要条件即可:
- 破坏请求与保持条件 :一次性申请所有的资源。
- 破坏不剥夺条件 :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
- 破坏循环等待条件 :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
避免死锁
在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。
安全状态 指的是系统能够按照某种线程推进顺序(P1、P2、P3.....Pn)来为每个线程分配所需资源,直到满足每个线程对资源的最大需求,使每个线程都可顺利完成。称 <P1、P2、P3.....Pn> 序列为安全序列。
sleep()方法和wait()方法对比
共同点 :两者都可以暂停线程的执行。
区别 :
sleep()
方法没有释放锁,而wait()
方法释放了锁 。wait()
通常被用于线程间交互/通信,sleep()
通常被用于暂停执行。wait()
方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify()
或者notifyAll()
方法。sleep()
方法执行完成后,线程会自动苏醒,或者也可以使用wait(long timeout)
超时后线程会自动苏醒。sleep()
是Thread
类的静态本地方法,wait()
则是Object
类的本地方法。
为什么wait()方法不定义在Thread中?
wait()是让获得对象锁的线程实现等待,会自动释放当前线程占有的对象锁。每个对象都拥有对象锁,既然是要释放对象锁,自然是对对象进行操作而非线程。
反之,sleep()方法是让当前线程暂停执行,不涉及到对象锁,也不需要获得对象锁。
可以直接调用Thread类的run方法吗?
new一个Thread,线程就进入了新建状态。调用start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。start()会执行线程的相应准备工作,然后自动执行run()方法的内容,这是真正的多线程。但是,直接执行 run()
方法,会把 run()
方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
总结:调用 start()
方法方可启动线程并使线程进入就绪状态,直接执行 run()
方法的话不会以多线程的方式执行。